본문 바로가기
프로그래밍

Golang을 쓰면서 알게된 것들

by 자유코딩 2020. 9. 30.

golang

  • golang은 stack 자료형이 없다.
  • arraylist가 제공되지 않는다. 배열로 append 한다.
  • append는 있지만 delete는 제공되지 않는다.
  • rune 타입이라는게 있다. int 와 같은 형태지만 int와는 다르다.
  • map 이 서로 같은지 비교하려면 reflect.DeepEqual(a,b)를 해야한다.
  • golang은 관례상 error 값이 함수 리턴의 마지막 값이다.
func main() (data, error) {


}
  • golang은 가변길이 파라미터를 지원한다.
package main
import "fmt"
func varchar(txt... string) {
  fmt.Println(txt)
}
func main() {
  varchar("123", "234", "234")
}
  • golang의 메소드를 대문자로 선언하는 이유

    • golang은 따로 private, public이 없다.
    • 메소드 이름을 대문자로 선언하면 다른 모듈에서 보인다.
    • 메소드 이름을 소문자로 선언하면 다른 모듈에서 안 보인다.
  • golang에서 비동기 setTimeout을 사용하는 방법

time.AfterFunc(5*time.Second, func(){ 
  // 콜백으로 실행될 내용
})
fmt.Println("먼저 실행")
// 5초후 실행된다.
  • golang에서는 enum이 따로 없다. 상수를 정의해서 사용한다.
type status int
const (
  UNKNOWN status = iota
  TODO
  DONE
)
func main() {
  fmt.Println(UNKNOWN) // 0 출력
  fmt.Println(TODO) // 1 출력
  fmt.Println(DONE) // 2 출력
}
  • golang은 테스트에서 assertEqual이 없다. 직접 만들어서 써야한다.

  • golang은 값에 의한 호출만 지원한다.

    • call by value
package main

import "fmt"

type Str struct {
    Name string
    Age  int
}

func funcA(str *Str) {
    str.Name = "ChangeName"
    fmt.Println(*str)
}

func main() {
    str := Str{ // 구조체를 만들고
        Name: "hello",
        Age:  10,
    }
    fmt.Println(str)
    funcA(&str) // 주소를 함수에 전달하는 방식
    fmt.Println(str)
}
  • golang의 에러 핸들링
    • if 문에서 err := 로 대입한다. 그리고 err가 nil이 아니면 처리한다.
package main

import (
    "errors"
    "fmt"
)

func funcA() error {
    return errors.New("hello error")
}

func main() {
    if err := funcA(); err != nil {
        fmt.Println("error")
        fmt.Println(err)
    }
}
  • json data의 처리
보통의 json 문자열의 경우에는 json.Unmarshal해서 처리한다.
func main() {
    dd := `{
        "name": "daf",
        "age": 3
    }`

    fmt.Println(reflect.TypeOf(dd))

    var obj interface{}
    err := json.Unmarshal([]byte(dd), &obj)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("--------\n", reflect.TypeOf(obj))
    }
}

Unmarshal 하면 map[string] interface{} 형태의 값을 얻을 수 있다.
  • 그럼 이번엔 코드 데이터를 json 문자열로 변환한다.
type Serial struct {
    Name string
    Age  int
}

func main() {
    serial := Serial{
        Name: "hello",
        Age:  10,
    }
    byteArr, err := json.Marshal(serial)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(byteArr))
}

Serial이라는 구조체를 선언했다.
json.Marshal()을 하면 byte 배열이 나온다.
바이트 배열을 string() 해주면 json 문자열로 변경된다.

  • map[string]string도 Json 형태로 바꿀 수 있다.
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var mapTest map[string]string
    mapTest = make(map[string]string)
    mapTest["hello"] = "hihi"
    jsonMap, err := json.Marshal(mapTest)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(string(jsonMap))
}

map은 make(map[string]string)처럼 make를 해서 인스턴스를 생성해야한다.

  • golang에서 구조체는 필드들의 집합이다.
  • golang에서 인터페이스는 함수들의 집합체이다.
type Person struct {
    Name string
    Age  int
}
type PersonActivity interface {
    Call()
    Work(cnt int)
}

func main() {

}
  • golang에서 구조체와 인터페이스를 같이 쓰는 방법
package main

import "fmt"

