시작 전에

해당 포스트에서는 ‘러닝 Go’ 라는 도서를 읽고, 새롭게 알게된 지식들을 작성한 것입니다. 저는 한번 공부해본 분야의 서적을 읽을 경우, 하나하나 세세하게 읽기 보다는 저자의 노하우나 팁을 위주로 읽는 편입니다.


기본 데이터 타입

  • 최신 언어들과 마찬가지로, 초기화를 하지 않은 변수는 기본값인 제로값을 할당합니다.
  • 문자관련 자료형 선언 시, 작은 따옴표와 큰 따옴표가 구분됩니다. 작은 따옴표는 룬 리터럴이고 큰 따옴표는 문자열 리터럴입니다.
  • 상수는 리터럴에 이름을 부여하는 방법입니다(const). 변수를 변경 불가능하게 선언하는 방법은 없습니다.

복합 타입

  • 배열간 비교는 ‘==‘로 가능합니다.
  • [3]int 로 선언한 것은 [4]int와 다른 타입으로 선언되어 집니다. 이것은 배열의 크기를 지정하기 위해 변수를 이용할 수 없다는 것을 의미합니다. 왜냐하면 타입은 실행 중이 아니라 컴파일 중에 반드시 해석되어야 하기 때문입니다.
  • Go가 값에 의한 호출을 사용합니다. append로 전달된 슬라이스는 복사된 값이 함수로 전달됩니다. 이 함수는 복사된 슬라이스에 값들을 추가하고 추가된 복사본을 반환합니다. 그렇기 때문에 함수 호출에 사용한 변수에 반환된 슬라이스를 재할당 해주어야 합니다.
  • 슬라이스가 append 사용으로 capacity 증가가 필요할 때, Go 런타임은 새로운 메모리를 할당하고 기존 데이터를 이전 메모리로부터 새로운 메모리로 복사를 하기 위한 시간이 요구됩니다. 이전에 사용된 메모리는 가비지 컬렉션에서 정리가 됩니다. capacity는 보통 2배가 됩니다. cap는 이 수용력을 리턴하고 len은 현재 array에 있는 길이를 리턴합니다.
  • 슬라이스에서 슬라이스를 가져왔을 때, 실제 데이터의 복사를 만들지는 않습니다. 대신 메모리를 공유하는 두개의 변수를 가지게 되니다. 이는 슬라이스의 요소를 변경하면 요소를 공유하고 있던 모든 슬라이스에 영향이 생긴다는 의미입니다.
  • 원본 슬라이스로부터 독립적인 슬라이스를 생성할 필요가 있다면, copy 함수를 사용하면 됩니다.
  • go의 문자열은 룬으로 이루어 진 것이 아닙니다. 내부적으로 문자열은 일련의 바이트를 사용합니다.

제어

  • 섀도잉 변수는 포함된 불록 내에 이름이 같은 변수가 있는 것을 의미합니다. 섀도우 변수가 존재 하는한 섀도잉 대상이 된 변수는 접근할 수 없습니다.
  • For-Range로 문자열을 순회하면 바이트가 아닌 룬 타입으로 순회됩니다.
  • For-range에 전달된 값은 복사본입니다. 고루틴 사용 시 주의 해야 합니다.
  • switch는 아래 구문까지 실행되지 않는다. 확장된 if문 같은 느낌입니다.

함수

  • 함수에서 리턴값에 변수 명을 지정한 경우, 빈 반환이 가능합니다. (Return 만 쓰는거). 가독성이 좋지 않고 제로값이 반환될 수 있어 추천되지 않습니다.
  • 현대 언어의 특징 중 하나로, go 에서도 함수는 값으로 분류됩니다.
  • 함수 내부에 선언된 함수를 클로저라고 부릅니다.
  • 클로저는 함수의 범위를 제한합니다. 함수가 다른 하나의 함수에서만 호출되는데 여러 번 호출되는 경우 내부 함수를 사용하여 호출된 함수를 숨길수 있습니다. 이는 패키지 레벨에 선언 수를 줄여, 사용되지 않는 이름을 쉽게 찾을 수 있도록 만듭니다.
  • 함수는 값이고 파라미터와 반환값을 사용하여 함수의 타입을 지정할 수 있기 때문에, 파라미터로 함수를 다른 함수로 넘길 수 있다. 함수를 데이터처럼 다루는 것에 익숙하지 않다면, 지역 변수를 참조하는 클로저를 생성하고 해당 클로저를 다른 함수로 전달하는 의미에 대해 생각해 볼 필요가 있습니다.
  • 함수에서 클로저를 반환할 수 있습니다.
  • defer는 호출하는 함수를 둘러싼 함수가 종료 될 때까지 수행을 연기합니다.
