모든 Go 파일은 첫 줄에 선언한 패키지에 속합니다. 한 패키지의 코드는 다른 패키지를 import하고 이름 앞에 패키지 이름을 붙여서 사용합니다. 패키지 밖에서 쓸 수 있는 것은 대문자로 시작하는 이름뿐입니다.
fmt, math, strings는 표준 라이브러리 패키지입니다. Go와 함께 제공되므로 "추가"한다는 것은 import 블록에 이름을 쓰는 것을 뜻합니다. 아무것도 내려받지 않습니다. fmt.Println, math.Sqrt, strings.Repeat는 공개되어 있으므로 대문자로 시작합니다.
package 절
모든 .go 파일의 주석이 아닌 첫 줄은 package name입니다. 한 디렉터리의 모든 파일은 같은 이름을 써야 하며, 함께 하나의 패키지를 이룹니다. 패키지 이름이 두 개인 디렉터리는 컴파일되지 않습니다:
found packages greeting (b.go) and greet (greet.go) in /home/ana/shop/greet
(유일한 예외는 package name_test를 쓸 수 있는 테스트 파일입니다.)
package main은 특별합니다. 실행 파일을 만들고, 프로그램은 그 func main()에서 시작합니다. 다른 모든 패키지는 import되기 위한 라이브러리입니다.
관례상 패키지 이름은 디렉터리 경로의 마지막 요소와 같습니다. shop/greet의 코드는 package greet입니다. 패키지 이름은 짧고, 소문자이고, 한 단어이며, 밑줄이나 대소문자 혼용이 없습니다. string_utils나 greetHelpers가 아니라 strconv, httptest, greet입니다.
패키지 import하기
import 경로는 문자열입니다. 표준 라이브러리라면 Go 소스 트리 아래의 패키지 경로이고, 그 밖의 것은 모듈 경로로 시작합니다:
import (
"fmt" // standard library
"net/http" // standard library, nested path
"math/rand/v2" // standard library, version 2 of math/rand
"github.com/google/uuid" // another module (needs go get)
"example.com/shop/greet" // a package in your own module
)
경로는 패키지가 있는 곳이고, 이름은 코드에 쓰는 것입니다. net/http의 이름은 http이고, math/rand/v2의 이름은 rand입니다. net/http.Get이 아니라 http.Get이라고 씁니다.
gofmt와 goimports는 블록을 정렬하며, 관례상 표준 라이브러리 import와 나머지를 빈 줄로 구분합니다.
사용하지 않는 import는 컴파일 오류입니다:
./main.go:5:2: "os" imported and not used
Go를 지원하는 에디터는 저장할 때 goimports를 실행해서 빠진 표준 import를 추가하고 쓰지 않는 것을 지워 줍니다.
공개 이름과 비공개 이름
Go에서 가시성은 규칙 하나로 정해집니다. 이름이 대문자로 시작하면 공개됩니다. 함수, 타입, 변수, 상수, 구조체 필드, 메서드 모두에 적용됩니다. public, private, protected 키워드는 없습니다.
| 이름 | 패키지 밖에서 보이는가? |
|---|---|
func Hello() | 예 |
func hello() | 아니요 |
type User struct | 예 |
User.Name 필드 | 예 |
User.email 필드 | 아니요 |
const MaxSize | 예 |
var defaultTimeout | 아니요 |
다른 패키지의 비공개 이름을 쓰려고 하면 undefined 오류가 납니다:
./main.go:11:20: undefined: greet.prefix
다른 패키지가 여러분의 타입을 살펴보기 때문에, 파일 하나 안에서도 이 규칙이 중요합니다. encoding/json은 공개 필드만 볼 수 있습니다:
token은 조용히 JSON에서 빠집니다. 필드가 비공개 데이터라면 기능이지만, 원하던 필드를 대문자로 쓰는 것을 잊었다면 놀라운 일입니다.
패키지를 여러 파일로 나누기
패키지는 원하는 만큼 많은 파일로 나눌 수 있습니다. 같은 package 줄을 가진 디렉터리의 모든 파일은 아무것도 import하지 않아도 다른 파일에 선언된 모든 이름을 공개 여부와 상관없이 봅니다:
shop/
├── go.mod (module example.com/shop)
├── main.go
└── math.go
// main.go
package main
import "fmt"
func main() {
fmt.Println(total(2, 3))
}
// math.go
package main
func total(a, b int) int { return a + b }
main.go는 total을 직접 호출합니다. #include도 없고, 같은 패키지의 파일 사이에는 import도 없습니다.
go run main.go에서 나오는 "undefined" 오류
Go에서 가장 많이 검색되는 패키지 문제이며, 원인은 하나입니다. main이 있는 파일만 실행하면 그 파일만 컴파일됩니다:
go run main.go
# command-line-arguments
./main.go:6:14: undefined: total
command-line-arguments는 파일 목록으로 빌드한 패키지에 Go가 붙이는 이름이며, math.go는 그 목록에 없습니다. 대신 패키지 전체를 실행하세요:
go run .
5
go build main.go도 마찬가지입니다. go run .과 go build(또는 go build ./cmd/app)를 쓰면 이 문제는 생기지 않습니다. go run .으로도 계속된다면 두 파일 모두 package main인지, 어느 쪽도 빌드 제약(//go:build)이나 _test.go 접미사 때문에 제외되지 않았는지 확인하세요.
직접 만든 패키지
모듈의 하위 디렉터리는 별도의 패키지입니다. go.mod의 모듈 경로에 디렉터리를 더해서 import하세요:
shop/
├── go.mod (module example.com/shop)
├── main.go
└── greet/
└── greet.go
// greet/greet.go
package greet
const prefix = "Hi, "
// Hello returns a greeting for name.
func Hello(name string) string {
return prefix + name
}
// main.go
package main
import (
"fmt"
"example.com/shop/greet"
)
func main() {
fmt.Println(greet.Hello("Ana"))
}
Hi, Ana
여기서 두 가지에 걸려 넘어지기 쉽습니다. import는 상대 경로가 아니라 모듈 경로에 디렉터리를 더한 것입니다. 모듈 모드에서 "./greet"는 동작하지 않습니다. 그리고 prefix는 소문자이므로 main에서 보이지 않습니다. 패키지 API에 속하는 것은 Hello뿐입니다.
internal 패키지
internal이라는 디렉터리는 그 안의 것을 누가 import할 수 있는지 제한합니다. shop/internal/store의 코드는 shop/을 루트로 하는 모든 패키지에서 import할 수 있고, 그 밖에서는 할 수 없습니다. 다른 모듈이 시도하면 다음 오류를 받습니다:
use of internal package example.com/shop/internal/store not allowed
외부에 약속하지 않고 자기 패키지들끼리 공유하고 싶은 코드에 internal/을 쓰세요. 바깥 코드가 의존할 수 없으므로 API를 자유롭게 바꿀 수 있습니다.
import 별칭
경로 앞에 이름을 쓰면 import에 다른 지역 이름을 줄 수 있습니다. 보통은 같은 이름의 패키지가 둘일 때입니다:
두 패키지 모두 이름이 rand이므로 적어도 하나에는 별칭이 필요합니다. 별칭은 충돌과 불분명한 이름에 쓰세요. 줄이려고 패키지 이름을 바꾸지는 마세요. 읽는 사람은 strconv는 알아도 여러분이 붙인 sc는 모릅니다.
빈 import와 점 import
빈 import(_). 부수 효과만을 위해 패키지를 import합니다. 패키지 변수와 init 함수는 실행되지만, 이름은 하나도 쓰지 않습니다. 데이터베이스 드라이버와 이미지 디코더가 이런 식으로 자신을 등록합니다:
import (
"database/sql"
_ "github.com/lib/pq" // registers the "postgres" driver with database/sql
)
_가 없으면 이 import는 사용하지 않는 import 오류가 됩니다.
점 import(.). 패키지의 공개 이름을 파일의 스코프에 직접 넣어서 fmt.Println 대신 Println이라고 쓰게 합니다. 이름의 출처를 숨기므로 몇몇 테스트 헬퍼 패턴을 빼면 권하지 않습니다.
import 순환
Go는 두 패키지가 직접 또는 연쇄를 통해 서로를 import하는 것을 허용하지 않습니다:
package example.com/shop
imports example.com/shop/greet from main.go
imports example.com/shop/other from c.go
imports example.com/shop/greet from o.go: import cycle not allowed
오류는 그 연쇄를 나열합니다. 거의 모든 경우를 해결하는 방법이 두 가지 있습니다:
- 두 패키지가 모두 필요로 하는 코드를 둘 다 import하는 더 낮은 수준의 세 번째 패키지로 옮기세요.
- 패키지 A가 B의 무언가를 호출하기만 하면 된다면, A에 작은 인터페이스를 정의하고 B의 타입이 그것을 만족하게 하세요. 그러면 A는 더 이상 B를 import하지 않습니다.
순환은 보통 실제로는 함께 있어야 할 코드 계층 사이에 패키지 경계를 그었다는 뜻이므로, 분할을 다시 생각해 보라는 신호이기도 합니다.
흔한 실수
- 파일 하나만 실행함. 여러 파일로 된 패키지에서
go run main.go를 썼다면go run .을 쓰세요. - 상대 import. 모듈 모드에서
import "./utils"는 실패합니다."example.com/yourmodule/utils"를 쓰세요. - 공개하려던 이름을 소문자로 씀. 다른 패키지에 필요한 함수, 필드, 메서드는 대문자로 시작해야 합니다.
- 디렉터리와 다른 패키지 이름. 문법상 맞지만 헷갈립니다. import 경로는
utils인데 코드는helpers.X라고 씁니다. 둘을 같게 유지하세요. - 무엇이든 담는 패키지 이름.
util,common,misc는 내용에 대해 아무것도 말해 주지 않고, import 순환을 끌어들이는 자석으로 자라기 쉽습니다. 제공하는 것에 따라 패키지 이름을 지으세요:money,auth,slug.
자주 묻는 질문
같은 패키지의 함수가 undefined라고 나오는 이유는 무엇인가요?
아마 go run main.go를 실행했을 것입니다. 파일 이름을 지정하면 그 파일만 컴파일되므로 같은 패키지의 다른 파일에 정의된 함수가 빠집니다: undefined: total. 대신 디렉터리의 모든 .go 파일을 컴파일하는 go run .을 실행하세요. 모든 파일의 package 줄이 같은지도 확인하세요.
Go에서 fmt 패키지는 어떻게 추가하나요?
fmt는 표준 라이브러리의 일부이고 Go와 함께 제공되므로 설치할 것이 없습니다. package 줄 다음에 import "fmt"를 추가하고 fmt. 접두사로 함수를 호출하세요. 예를 들어 fmt.Println("hi")입니다. strings, os, time, net/http를 비롯한 다른 모든 표준 패키지도 마찬가지입니다.
Go에서 직접 만든 패키지는 어떻게 import하나요?
모듈 경로에 디렉터리를 더한 경로로 import합니다. 모듈 이름이 example.com/shop이라면 greet 폴더의 패키지는 import "example.com/shop/greet"로 가져와 greet.Hello()로 씁니다. import하는 쪽에서는 대문자로 시작하는 이름만 보입니다.
Go에서 exported와 unexported의 차이는 무엇인가요?
대문자로 시작하는 이름(Hello, Total, User.Name)은 공개(exported)되어 다른 패키지에서 쓸 수 있습니다. 소문자 이름(hello, total, user.name)은 비공개(unexported)이며 자기 패키지 안에서만 보입니다. public이나 private 키워드는 없습니다.
Go에서 "import cycle not allowed"는 어떻게 고치나요?
두 패키지가 직접 또는 연쇄를 통해 서로를 import하고 있으며, Go는 이를 금지합니다. 공유하는 코드를 둘 다 import하는 세 번째 패키지로 옮기거나, 동작이 필요한 패키지에 작은 인터페이스를 정의해서 더 이상 다른 패키지를 import하지 않게 하세요.