type Person struct { // Person 구조체를 정의한다.
// 구조체는 클래스인데 메소드가 없다고 생각하면 된다.
    Age  int
    Name string
}

type IPerson interface { // 인터페이스를 정의한다.
// 인터페이스는 필드 없이 메소드만 있는 클래스라고 생각할 수 있다.
    IsWork() bool
    GetAge() int
    GetName() string
}

func (p Person) IsWork() bool { 
    //함수 앞쪽에 구조체를 정의한다. IsWork를 구현했으므로 IPerson 인터페이스도 구현했다.
    // 이런식으로 해당 구조체의 함수를 정의하고 쓸 수 있다.
    fmt.Println(p)
    return false
}

func main() {
    person := Person{ // 클래스처럼 객체를 만든다.
        Name: "hello",
        Age:  10,
    }
    person.IsWork() //이렇게 전달하면 IsWork에서는 p가 생성한 객체로 전달된다.
}

  • golang defer 함수
    • golang의 defer 함수는 finally 같은 역할을 한다.
    • socket, file close에서 사용할 수 있다.
package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("1.txt")
    defer f.Close()

    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(f)
    // defer를 위에 썼지만 실제로 파일읽기가 close 되는 것은 main 함수가 종료되기 직전이다.
}
  • golang panic
    • panic은 현재 함수를 멈춘다.
    • 현재 함수를 멈추고 defer를 모두 실행하고 리턴한다.
    • recover 함수를 사용하면 panic 상태를 다시 정상으로 만들 수 있다.
package main

import (
    "fmt"
    "os"
)

func main() {
    // 잘못된 파일명을 넣음
    openFile("Invalid.txt") // 1

    // recover에 의해
    // 이 문장 실행됨
    println("Done") // 7
}

func openFile(fn string) {
    // defer 함수. panic 호출시 실행됨
    defer func() { // 4 defer 호출
        if r := recover(); r != nil { // recover가 실행된다.
            fmt.Println("OPEN ERROR", r) // 5 
        }
    }()

    f, err := os.Open(fn) // 2
    if err != nil {
        panic(err) // 3 패닉 발생. 여기서 멈추고 defer 를 호출한다.
    }

    defer f.Close() // 6
}
  • 고루틴, 채널

  • 고루틴의 목적

    • OS 스레드보다 가볍게 비동기 처리를 하기 위해서 만들었다.
    • OS 스레드와 1:1 대응되지 않는다.
    • OS 스레드가 1MB의 스택을 갖는다. 고루틴은 몇 KB의 스택을 갖는다.(필요시 동적으로 증가한다.)
package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 10; i++ {
        fmt.Println(s, "***", i)
    }
}

func main() {
    // 함수를 동기적으로 실행
    say("Sync") // 함수가 10번 실행된다.

    // 함수를 비동기적으로 실행
    go say("Async1") // 자바의 스레드처럼 순서를 보장하지 않는다.
    go say("Async2") // 1,2,3 이 무작위로 번갈아가면서 나온다.
    go say("Async3")

    // 3초 대기
    time.Sleep(time.Second * 3)
}

보통 이렇게 무작위로 실행되는 비동기 작업은 흐름을 제어할 필요가 있다.

wait 를 사용해서 고루틴을 기다릴 수 있다.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wait sync.WaitGroup
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("1")
    }()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("2")
    }()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("3")
    }()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("4")
    }()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("5")
    }()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("6")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("7")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("8")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("9")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("10")
    }()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.

    fmt.Println("adsf")
}

wait.Wait() 부분을 주석처리했다.
이렇게 코드를 작성하면 아래처럼 실행 순서가 섞인다.

4
6
3
2
1
5
7
8
9
adsf
10

실행 순서를 유지하고 싶다면 wait.Wait()를 사용해야한다.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wait sync.WaitGroup
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("1")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("2")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("3")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("4")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("5")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("6")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("7")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("8")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("9")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("9")
    }()

    wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.

    fmt.Println("adsf")
}

wait.Add()
go func()
wait.Wait()

이 3가지는 세트로 같이 사용해야한다.

wait에 추가되지 않으면 몇개의 go func 를 기다려야 하는지 모르기 때문에 기다리지 않는다.
wait.Wait()가 없어도 기다리지 않는다.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wait sync.WaitGroup
    wait.Add(1) // 몇개의 go routine을 기다릴 것인지 지정
    go func() {
        defer wait.Done()
        fmt.Println("1")
    }()
    wait.Wait()

    // wait.Wait() // wait가 있기 때문에 asdf 보다 hello hi가 항상 먼저 나온다.

    fmt.Println("adsf")
}