func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string) (err error) {
	tx, err := db.BeginTx(ctx, nil) if err != nil {
		return err 
	}
	defer func() {
		if err == nil {
			err = tx.Commit() 
		}
		if err != nil { 
			tx.Rollback()
		} 
	}()
	_, err = tx.ExecContext(ctx, INSERT INTO FOO (val) values $1, value1) 
	if err != nil {
		return err 
	}
	return nil
}

포인터

  • go는 값에 의한 복사를 채택합니다. 즉 파라미터로 struct를 넘길 경우 해당 struct를 수정해도 해당 함수가 종료 된 후 값이 변경되지 않습니다
  • 하지만 map, slice등은 반영이 됩니다. 이유는 맵과 슬라이스 이 둘은 포인터로 구현이 되었기 때문입니다
  • 포인터의 제로 값은 nil 입니다. 슬라이스, 맵, 함수, 채널, 인터페이스 타입들은 포인터로 구현되어 있어 nil값을 할당할 수 있습니다.
  • nil은 특정 타입에 값의 부재를 표현하는 타입이 지정되지 않은 식별자입니다. C 언어의 NULL과 다르게, nil은 0의 다른 이름이 아닙니다. 그래서 nil을 숫자로 바 꾸거나 숫자를 nil로 바꿀 수 없습니다.
  • Go는 원시 값과 구조체 모두를 위해 값으로 사용할지 포인터로 사용할지에 대한 선택을 제공 합니다. 대부분의 경우에는 값으로 사용하는 것을 추천합니다. 값으로 사용하는 것은 데이터가 언제 어떻게 수정 되는지 이해하기 쉬워집니다. 값으로 사용하는 또 다른 이득은 가비지 컬렉션이 해야 하는 일의 양을 줄여준다는 것입니다.
  • 포인터들은 데이터 흐름을 이해하기 어렵게 만들며 가비지 컬렉터에게 추가적인 작업을 줍니다. 함수로 구조체 전달을 포인터로 하여 항목을 채우는 것보다 함수 내에서 구조체를 초기화 하고 반환하는 것이 좋습니다
  • 1 메가바이트 보다 작은 데이터 구조의 경우 실제로 값 타입으로 반환하는 것보다 포인터 타입으로 반환하는 것이 더 느립니다.
  • Go 런타임 내에서 맵은 구조체를 가리키는 포인터로 구현되어 있습니다. 함수로 맵을 넘기는 것은 포인터를 복사한다는 의미 입이다.
  • 불변성에 관점에서도 맵은 최종적으로 어떤 결과가 들어가 있을 것인지를 확인하는 유일한 방법이 맵이 이용된 모든 함수를 추적하는 것뿐이기 때문에 나쁜 선택이 됩니다. 이렇게 하면 API 자체가 문서화가 되는 것을 방해한다. 동적 언어를 사용했다면, 다른 언어의 구조체의 결핍을 위한 대체로 맵이 사용 되서는 안됩니다. Go는 강한 타입 언어입니다. 맵으로 넘기기 보다는 구조체를 사용하도록 해야 합니다.
  • 슬라이스의 내용을 수정 하는 것은 원본 변수에 반영이 되지만, append를 통해 길이를 변경하는 것은 슬라이스의 수용력이 길이보다 큰 경우조차도 원본 변수에 반영되지 않습니다. 그 이유는 슬라이스는 3개의 항목을 가지는 구조체로 구현이 되어 있기 때문입니다.
  • 슬라이스를 사용하는 예시로, 다른 언어에서는 바이트 버퍼를 매번 새로 할당하여 문서를 읽는다. go 에서는 데이터 소스에서 매번 읽을 때마다 새 할당을 반환하기 보다, 일단 바이트 슬라이스를 생성하고 데이터 소스를 읽어 들이는 버퍼로 사용한다.
