본문으로 건너뛰기

첫 프로그램과 go 명령어

이 챕터에서 다루는 것

실행 가능한 Go 프로그램의 최소 구성을 만들고, go run·go build·go install이 각각 무엇을 남기는지 확인한다. 그리고 이 강의 내내 가장 자주 쓰게 될 도구인 go doc으로 표준 라이브러리 문서를 읽는 법을 익힌다.

실행 가능한 프로그램의 최소 조건

Go 프로그램이 실행 파일이 되려면 딱 두 가지가 필요하다.

  1. 패키지 이름이 **main**일 것
  2. 인자도 반환값도 없는 **func main()**이 있을 것
package main

func main() {
}

이게 컴파일되는 가장 짧은 Go 프로그램이다. 아무것도 하지 않지만 빌드되고 실행된다.

package main이 아닌 패키지는 라이브러리다. 컴파일은 되지만 실행 파일이 되지 않는다. Java의 public static void main이나 Python의 if __name__ == "__main__"에 대응하는 자리인데, Go에서는 이게 패키지 이름 자체로 표현된다는 점이 다르다.

모듈 만들기

go run main.go처럼 파일 하나를 직접 실행하는 건 모듈 없이도 되지만, go buildgo install은 모듈 안에서 동작한다. 그러니 처음부터 모듈로 시작하자.

mkdir hello && cd hello
go mod init example.com/hello
go: creating new go.mod: module example.com/hello
go: to add module requirements and sums:
go mod tidy

example.com/hello모듈 경로다. 남에게 배포할 라이브러리라면 실제 저장소 주소 (github.com/사용자/저장소)를 써야 하지만, 혼자 연습할 때는 아무 이름이나 상관없다. 모듈 경로를 정하는 기준은 6-2에서 다룬다.

첫 프로그램

examples/01-getting-started/04-first-program/main.go
// Command hello는 인자로 받은 이름에게 인사한다.
package main

import (
"fmt"
"os"
"strings"
)

// greet는 name에게 보낼 인사 문자열을 만든다. name이 비어 있으면 "Go"를 쓴다.
func greet(name string) string {
name = strings.TrimSpace(name)
if name == "" {
name = "Go"
}
return fmt.Sprintf("Hello, %s!", name)
}

func main() {
name := ""
if len(os.Args) > 1 {
name = os.Args[1]
}
fmt.Println(greet(name))
}
go run ./04-first-program
go run ./04-first-program 세연
Hello, Go!
Hello, 세연!

짚어 둘 것 몇 가지.

  • import는 패키지 경로다. "fmt", "os", "strings"는 표준 라이브러리라 $GOROOT/src 아래에 있다.
  • os.Args[0]은 프로그램 이름이다. 실제 인자는 인덱스 1부터다. C의 argv와 같다.
  • 주석 규칙. func greet 바로 위 주석처럼 선언 바로 위에 붙은 주석이 그 선언의 문서가 된다. 별도의 문서 문법(JavaDoc, docstring)이 없다. 그냥 주석이다. 관례상 선언 이름으로 문장을 시작한다 — greet는 ....
  • 파일 맨 위 // Command hello는 ...패키지 문서다. package 선언 바로 위에 있으면 그렇게 취급된다.

go run / go build / go install

세 명령의 차이는 결과물을 어디에 남기느냐뿐이다.

go run — 실행하고 버린다

go run ./04-first-program

컴파일해서 임시 디렉터리에 실행 파일을 만들고, 실행한 뒤 지운다. 디렉터리에 아무것도 남지 않는다.

ls 04-first-program
main.go

개발 중 빠르게 돌려 볼 때 쓴다. 컴파일 결과 자체는 빌드 캐시에 남으므로 두 번째 실행부터는 빠르다.

go build — 현재 디렉터리에 실행 파일을 만든다

패키지 디렉터리 안에서 그냥 치면 디렉터리 이름으로 실행 파일이 생긴다.

cd 04-first-program
go build
ls -l
total 4912
-rwxr-xr-x@ 1 sgn04088 staff 2509666 Aug 8 13:08 04-first-program
-rw-r--r--@ 1 sgn04088 staff 458 Aug 8 13:05 main.go

2.5MB. "Hello"만 찍는 프로그램치고 크다고 느껴지겠지만, 여기에는 Go 런타임·GC·스케줄러와 fmt가 끌고 오는 리플렉션이 전부 들어 있다. 대신 이 파일 하나만 있으면 어디서든 돈다.

이름을 지정하려면 -o를 쓴다.

go build -o /tmp/hello ./04-first-program
ls -lh /tmp/hello
-rwxr-xr-x@ 1 sgn04088 staff 2.4M Aug 8 13:08 /tmp/hello

