2024-01-18-TIL
2024-01-18-TIL
Today I Studied
Go의 기본 문법과 타입 시스템을 주제별로 정리한다. Java에 익숙한 입장에서 다르게 동작하는 부분(값 전달, 리시버, 슬라이스, 에러 처리)을 중심으로 본다.
Go References
언어 동작의 최종 기준은 The Go Programming Language Specification이고, 패키지와 모듈을 어떻게 구성하고 빌드하는지는 How to Write Go Code에 정리되어 있다. Go by Example은 기능별로 짧은 실행 예제를 보여 주어 문법을 빠르게 훑기에 좋다.
golang-standards/project-layout은 cmd/, internal/, pkg/ 같은 디렉터리 구성을 제안하는 저장소인데, README에서 스스로 Go 팀이 정한 공식 표준이 아니라고 밝힌다. 이 중 internal/만은 언어 도구가 실제로 강제하는 규칙으로, internal 아래 패키지는 그 부모 디렉터리 트리 안에서만 import할 수 있다.
- https://go.dev/ref/spec
- https://go.dev/doc/code
- http://golang.site/
- https://gobyexample.com/
- https://github.com/golang-standards/project-layout
Go vs Elixir
Go와 Elixir는 둘 다 동시성을 언어의 중심에 둔다. Go는 고루틴과 채널을 쓰는 네이티브 컴파일 언어이고, Elixir는 Erlang VM(BEAM) 위에서 경량 프로세스와 메시지 전달로 동시성을 다루는 함수형 언어다. 아래 글은 백엔드 개발에서 Elixir 대신 Go를 선택하는 이유를 다루는 의견 글이다.
- https://medium.com/@itskenzylimon/why-choose-golang-over-elixir-in-backend-development-71ed82a2571c
Go Module
모듈은 함께 버전이 매겨지는 패키지의 묶음이다. 루트의 go.mod에 모듈 경로, Go 버전, 의존성(require)을 적고, go.sum에는 내려받은 의존성의 체크섬이 기록되어 빌드 재현성과 무결성을 보장한다. 자주 쓰는 명령은 다음과 같다.
1
2
3
go mod init example.com/hello # go.mod 생성
go get example.com/pkg@v1.2.3 # 의존성 추가 또는 버전 변경
go mod tidy # 쓰지 않는 의존성 제거, 누락된 의존성 추가
Go 모듈은 의존성 버전을 고를 때 Minimal Version Selection을 사용한다. 요구된 버전들 중 조건을 만족하는 가장 낮은 버전을 고르기 때문에, 새 버전이 나왔다고 빌드 결과가 저절로 바뀌지 않는다.
- https://go.dev/ref/mod
- https://go.dev/blog/using-go-modules
- https://www.digitalocean.com/community/tutorials/how-to-use-go-modules
- https://go.dev/doc/modules/gomod-ref
- https://soyoung-new-challenge.tistory.com/130
- https://levelup.gitconnected.com/go-for-java-devs-compiling-packages-and-modules-and-built-in-functionality-672597c19ae4
- https://deku.posstree.com/ko/golang/module/
Golang Frameworks
Go는 표준 라이브러리의 net/http만으로도 웹 서버를 만들 수 있어서, 프레임워크는 라우팅, 미들웨어, 요청 바인딩 같은 편의 기능을 더하는 얇은 계층에 가깝다. Gin, Echo, Fiber 같은 프레임워크가 자주 비교된다. JetBrains Developer Ecosystem 2023 결과 페이지에서 Go 개발자들이 실제로 쓰는 프레임워크와 도구의 분포를 볼 수 있다.
- https://masteringbackend.com/posts/top-5-golang-frameworks
- https://www.jetbrains.com/lp/devecosystem-2023/go/?source=google&medium=cpc&campaign=APAC_en_DC_Devecosystem2023_results_Languages_Search&term=golang%20frameworks&content=685424361589&gclid=Cj0KCQiA2KitBhCIARIsAPPMEhKgsJjbzPm4xar8gt57ZcEXVDmW01HhZys3UnBQzJVkQBhEHddesWQaAlV_EALw_wcB
Go Generics
Go 1.18에서 타입 파라미터가 도입되었다. Go 블로그의 When To Use Generics는 기준을 이렇게 정리한다. 슬라이스, 맵, 채널처럼 언어가 제공하는 컨테이너를 다루는 함수나 연결 리스트 같은 범용 자료구조에는 타입 파라미터가 맞다. 반대로 값에 대해 메서드만 호출하면 되는 경우에는 인터페이스를 쓰라고 권한다. 같은 코드를 타입만 바꿔 여러 번 쓰고 있다면 그때 타입 파라미터를 고려하라는 것이 핵심이다.
interface{}(Go 1.18부터 any)는 어떤 값이든 담을 수 있지만, 꺼낼 때 타입 단언이 필요하고 잘못된 타입은 런타임에야 드러난다. 타입 파라미터는 같은 유연성을 컴파일 타임 타입 검사와 함께 제공한다.
1
2
3
4
5
6
7
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, 0, len(s))
for _, v := range s {
r = append(r, f(v))
}
return r
}
- https://go.dev/blog/when-generics
- https://stackoverflow.com/questions/45171642/how-is-the-empty-interface-different-than-a-generic
- https://eli.thegreenplace.net/2020/embedding-in-go-part-3-interfaces-in-structs/
Go Interfaces
Go의 인터페이스는 암묵적으로 구현된다. Java처럼 implements를 선언하지 않고, 인터페이스가 요구하는 메서드를 모두 가진 타입이면 그 인터페이스를 만족한다. 그래서 인터페이스는 사용하는 쪽에서 필요한 만큼 작게 정의하는 경우가 많다.
- https://gobyexample.com/interfaces
Go Struct and Interface Embedding
Go에는 상속이 없고 임베딩으로 조합한다. 구조체에 다른 타입을 필드 이름 없이 넣으면 그 타입의 필드와 메서드가 바깥 구조체로 승격(promote)되어 직접 호출할 수 있다. 인터페이스에 인터페이스를 임베딩하면 메서드 집합이 합쳐진다(io.ReadWriter가 io.Reader와 io.Writer를 임베딩하는 식). 구조체에 인터페이스를 임베딩하면 그 구조체는 인터페이스를 만족하지만, 임베딩된 값이 nil인 상태에서 직접 구현하지 않은 메서드를 호출하면 panic이 난다는 점을 Eli Bendersky의 글이 짚는다.
- https://ankur-a22.medium.com/struct-and-interface-embedding-f0da8517fa8a
Go Method Receiver: Value Receiver vs Pointer Receiver
메서드는 리시버를 가진 함수다. 값 리시버(func (c Counter) Get())는 호출 시 리시버의 복사본을 받으므로 메서드 안에서 필드를 바꿔도 원본에 반영되지 않는다. 포인터 리시버(func (c *Counter) Inc())는 원본을 가리키므로 상태를 바꿀 수 있고 큰 구조체의 복사 비용도 피한다.
명세상 타입 T의 메서드 집합에는 값 리시버 메서드만, *T의 메서드 집합에는 둘 다 포함된다. 그래서 포인터 리시버 메서드로 인터페이스를 구현했다면 T 값은 그 인터페이스를 만족하지 않는다. Go FAQ는 한 타입의 메서드가 일부라도 포인터 리시버라면 일관성을 위해 전부 포인터 리시버로 맞추라고 권한다.
- https://medium.com/globant/go-method-receiver-pointer-vs-value-ffc5ab7acdb
- https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver
- https://blog.devgenius.io/receiver-parameter-in-go-f0c3e25b7b10
- https://medium.com/@adityaa803/wth-is-a-go-receiver-function-84da20653ca2
- https://medium.com/the-bug-shots/understanding-value-and-pointer-receivers-in-golang-82dd73a3eef9
Go Generics vs Interfaces
인터페이스 값은 런타임에 동적 타입 정보와 데이터를 함께 들고 다니며 메서드 호출이 간접 호출로 이루어진다. 제네릭은 컴파일 시점에 타입이 정해지므로 타입 안전성이 더 강하다. 다만 Go의 제네릭 구현은 타입마다 코드를 전부 따로 만들지 않고, 메모리 모양(GC shape)이 같은 타입끼리 코드를 공유하면서 딕셔너리로 타입 정보를 넘기는 방식이라 항상 인터페이스보다 빠른 것은 아니다.
- https://blog.stackademic.com/golang-design-generics-vs-interfaces-how-it-really-works-under-the-hood-c1e5057a7ae6
Go Arrays vs Slices
배열은 길이가 타입의 일부([4]int와 [5]int는 다른 타입)이고, 대입하거나 함수에 넘기면 전체가 복사되는 값이다. 슬라이스는 내부 배열을 가리키는 포인터, 길이, 용량으로 이루어진 작은 구조이고 실무에서는 대부분 슬라이스를 쓴다.
- https://blog.soumendrak.com/go-arrays-vs-slices
Go Slices Copy
슬라이스를 함수에 넘기면 슬라이스 헤더(포인터, 길이, 용량)가 값으로 복사된다. 헤더는 복사본이지만 같은 내부 배열을 가리키므로 함수 안에서 s[0] = x로 바꾼 요소는 호출한 쪽에서도 보인다. 반면 append가 용량을 넘겨 새 배열을 할당하면 그 뒤의 변경은 원본과 분리된다. 독립된 사본이 필요하면 내장 함수 copy를 쓴다. copy는 두 슬라이스 길이 중 작은 쪽만큼만 복사하므로 대상 슬라이스를 먼저 같은 길이로 만들어야 한다.
1
2
dst := make([]int, len(src))
copy(dst, src)
- https://medium.com/@ansujain/understanding-slices-in-go-pass-by-value-and-pass-by-pointer-9a52830c741e
- https://trstringer.com/golang-slice-references/
- https://stackoverflow.com/questions/39993688/are-slices-passed-by-value
- https://pyrasis.com/book/GoForTheReallyImpatient/Unit22/03
- https://freshman.tech/snippets/go/copy-slices/
- https://gosamples.dev/copy-slice/
Go Maps
맵은 해시 테이블 기반의 내장 타입이다. Go 블로그는 맵을 포인터나 슬라이스처럼 참조 타입이라고 설명한다. 초기화하지 않은 nil 맵은 읽기는 되지만 쓰면 panic이 나므로 make나 리터럴로 만들어야 한다. 순회 순서는 정해져 있지 않고, 여러 고루틴이 동시에 쓰면 안전하지 않다. v, ok := m[k] 형태로 키 존재 여부를 구분한다.
- https://go.dev/blog/maps
Go Struct
구조체는 필드의 묶음이고 대입이나 함수 인자로 넘길 때 값 전체가 복사된다. 필드 이름이 대문자로 시작하면 패키지 밖으로 공개(exported)되고, 소문자면 패키지 내부에서만 보인다.
- https://go.dev/tour/moretypes/2
- https://deku.posstree.com/ko/golang/struct/
- https://www.w3schools.com/go/go_struct.php
- https://gobyexample.com/structs
Go Constant Struct
Go 명세에서 상수는 불리언, 룬, 정수, 부동소수점, 복소수, 문자열만 될 수 있다. 그래서 구조체 상수는 만들 수 없고, 패키지 수준 var로 두거나 매번 새 값을 반환하는 함수로 대신한다.
- https://stackoverflow.com/questions/43368604/constant-struct-in-go
Go Call by Value vs Call by Reference
Go FAQ에 따르면 Go의 모든 인자는 값으로 전달된다. 포인터를 넘기면 포인터 값이 복사되고, 슬라이스나 맵을 넘기면 내부 데이터를 가리키는 헤더가 복사된다. 그래서 참조처럼 보이는 동작도 사실은 “참조를 담은 값의 복사”로 이해하면 앞의 슬라이스 동작이 일관되게 설명된다.
- https://blog.naver.com/pjt3591oo/221598199432
Go Functions vs Methods
함수는 독립적으로 선언되고, 메서드는 특정 타입에 리시버로 묶인 함수다. 메서드는 같은 패키지에 정의된 타입에만 선언할 수 있다.
- https://velog.io/@kameals/Golang-functions-vs-methods
Go Functional Programming
Go에서 함수는 일급 값이라 변수에 담고, 인자로 넘기고, 클로저로 반환할 수 있다. 제네릭 도입 이후에는 Map, Filter 같은 고차 함수를 타입 안전하게 작성할 수 있게 되었다.
- https://bitfieldconsulting.com/golang/functional
What is Panic in Golang
panic은 정상 흐름을 멈추고 현재 함수의 지연 함수(defer)를 실행한 뒤 호출 스택을 거슬러 올라가는 동작이다. 지연 함수 안에서 recover를 호출하면 panic을 멈추고 정상 흐름으로 돌아올 수 있으며, recover는 지연 함수 안에서만 의미가 있다. Go에서 panic은 프로그래밍 오류처럼 복구할 수 없는 상황에 쓰고, 예상 가능한 실패는 에러 값으로 다룬다.
1
2
3
4
5
6
7
8
func safeDiv(a, b int) (q int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil
}
- https://levelup.gitconnected.com/golang-understanding-panic-with-examples-34add963bb41
- https://www.educative.io/answers/what-is-panic-in-golang
- https://www.digitalocean.com/community/tutorials/handling-panics-in-go
- https://go.dev/src/runtime/panic.go
- https://phsun102.tistory.com/109
- https://go.dev/blog/defer-panic-and-recover
- https://gobyexample.com/panic
Error Handling in Go
Go의 에러는 Error() string 메서드를 가진 error 인터페이스 값이다. 함수는 결과와 함께 에러를 반환하고, 호출자는 if err != nil로 확인한다. Rob Pike의 Errors are values는 에러가 예외가 아니라 일반 값이므로, 반복되는 검사를 타입이나 메서드로 묶어 프로그래밍적으로 다룰 수 있다고 강조한다. Go 1.13부터는 fmt.Errorf의 %w로 에러를 감싸고 errors.Is, errors.As로 감싼 체인 안의 에러를 비교하거나 꺼낼 수 있다. 에러를 ==로 비교할 때는 같은 값(sentinel error)이어야 같다고 판정되므로 비교 방식에 주의해야 한다.
- https://go.dev/blog/errors-are-values
- https://go.dev/doc/tutorial/handle-errors
- https://earthly.dev/blog/golang-errors/
- https://medium.com/@leodahal4/handle-errors-in-go-like-a-pro-5f2ab97c660b
- https://www.hacklewayne.com/golang-error-handling-yes-to-values-no-to-exceptions-a-win-for-the-future
- https://medium.com/go-puzzlers/go-puzzlers-comparing-error-values-ccf26de4b594
- https://sher-chowdhury.medium.com/go-error-handling-part-1-errors-as-values-e6409644a3df
댓글
아직 댓글이 없습니다