Go 패키지와 모듈로 코드 조직화하기

Go 패키지와 모듈로 코드 조직화하기

Go에서 코드는 패키지(package) 단위로 묶여요. 같은 디렉터리에 있는 소스 파일들이 모여 하나의 패키지를 이루고, 그 안에서 정의한 함수·타입·변수·상수는 패키지 내부의 다른 파일들끼리 서로 공유할 수 있어요. 그리고 그보다 더 큰 묶음인 **모듈(module)**은 관련된 여러 패키지를 함께 배포하는 단위예요. 코드를 만들기 시작할 때 모듈 경로를 정하고 패키지를 그 아래에 배치하는 방식부터 이해하면, 파일이 늘어나도 구조가 흐트러지지 않아요.

Go 공식 문서는 모듈 루트에 go.mod 파일을 두고 그 경로를 앞부분에 붙여 import 경로를 만든다고 안내해요. 이 페이지는 공식 "How to Write Go Code" 문서의 핵심을 따라가며, 패키지와 모듈을 어떻게 잡아야 하는지 차근차근 설명할게요.

출처: How to Write Go Code (공식 문서)

코드를 담는 단위: 패키지와 모듈

Go 프로그램은 패키지로 조직됩니다. 패키지는 같은 디렉터리에 있는 소스 파일의 모임이고, 그 파일들이 함께 컴파일돼요. 한 파일에서 정의한 함수·타입·변수·상수는 같은 패키지 안의 다른 파일에서도 그대로 보여요.

그 위에 모듈이 있어요. 모듈은 함께 배포되는 관련 패키지들의 묶음이고, 저장소(repository)는 보통 하나의 모듈을 담아요. 저장소 루트에 있는 go.mod 파일이 모듈 경로를 선언하고, 이 경로가 그 모듈 안 모든 패키지의 import 경로 접두사가 돼요.

$ go mod init example/user/hello
go: creating new go.mod: module example/user/hello
module example/user/hello

go 1.16

모든 소스 파일의 첫 줄은 패키지 선언이어야 해요. 그리고 실행 프로그램(executable)은 반드시 package main으로 시작해야 하죠.

import 경로가 어디를 가리키나

모듈 경로는 import 경로의 접두사일 뿐 아니라, go 명령이 그 모듈을 어디서 내려받을지를 알려주는 단서가 돼요. 가령 github.com/google/go-cmp 모듈을 가져온다면 go 명령은 그 모듈이 위치한 저장소를 참고해 소스를 가져와요.

패키지의 import 경로는 모듈 경로 + 그 모듈 안에서의 하위 디렉터리로 정해져요. 예를 들어 모듈 github.com/google/go-cmp 안에 cmp/ 디렉터리의 패키지가 있다면, 그 패키지의 import 경로는 github.com/google/go-cmp/cmp가 돼요. 표준 라이브러리의 패키지에는 모듈 경로 접두사가 붙지 않아요.

첫 프로그램과 패키지 만들기

hello 프로그램 안에서 morestrings라는 작은 패키지를 직접 만들어 import해 볼게요. 먼저 morestrings 디렉터리에 reverse.go 파일을 만들고 함수를 정의해요.

// Package morestrings implements additional functions to manipulate UTF-8
// encoded strings, beyond what is provided in the standard "strings" package.
package morestrings

// ReverseRunes returns its argument string reversed rune-wise left to right.
func ReverseRunes(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

ReverseRunes는 대문자로 시작하니까 외부로 내보내진(exported) 함수예요. 그래서 이 패키지를 import한 다른 패키지에서 호출할 수 있어요. go build로 컴파일이 되는지 확인하면 별도 출력 파일 없이 컴파일 결과만 로컬 빌드 캐시에 저장돼요.

이제 hello.go에서 그 패키지를 가져와 사용해볼게요.

package main

import (
	"fmt"

	"example/user/hello/morestrings"
)

func main() {
	fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
}

원격 모듈의 패키지 가져오기

import 경로는 Git 같은 버전 관리 시스템으로 소스를 얻는 방법까지 설명할 수 있어요. go 도구는 이 성질을 이용해 원격 저장소에서 패키지를 자동으로 내려받죠. 외부 의존성이 생기면 go.mod에 버전을 기록해야 해요. go mod tidy는 import한 패키지에 필요한 모듈 요구사항을 추가하고, 더 이상 쓰지 않는 모듈의 요구사항은 제거해요.

$ go mod tidy
go: finding module for package github.com/google/go-cmp/cmp
go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.5.4
module example/user/hello

go 1.16

require github.com/google/go-cmp v0.5.4

다운로드한 모듈은 GOPATH가 가리키는 디렉터리의 pkg/mod 하위에 저장되고, 같은 버전을 요구하는 모듈끼리 공유할 수 있어서 읽기 전용으로 표시돼요. go clean -modcache로 내려받은 모듈 전부를 지울 수도 있어요.

테스트 추가하기

Go는 go test 명령과 testing 패키지로 이뤄진 가벼운 테스트 프레임워크를 갖고 있어요. 파일 이름이 _test.go로 끝나는 파일에 func (t *testing.T) 시그니처의 TestXXX 함수를 만들면 그게 테스트가 돼요. 그 안에서 t.Error 같은 실패 함수를 부르면 테스트가 실패한 것으로 간주돼요.

package morestrings

import "testing"

func TestReverseRunes(t *testing.T) {
	cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
	}
	for _, c := range cases {
		got := ReverseRunes(c.in)
		if got != c.want {
			t.Errorf("ReverseRunes(%q) == %q, want %q", c.in, got, c.want)
		}
	}
}
$ go test
PASS
ok  	example/user/hello/morestrings 0.165s

더 알아보기 (Learn more)