func something(){
    file, err := os.Open(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    data := make([]byte, 100)
    for {
        count, err := file.Read(data)
        if err != nil {
            return err  
        }
        if count == 0 {
            return nil
        }
        process(data[:count])
    }
}
  • 버퍼를 사용하는 것은 가비지 컬렉터의 작업량을 줄이는 방법 중 하나입니다. 프로그래머들이 말 하는 ‘가비지’는 ‘더 이상 어떤 포인터도 가리키지 않는 데이터’를 의미합니다.
  • Go에서 값 타입(기본 값, 배열, 구조체)을 살펴볼 때, 한가지 공통점을 지닙니다. 컴파일 시점에 해당 타입들이 얼마큼의 메모리를 사용할지를 정확히 알 수 있다는 것입니다. 이것이 크기가 배열 타입의 일부로 간주되는 이유입니다. 배열 크기를 알고 있기 때문에, 힙 대신 스택에 할당할 수 있습니다. 포인터 타입의 크기도 알고 있기 때문에, 이것 또한 스택에 저장됩니다.
  • 함수의 리턴값이 포인터 변수가 된다면, 함수가 종료되었을 때 포인터가 가리키는 메모리 공간(스택)은 더 이상 유효하지 않게 됩니다. 컴파일러가 데이터가 스택에 저장될 수 없다고 판단했을 때, 포인터가 가리키는 데이터는 스택을 벗어났고 해당 데이터는 컴파일러가 힙에 저장하게 됩니다.
  • 다른 언어와 달리 먼저 힙에 저장하지 않는 이유는 다음과 같습니다. 먼저 가비지 컬렉터의 작업 하는데 시간이 든다. 힙에 있는 사용 가능한 모든 메모리 청크를 추적 유지하거나 메모리 블록 이 여전히 유효한 포인터를 가지고 있는지 추적하는 것은 쉬운 일이 아닙니다. 이것은 프로그램이 수행하도록 작성된 내용을 처리하는 것과는 별개로 진행됩니다.
  • 많은 가비지 컬렉션 알고리즘이 작성되어 오면서 두 가지 대략적인 범주로 분류할 수 있다. 그것은 높은 처리량(단일 스 캔에서 가능한 많은 가비지 찾기) 또는 낮은 지연(가능한 빠르게 가비지 스캔을 완료)을 위해 설계가 됩니다.
  • Go 런타임에 사용되는 가비지 컬렉터는 낮은 지연 시간을 선호합니다. 각 가비지 컬렉션 주기는 500 밀리초 보다 적게 소비하도록 설계되었습니다.
  • Go에서 구조체 슬라이스는 모든 데이터가 메모리에 연속적으로 배치됩니다. 이는 빠르게 로드 하고 빠르게 처리할 수 있게 합니다. 구조체를 가리키는 포인터의 슬라이스(혹은 항목이 포인터 인 구조체)는 램 전체에 데이터가 흩어져 있어 읽기 및 처리 속도가 훨씬 느립니다.
  • 자바의 객체는 포인터로 구현이 되어 있어 모든 객체 변수 인스턴스에 대한 포인터만 스택에 할당되고 객체 내의 데이터는 힙에 할당됩니다. 기본 값(숫자, 불리언, 문자)는 완전히 스택에 저장된다. 이것은 자바의 가비지 컬렉터가 많은 작업을 수행해야 함을 의미합니다.
  • 자바의 리스트와 같은 것들은 실제로 포인터 배열에 대한 포인터라는 것을 의미합니다. 그것이 선형 데이터 구조처럼 보인다 해도, 데이터를 읽을 때 띄엄띄엄 접근하여 매우 비효율적입니다.
  • Go가 포인터를 드물게 사용하도록 권장하는 이유는 이와 같습니다. 가능한 많이 스택에 저장하도록 하여 가비지 컬렉터의 작업량을 줄이도록 해야 합니다.
  • 구조체의 슬라이스나 기본 타입은 빠른 접근을 위에 메모리에 연속적으로 데이터를 정렬합니다(스택 영역). 그리고 가비지 컬렉터가 일을 시작할 때, 가장 많은 가비지를 모으는 것보다 빠르게 반환할 수 있도록 최적화되어 있습니다. 이런 접근 방식이 작동하도록 만드는 핵심은 처음부터 가비지를 덜 만들게 하는 것입니다.

go 언어의 가비지 컬렉터에 대해서는 직접 구현해보는 다른 포스트를 작성할 예정입니다.

메서드

  • 메서드가 리시버를 수정한다면, 반드시 포인터 리시버를 사용해야 합니다.
  • 메서드가 nil 인스턴스를 처리할 필요가 있다면, 반드시 포인터 리시버를 사용해야 합니다.
  • 값 타입에도 불구하고 포인터 리시버로 메서드를 호출할 수 있습니다. 값 타입인 지역 변수를 포인터 리시버와 함께 사용하면, Go는 자동으로 지역변수를 포인터 타입으로 변환힙니다. 예를 들어 c.Increment()가 (&c).Increment()로 변환 됩니다.
  • Go 구조체에 대한 getter와 setter 메서드는 작성하지 않는 것을 추천합니다. Go는 각 항목에 직접 접근하는 것을 권장합니다.
  • 메서드를 변수에 할당하거나 타입이 func(int)int의 파라미터로 전달할 수도 있습니다. 이것을 메서드 값 이라 부릅니다.
  • go 언어는 내장 임베딩을 지원힙니다. Struct 안에 inner struct가 있는경우 해당 inner struct 의 값에 바로 접근이 가능합니다.

인터페이스

  • Go의 인터페이스는 다른 객체 지향 언어와 다르게 암묵적으로 구현이 됩니다. 구체 타입은 구현하는 인터페이스를 선언하지 않습니다. 구체 타입을 위한 메서드 세트는 인터페이스를 위한 메서드 세트의 모든 메서드를 포함한다. 구체 타입은 인터페이스 타입으로 선언된 변수나 항목에 할당될 수 있음을 의미합니다. 이 암묵적 행동은 인터페이스가 타입 안정성과 디커플링을 가능하게 하여 정적 및 동적언어의 기능을 연결합니다.
  • 인터페이스에 인터페이스를 임베딩 할 수 있다
  • Go 개발자가 “인터페이스를 받고 구조체를 반환해라”라고 말하는 것을 종종 들었을 것입니다. 이것이 의미하는 것은 함수로 실행되는 비즈니스 로직은 인터페이스를 통해 실행되어야 하는 것이지만, 함수의 출력은 구체 타입이어야 한다는 것이다.

인터페이스에 대해서는 차후 의존성 주입을 직접 해보며 좀더 자세히 공부할 예정입니다.

에러

  • defer 함수를 통해서 오류를 한번에 우아하게 처리할 수 있습니다. Ex. db 작업을 한번애 여러번 사용시 fail이 되면 에러를 리턴하고 defer에서 에러 로직을 공통적으로 처리합니다.

동시성

  • 동시성 프로그래밍은 병렬 프로그래밍을 의미하지 않습니다.
  • 고루틴은 Go 런타임에서 관리하는 가벼운 프로세스입니다. Go 프로그램이 실행이 되면, Go 런 타임은 여러 스레드를 생성하고 프로그램을 실행하기 위해 단일 고루틴을 시작합니다. 프로그램에서 생성된 모든 고루틴은 초기에 생성된 하나를 포함하여, 운영체제에서 CPU 코어에 따라 스레드를 스케줄링을 하듯이 Go 런타임 스케줄러가 자동으로 스레드들을 할당합니다.
  • 기본적으로 채널은 버퍼가 없다. 버퍼가 없는 열린 채널에 쓰기를 할 때마다 다른 고루틴에서 같은 채널을 읽을 때까지 해당 고루틴은 일시 중지됩니다. 비슷하게, 버퍼가 없는 열린 채널에 읽기를 하면 다른 고루틴에서 같은 채널에 쓰기를 할 때까지 해당 고루틴을 일시 중지된다.
  • 버퍼가 있는 채널도 가지고 있다. 이런 채널은 블로킹 없이 제한된 쓰기의 버퍼를 가진다. 채널에서 읽어가는 것 없이 버퍼가 다 채워지면, 채널이 읽어질 때까지 쓰기 고루 틴은 일시 중지된다. 가득 찬 버퍼 블록을 가진 채널에 쓰는 것과 같이 비어 있는 버퍼를 가진 채널로 읽기를 해도 블로킹이 됩니다.
  • 채널을 닫아야 하는 책임은 채널에 쓰기를 하는 고루틴에 있다. 채널 닫기는 해당 채널이 닫혀 지기를 기다리는 고루틴이 있는 경우에만 필요합니다(for-range 루프를 사용해서 채널을 읽는 것과 같은). 채널도 단지 다른 변수이기 때문에, Go의 런타임은 더 이상 사용되지 않는다는 것이 확인되면 가비지 컬렉터로 정리를 합니다.

고루틴, 채널, 컨텍스트 관련해서는 동시성 프로그래밍 in Go를 읽고 더 자세히 공부할 예정입니다.