channel을 만들고 데이터 보내기

package main

import (
    "encoding/json"
)

func main() {
    ch := make(chan map[string]string)
    go func() {
        m := make(map[string]string)
        m["a"] = "df"
        ch <- m
    }()

    i := <-ch
    jsonI, err := json.Marshal(i)
    if err != nil {
        println(err)
    }
    println(string(jsonI))
}

channel을 사용하면 wait를 쓰지 않고 wait를 할 수 있다.

channel을 써서 여러개의 데이터를 처리하려면 이렇게 한다.

package main

func main() {
    c := make(chan int)
    go func() {
        c <- 1
        c <- 2
        c <- 3
        c <- 4
        close(c) // 안 쓰면 
        //fatal error: all goroutines are asleep - deadlock! 에러가 발생한다.
    }()
    for num := range c {
        println(num)
    }
}

채널이 열렸는지 닫혔는지도 알 수 있다.

package main

func main() {
    chValue := make(chan int)
    go func() {
        chValue <- 1
        close(chValue)
    }()
    num, ok := <-chValue
    println(num) // 채널로 받아온 값 출력
    println(ok)  // 채널이 닫혔는지 여부
}
  • 채널의 버퍼 크기를 직접 정할 수도 있다.
  • 하지만 권하지 않는다.
  • 제한된 버퍼가 가득차면 고루틴이 채널에 보내는 곳에서 멈춘다.
  • 버퍼 없는 채널로 만들고 필요에 따라 조절하는 것이 좋다.

솔직한 후기

1. 일단 없는게 굉장히 많았다.

enum 없다.

클래스 없다.

interface와 구조체를 활용하면 된다는데 그것보단 한 클래스 안에 상태와 메소드를 둘 다 정의하는게 훨씬 안 헷갈리고 편하다.

삼항연산자도 없다. 삼항연산자는 잘쓰면 그 자리에 값을 할당할 수 있어서 정말 편했다.

go func()를 사용해서 비동기 제어를 하는게 잘 눈에 들어오지 않는다.

예를 들어서 js/ts라면 async await 를 쓰면 알아서 된다.

async func() {
    try{
        await 비동기작업
        await 비동기작업
        await 비동기작업
        await 비동기작업
        await 비동기작업
        await 비동기작업
    } catch(error) {
        // 에러 핸들링, 로깅
    }
}

저렇게만 써도 비동기 작업의 순서를 제어할 수 있다.

물론 js/ts는 콜스택에서 싱글스레드로 동작한다. golang 은 애초에 멀티 스레드를 경량화해서 동작하기 때문에 이런것은 장점이다.

하지만 이 장점을 생각하면 차라리 코틀린을 쓰는게 낫겠다는 생각이 들었다.

코틀린을 쓰면 일단 자바에서 하는 일은 다 할 수 있다. 자바가 그동안 쌓아온 서비스 개발의 레퍼런스를 모두 활용할 수 있다.

거기에 코루틴도 쓸 수 있다. 코루틴과 고루틴은 굉장히 비슷하다.

코틀린을 쓰면 클래스, 인터페이스 모두 다 쓸 수 있다.

json 을 다루는 부분에서도 굉장히 별로였다.

golang 은 encoding/json 패키지의 Marshal, Unmarshal 을 활용해서 데이터를 다루게 된다.

byte[] 로 형변환을 한 후에 처리하게 되는데 이게 되게 번거롭다.

js 였다면 언어차원에서 json을 지원한다.

서버로 들어온 데이터를 그냥 읽으면 된다.

kotlin 같은 경우엔 java의 auto mapper 를 쓰거나 하면 편하게 쓸 수 있다.

그동안 생겨난 라이브러리의 숫자를 봐도 golang 과 jvm은 비교가 안된다.

npm 패키지는 35만개가 넘는 라이브러리가 있다.

이걸 왜 쓰는지 잘 이해가 되지 않는 느낌이었다.

고루틴과 코틀린 코루틴 사이의 성능 차이를 안다면 더 확실한 의견을 가질 수 있을 것 같다

댓글