go build로 만든 실행 파일은 저장소에 커밋하지 않는다. .gitignore에 넣거나, 아예 -obin/ 같은 별도 디렉터리에 내보내자.

go install$GOBIN에 설치한다

go install ./04-first-program
ls ~/go/bin
04-first-program
dlv
gofumpt
goimports
golangci-lint
gopls
staticcheck

GOBIN이 설정돼 있지 않으면 $GOPATH/bin으로 간다(1-3 참고). PATH에 들어 있다면 이제 어디서든 이름으로 실행된다.

04-first-program 세계
Hello, 세계!

go install남의 도구를 설치할 때 훨씬 많이 쓴다. 앞 챕터에서 깐 gopls, staticcheck가 전부 이 방식이다.

go install golang.org/x/tools/gopls@latest

@latest, @v0.23.0처럼 버전을 붙이면 현재 프로젝트의 의존성과 무관하게 그 도구만 독립적으로 빌드해서 설치한다.

정리

명령결과물쓸 때
go run없음 (임시 후 삭제)개발 중 빠른 확인
go build현재 디렉터리 (또는 -o 경로)배포용 바이너리 만들기
go install$GOBIN 또는 $GOPATH/bin개발 도구 설치

컴파일러가 잡아 주는 것

Go 컴파일러는 다른 언어라면 린터가 할 법한 일을 에러로 처리한다.

package main

import (
"fmt"
"os"
)

func main() {
count := 3
fmt.Println("hello")
}
go build ./...
# example.com/errdemo
./main.go:5:2: "os" imported and not used
./main.go:9:2: declared and not used: count

미사용 import와 미사용 지역 변수는 경고가 아니라 컴파일 에러다. 처음엔 짜증나지만 이유가 있다.

  • 죽은 import가 쌓이면 빌드가 느려지고 의존성 그래프가 지저분해진다.
  • 미사용 변수는 대개 오타이거나 리팩터링하다 만 흔적이다.

이걸 "경고"로 두면 아무도 고치지 않는다는 게 Go 팀의 판단이었다. 정말로 잠시 남겨 두고 싶으면 _ = count처럼 블랭크 식별자에 대입하면 되지만, 커밋 전에는 지우자.

노트

전역 변수와 함수 매개변수는 미사용이어도 에러가 아니다. 인터페이스를 구현하느라 쓰지 않는 매개변수를 받는 경우가 흔하기 때문이다.

go doc — 문서는 툴체인 안에 있다

브라우저를 열지 않고 터미널에서 바로 표준 라이브러리 문서를 읽을 수 있다.

심벌 하나 보기

go doc fmt.Println
package fmt // import "fmt"

func Println(a ...any) (n int, err error)
Println formats using the default formats for its operands and writes to
standard output. Spaces are always added between operands and a newline
is appended. It returns the number of bytes written and any write error
encountered.

시그니처와 문서 주석이 그대로 나온다. 앞서 본 것처럼 문서 주석이 곧 소스의 주석이라, 따로 관리되는 문서가 낡을 일이 없다.

타입과 그 메서드 목록 보기

go doc strings.Builder
package strings // import "strings"

type Builder struct {
// Has unexported fields.
}
A Builder is used to efficiently build a string using Builder.Write methods.
It minimizes memory copying. The zero value is ready to use. Do not copy a
non-zero Builder.

func (b *Builder) Cap() int
func (b *Builder) Grow(n int)
func (b *Builder) Len() int
func (b *Builder) Reset()
func (b *Builder) String() string
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)

"이 타입으로 뭘 할 수 있지?"에 가장 빠르게 답하는 방법이다.

패키지 전체 훑기

go doc strings
package strings // import "strings"

Package strings implements simple functions to manipulate UTF-8 encoded strings.

For information about UTF-8 strings in Go, see https://blog.golang.org/strings.

func Clone(s string) string
func Compare(a, b string) int
func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsFunc(s string, f func(rune) bool) bool
func ContainsRune(s string, r rune) bool
func Count(s, substr string) int
func Cut(s, sep string) (before, after string, found bool)
func CutPrefix(s, prefix string) (after string, found bool)
func CutSuffix(s, suffix string) (before string, found bool)
func EqualFold(s, t string) bool
func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string
func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string]
...

(길어서 잘랐다.) 함수 목록만 쭉 훑는 것만으로 그 패키지가 무엇을 해 주는지 감이 온다. Go 표준 라이브러리는 이름이 정직해서 이 방식이 잘 통한다.

구현까지 보기

go doc -src strings.Contains
package strings // import "strings"

// Contains reports whether substr is within s.
func Contains(s, substr string) bool {
return Index(s, substr) >= 0
}

표준 라이브러리 소스를 읽는 건 Go를 배우는 가장 좋은 방법 중 하나다. 대부분의 함수가 이 정도로 짧다.

내가 쓴 코드도 대상이다

go doc ./04-first-program
Command hello는 인자로 받은 이름에게 인사한다.

greet는 소문자로 시작하니 비공개(unexported) 라서 기본 출력에 안 나온다. -u를 붙이면 보인다.

go doc -u ./04-first-program greet
func greet(name string) string
greet는 name에게 보낼 인사 문자열을 만든다. name이 비어 있으면 "Go"를 쓴다.

브라우저로 보기

go doc -http
doc: Documentation server listening on addr http://localhost:64796

pkg.go.dev와 같은 UI를 로컬에서 띄운다. 현재 모듈과 그 의존성까지 전부 포함되므로, 인터넷 없이도, 비공개 저장소 코드도 볼 수 있다. 포트는 매번 바뀐다. 처음 실행할 때는 문서 서버 구현체를 내려받느라 시간이 좀 걸린다.

:::warning go tool doc은 사라졌다 Go 1.26에서 cmd/docgo tool doc이 삭제됐다. 오래된 글에서 go tool doc을 보면 그냥 go doc으로 바꿔 읽으면 된다. 플래그와 인자가 동일하다. :::

go help로 나머지 찾기

go help
The commands are:

bug start a bug report
build compile packages and dependencies
clean remove object files and cached files
doc show documentation for package or symbol
env print Go environment information
fix apply fixes suggested by static checkers
fmt gofmt (reformat) package sources
generate generate Go files by processing source
get add dependencies to current module and install them
install compile and install packages and dependencies
list list packages or modules
mod module maintenance
work workspace maintenance
run compile and run Go program
telemetry manage telemetry data and settings
test test packages
tool run specified go tool
version print Go version
vet report likely mistakes in packages

Use "go help <command>" for more information about a command.

전부 20개다. 이 강의를 마칠 때쯤이면 bugtelemetry 빼고 다 쓰게 된다.

go help <명령>뿐 아니라 주제별 도움말도 있다. go help packages(패키지 지정 문법), go help environment(환경 변수 전체 목록)가 특히 유용하다.

패키지 지정 문법: ./...

명령 뒤에 붙는 인자는 패키지 패턴이다.

패턴의미
(생략)현재 디렉터리의 패키지
.현재 디렉터리의 패키지
./04-first-program그 디렉터리의 패키지
./...현재 디렉터리 아래 모든 패키지
fmt표준 라이브러리 fmt

./...는 앞으로 계속 나온다. go build ./..., go test ./..., go vet ./.... 프로젝트 전체를 대상으로 한다는 뜻으로 외워 두자.

흔히 하는 실수

  • go run main.go만 쓰다가 파일이 늘어나면 막힌다. 파일 목록을 직접 주면 그 파일들만 컴파일한다. 같은 패키지에 helper.go가 생기면 undefined: helper 에러가 난다. 처음부터 go run . 또는 go run ./경로를 쓰자.
  • go build한 바이너리를 커밋한다. 수 MB짜리 파일이 저장소에 들어간다.
  • go install한 도구가 실행되지 않는다. $GOPATH/bin이 PATH에 없는 것이다.
  • 문서를 찾으려고 매번 웹 검색을 한다. go doc이 훨씬 빠르고, 지금 쓰는 버전과 정확히 일치하는 문서를 보여 준다. 검색 결과는 5년 전 API인 경우가 흔하다.

정리

  • 실행 파일 = package main + func main().
  • 선언 바로 위 주석이 문서다. 별도 문법이 없다.
  • go run(안 남김) / go build(현재 디렉터리) / go install($GOBIN).
  • 미사용 import·변수는 컴파일 에러다. 의도된 설계다.
  • go doc <심벌>, go doc -src, go doc -http로 문서를 읽는다. go tool doc은 삭제됐다.
  • ./...는 "이 아래 전부"다.

연습문제

  1. greet를 별도 파일 greet.go로 분리하고(같은 package main), go run main.gogo run .을 각각 실행해 보자. 어떤 차이가 나는가?

  2. go doc os.ReadFilego doc -src os.ReadFile을 실행해 보자. 구현이 몇 줄인가? 그 함수가 내부에서 호출하는 다른 함수도 go doc -src로 따라가 보자.

  3. 이 프로그램이 인자를 두 개 이상 받으면 모두에게 인사하도록 고쳐 보자. 힌트: os.Args[1:]를 순회하고, strings.Join으로 합쳐도 좋다. 인자가 없을 때의 동작은 유지하자.