Go 표준 및 스타일 가이드라인
GitLab v19.1이 문서는 Go 언어를 사용하는 GitLab 프로젝트의 다양한 가이드라인과 모범 사례를 설명합니다. GitLab은 Ruby on Rails 위에 구축되어 있지만, 적합한 프로젝트에서는 Go도 사용합니다. 이 페이지는 다양한 경험을 바탕으로 Go 가이드라인을 정의하고 정리하는 것을 목표로 합니다.
이 문서는 Go 언어를 사용하는 GitLab 프로젝트의 다양한 가이드라인과 모범 사례를 설명합니다.
GitLab은 Ruby on Rails 위에 구축되어 있지만, 적합한 프로젝트에서는 Go도 사용합니다. Go는 매우 강력한 언어로 많은 장점이 있으며, 디스크/네트워크 접근과 같은 IO가 많은 프로젝트, HTTP 요청, 병렬 처리 등에 가장 적합합니다. GitLab에는 Ruby on Rails와 Go 두 언어가 모두 사용되므로, 어떤 언어가 해당 작업에 더 적합한지 신중하게 평가해야 합니다.
이 페이지는 다양한 경험을 바탕으로 Go 가이드라인을 정의하고 정리하는 것을 목표로 합니다. 여러 프로젝트가 서로 다른 기준으로 시작되었으며 각각 고유한 특성이 있을 수 있습니다. 이러한 내용은 각 프로젝트의 README.md 또는 PROCESS.md 파일에 설명되어 있습니다.
프로젝트 구조#
Go 애플리케이션 프로젝트의 기본 레이아웃에 따르면, 공식적인 Go 프로젝트 레이아웃은 없습니다. 하지만 Ben Johnson의 Standard Package Layout에 좋은 제안들이 있습니다.
다음은 참고할 수 있는 GitLab Go 기반 프로젝트 목록입니다:
Go 언어 버전#
Go 업그레이드 문서는 GitLab이 Go 바이너리 지원을 관리하고 제공하는 방법에 대한 개요를 제공합니다.
GitLab 컴포넌트에 새로운 버전의 Go가 필요한 경우, 고객, 팀 또는 컴포넌트에 부정적인 영향을 주지 않도록 업그레이드 프로세스를 따르세요.
경우에 따라 개별 프로젝트에서도 여러 버전의 Go로 빌드를 관리해야 할 수 있습니다.
의존성 관리#
Go는 의존성 관리를 위해 소스 기반 전략을 사용합니다. 의존성은 소스 리포지터리에서 소스로 직접 다운로드됩니다. 이는 의존성의 소스 리포지터리와 별개인 패키지 리포지터리에서 아티팩트로 의존성을 다운로드하는 일반적인 아티팩트 기반 전략과 다릅니다.
Go는 1.11 이전에는 버전 관리에 대한 공식 지원이 없었습니다. 해당 버전에서 Go 모듈과 시맨틱 버전 관리 사용이 도입되었습니다. Go 1.12에서는 클라이언트와 소스 버전 관리 시스템 사이의 중간 역할을 할 수 있는 모듈 프록시와, 의존성 다운로드의 무결성을 검증하는 데 사용할 수 있는 체크섬 데이터베이스가 도입되었습니다.
자세한 내용은 Go의 의존성 관리를 참조하세요.
코드 리뷰#
우리는 Go Code Review Comments의 공통 원칙을 따릅니다.
리뷰어와 메인테이너는 다음 사항에 주의해야 합니다:
-
defer함수: 필요한 경우 존재 여부 확인,err검사 이후에 사용. -
의존성을 매개변수로 주입.
-
JSON으로 마샬링할 때 빈 구조체 ([]
대신null`이 생성됨).
보안#
GitLab에서 보안은 최우선 사항입니다. 코드 리뷰 중에는 코드에서 발생할 수 있는 보안 취약점에 주의해야 합니다:
-
text/template 사용 시 XSS
-
Gorilla를 사용한 CSRF 보호
-
알려진 취약점이 없는 Go 버전 사용
-
시크릿 토큰 누출 금지
-
SQL 인젝션
프로젝트에서 SAST와 종속성 스캐닝을 실행하고 (또는 최소한 gosec 분석기), 보안 요구 사항을 따르세요.
웹 서버는 Secure와 같은 미들웨어의 장점을 활용할 수 있습니다.
리뷰어 찾기#
많은 프로젝트가 전담 메인테이너를 두기에는 너무 작습니다. 그래서 GitLab에는 Go 리뷰어 공유 풀이 있습니다. 리뷰어를 찾으려면 핸드북의 Engineering Projects 페이지에 있는 "GitLab" 프로젝트의 "Go" 섹션을 사용하세요.
이 목록에 자신을 추가하려면 team.yml 파일의 프로필에 다음을 추가하고 매니저에게 리뷰 및 머지를 요청하세요.
projects:
gitlab: reviewer go
코드 스타일 및 포맷#
패키지에서도 전역 변수 사용을 피하세요. 패키지가 여러 번 포함될 경우 사이드 이펙트가 발생할 수 있습니다.
커밋하기 전에 goimports를 사용하세요.
goimports는 Gofmt를 사용하여 Go 소스 코드를 자동으로 포맷하고, 임포트 라인을 포맷하며 누락된 것을 추가하고 참조되지 않는 것을 제거하는 도구입니다.
대부분의 에디터/IDE는 파일 저장 전후에 명령어를 실행할 수 있으며, 파일을 저장할 때마다 goimports가 적용되도록 설정할 수 있습니다.
소스 파일에서 private 메서드는 첫 번째 호출 메서드 아래에 배치하세요.
자동 린팅#
registry.gitlab.com/gitlab-org/gitlab-build-images:golangci-lint-alpine 사용은 16.10부터 더 이상 사용되지 않습니다.
업스트림 버전의 golangci-lint를 사용하세요. 기본적으로 활성화/비활성화된 린터 목록을 참조하세요.
Go 프로젝트는 다음 GitLab CI/CD job을 포함해야 합니다:
variables:
GOLANGCI_LINT_VERSION: 'v1.56.2'
lint:
image: golangci/golangci-lint:$GOLANGCI_LINT_VERSION
stage: test
script:
# Write the code coverage report to gl-code-quality-report.json
# and print linting issues to stdout in the format: path/to/file:line description
# remove `--issues-exit-code 0` or set to non-zero to fail the job if linting issues are detected
- golangci-lint run --issues-exit-code 0 --print-issued-lines=false --out-format code-climate:gl-code-quality-report.json,line-number
artifacts:
reports:
codequality: gl-code-quality-report.json
paths:
- gl-code-quality-report.json
프로젝트의 루트 디렉터리에 .golangci.yml을 포함하면 golangci-lint를 구성할 수 있습니다. golangci-lint의 모든 옵션은 이 예시에 나열되어 있습니다.
재귀적 인클루드가 사용 가능해지면, 이 analyzer와 같은 job 템플릿을 공유할 수 있습니다.
Go GitLab 린터 플러그인은 gitlab-org/language-tools/go/linters 네임스페이스에서 유지 관리됩니다.
도움말 텍스트 스타일 가이드#
Go 프로젝트가 사용자를 위한 도움말 텍스트를 생성하는 경우, gitaly 프로젝트의 도움말 텍스트 스타일 가이드에 있는 조언을 따르는 것을 고려해 보세요.
의존성#
의존성은 최소한으로 유지해야 합니다. 새로운 의존성의 도입은 승인 가이드라인에 따라 머지 리퀘스트에서 논의되어야 합니다. 새로운 의존성의 보안 상태와 라이선스 호환성을 확인하기 위해 모든 프로젝트에서 종속성 스캐닝을 활성화해야 합니다.
모듈#
Go 1.11 이상에서는 Go Modules라는 이름으로 표준 의존성 시스템이 제공됩니다. 이는 재현 가능한 빌드를 위해 의존성을 정의하고 잠금하는 방법을 제공합니다. 가능한 한 사용해야 합니다.
Go Modules를 사용할 때는 vendor/ 디렉터리가 없어야 합니다. 대신, Go는 프로젝트를 빌드하는 데 필요할 때 자동으로 의존성을 다운로드합니다. 이는 Ruby 프로젝트에서 Bundler로 의존성을 처리하는 방식과 일치하며, 머지 리퀘스트 리뷰를 더 쉽게 만듭니다.
다른 프로젝트의 CI 실행에 대한 의존성으로 작동하도록 Go 프로젝트를 빌드하는 경우와 같이, vendor/ 디렉터리를 제거하면 코드를 반복적으로 다운로드해야 하는 경우가 있습니다. 이는 속도 제한이나 네트워크 장애로 인한 간헐적 문제를 초래할 수 있습니다. 이러한 상황에서는 다운로드된 코드를 캐시해야 합니다.
v1.11.4 이전 Go 버전에는 모듈 체크섬 버그가 있으므로, checksum mismatch 오류를 방지하려면 최소한 이 버전을 사용하세요.
ORM#
GitLab에서는 (Ruby on Rails의 ActiveRecord 제외) 객체-관계형 매핑 라이브러리(ORM)를 사용하지 않습니다. 프로젝트는 이를 피하기 위해 서비스로 구성할 수 있습니다.
pgx는 PostgreSQL 데이터베이스와 상호 작용하기에 충분합니다.
마이그레이션#
호스팅된 데이터베이스를 관리하는 드문 경우에는, ActiveRecord가 제공하는 것과 같은 마이그레이션 시스템을 사용해야 합니다. postgres 컨테이너에서 사용하도록 설계된 Journey와 같은 간단한 라이브러리를 장기 실행 Pod로 배포할 수 있습니다. 새 버전은 새 Pod를 배포하여 데이터를 자동으로 마이그레이션합니다.
테스팅#
테스팅 프레임워크#
표준 라이브러리가 이미 시작하기 위한 모든 것을 제공하므로, 테스팅을 위해 특정 라이브러리나 프레임워크를 사용하지 않아야 합니다. 더 정교한 테스팅 도구가 필요한 경우, 특정 라이브러리나 프레임워크 사용을 결정할 때 다음 외부 의존성을 고려해 볼 수 있습니다:
서브테스트#
코드 가독성과 테스트 출력을 향상시키기 위해 가능하면 서브테스트를 사용하세요.
테스트에서 더 나은 출력#
테스트에서 예상 값과 실제 값을 비교할 때, 구조체, 오류, 대량의 텍스트 또는 JSON 문서를 비교할 때 가독성을 향상시키기 위해
testify/require.Equal,
testify/require.EqualError,
testify/require.EqualValues
등을 사용하세요:
type TestData struct {
// ...
}
func FuncUnderTest() TestData {
// ...
}
func Test(t *testing.T) {
t.Run("FuncUnderTest", func(t *testing.T) {
want := TestData{}
got := FuncUnderTest()
require.Equal(t, want, got) // expected value comes first, then comes the actual one ("diff" semantics)
})
}
테이블 기반 테스트#
동일한 함수에 대한 여러 입력/출력 항목이 있는 경우 테이블 기반 테스트를 사용하는 것이 일반적으로 좋은 사례입니다. 다음은 테이블 기반 테스트 작성 시 따를 수 있는 몇 가지 가이드라인입니다. 이 가이드라인은 대부분 Go 표준 라이브러리 소스 코드에서 추출되었습니다. 적합하지 않은 경우 이 가이드라인을 따르지 않아도 괜찮다는 점을 유의하세요.
테스트 케이스 정의#
각 테이블 항목은 입력과 예상 결과가 있는 완전한 테스트 케이스이며, 테스트 출력을 읽기 쉽게 만드는 테스트 이름과 같은 추가 정보가 있을 수 있습니다.
-
테스트 내부에 익명 구조체의 슬라이스 정의.
-
테스트 외부에 익명 구조체의 슬라이스 정의.
-
코드 재사용을 위한 이름 있는 구조체.
테스트 케이스의 내용#
-
이상적으로, 각 테스트 케이스는 서브테스트 이름 지정에 사용할 고유 식별자 필드를 가져야 합니다. Go 표준 라이브러리에서는 일반적으로
name string필드입니다. -
테스트 케이스에서 assertion에 사용되는 항목을 지정할 때
want/expect/actual을 사용하세요.
변수 이름#
-
각 테이블 기반 테스트 map/구조체 슬라이스는
tests로 이름 지을 수 있습니다. -
tests를 반복할 때 익명 구조체는tt또는tc로 참조할 수 있습니다. -
테스트 설명은
name/testName/tn으로 참조할 수 있습니다.
벤치마크#
많은 IO를 처리하거나 복잡한 작업을 수행하는 프로그램은 시간에 따른 성능 일관성을 보장하기 위해 항상 벤치마크를 포함해야 합니다.
오류 처리#
컨텍스트 추가#
오류를 반환하기 전에 컨텍스트를 추가하는 것이 도움이 될 수 있습니다. 단순히 오류를 반환하는 것보다 개발자가 오류 상태에 들어갔을 때 프로그램이 무엇을 하려고 했는지 이해할 수 있어 디버깅이 훨씬 쉬워집니다.
예를 들어:
// Wrap the error
return nil, fmt.Errorf("get cache %s: %w", f.Name, err)
// Just add context
return nil, fmt.Errorf("saving cache %s: %v", f.Name, err)
컨텍스트를 추가할 때 유의할 사항:
-
호출자에게 근본 오류를 노출할지 결정하세요. 그렇다면
%w를 사용하고, 그렇지 않다면%v를 사용할 수 있습니다. -
failed,error,didn't와 같은 단어를 사용하지 마세요. 오류이기 때문에 사용자는 이미 무언가 실패했다는 것을 알고 있으며, 이는failed xx failed xx failed xx와 같은 문자열로 이어질 수 있습니다. 대신 무엇이 실패했는지 설명하세요. -
오류 문자열은 대문자로 시작하거나 구두점이나 줄 바꿈으로 끝나서는 안 됩니다. 이를 확인하기 위해
golint를 사용할 수 있습니다.
명명#
-
센티널 오류를 사용할 때는 항상
ErrXxx와 같이 이름 지어야 합니다. -
새 오류 타입을 생성할 때는 항상
XxxError와 같이 이름 지어야 합니다.
오류 타입 확인#
-
오류 동등성을 확인할 때
==를 사용하지 마세요. 대신 (Go 버전 >= 1.13의 경우)errors.Is를 사용하세요. -
오류가 특정 타입인지 확인할 때 타입 어서션을 사용하지 마세요. 대신 (Go 버전 >= 1.13의 경우)
errors.As를 사용하세요.
오류 작업을 위한 참고 자료#
CLI#
모든 Go 프로그램은 커맨드 라인에서 실행됩니다.
cli는 커맨드 라인 앱을 만드는 편리한 패키지입니다. 프로젝트가 데몬이든 단순한 CLI 도구든 사용해야 합니다. 플래그는 환경 변수에 직접 매핑할 수 있어, 프로그램과의 모든 가능한 커맨드 라인 상호 작용을 문서화하고 중앙화합니다. 코드 깊숙이 변수를 숨기는 os.GetEnv는 사용하지 마세요.
라이브러리#
LabKit#
LabKit은 Go 서비스를 위한 공통 라이브러리를 보관하는 곳입니다. LabKit 사용 예시는 workhorse와 gitaly를 참조하세요. LabKit은 세 가지 관련 기능을 내보냅니다:
-
gitlab.com/gitlab-org/labkit/correlation: 서비스 간 correlation ID를 전파하고 추출하는 기능. -
gitlab.com/gitlab-org/labkit/tracing: 분산 추적을 위한 Go 라이브러리 계측 기능. -
gitlab.com/gitlab-org/labkit/log: Logrus를 사용한 구조화된 로깅 기능.
이는 Workhorse, Gitaly 및 기타 Go 서버에서 일관된 기본 구현에 대한 얇은 추상화를 제공합니다. 예를 들어, gitlab.com/gitlab-org/labkit/tracing의 경우 애플리케이션 코드를 변경하지 않고도 Opentracing을 직접 사용하는 것에서 Zipkin 또는 Go kit의 자체 추적 래퍼로 전환할 수 있으며, 동시에 동일한 일관된 구성 메커니즘(즉, GITLAB_TRACING 환경 변수)을 유지할 수 있습니다.
구조화된 (JSON) 로깅#
모든 바이너리는 이상적으로 로그 검색 및 필터링에 도움이 되는 구조화된 (JSON) 로깅을 갖추어야 합니다. LabKit은 Logrus에 대한 추상화를 제공합니다. 모든 인프라가 JSON 형식을 가정하므로 JSON 형식의 구조화된 로깅을 사용합니다. Logrus를 사용할 때 내장된 JSON 포맷터를 사용하여 구조화된 로깅을 켤 수 있습니다. 이는 Ruby 애플리케이션에서 사용하는 것과 동일한 로깅 유형을 따릅니다.
Logrus 사용 방법#
Logrus 패키지를 사용할 때 따라야 할 몇 가지 가이드라인이 있습니다:
-
오류를 출력할 때 WithError를 사용하세요. 예를 들어,
logrus.WithError(err).Error("Failed to do something"). -
구조화된 로깅을 사용하므로
WithField또는WithFields를 사용하여 요청 URI와 같은 해당 코드 경로의 컨텍스트에서 필드를 로그로 남길 수 있습니다. 예를 들어,logrus.WithField("file", "/app/go").Info("Opening dir"). 여러 키를 로그로 남겨야 하는 경우WithField를 여러 번 호출하는 대신 항상WithFields를 사용하세요.
컨텍스트#
데몬은 장기 실행 애플리케이션이므로, 취소를 관리하고 불필요한 리소스 소비(DDoS 취약점으로 이어질 수 있음)를 방지하는 메커니즘이 있어야 합니다. Go Context는 블록될 수 있는 함수에서 사용해야 하며 첫 번째 매개변수로 전달해야 합니다.
Dockerfile#
모든 프로젝트는 프로젝트를 빌드하고 실행하기 위해 리포지터리의 루트에 Dockerfile을 가져야 합니다. Go 프로그램은 정적 바이너리이므로 외부 의존성이 필요하지 않으며, 최종 이미지의 셸은 불필요합니다.
멀티스테이지 빌드를 권장합니다:
-
올바른 Go 버전과 의존성으로 프로젝트를 빌드할 수 있습니다.
-
Scratch에서 파생된 작고 자립적인 이미지를 생성합니다.
생성된 Docker 이미지는 이식 가능한 명령어를 만들기 위해 Entrypoint에 프로그램이 있어야 합니다. 그렇게 하면 누구나 이미지를 실행할 수 있으며, 매개변수 없이 실행하면 도움말 메시지가 표시됩니다(cli가 사용된 경우).
Secure 팀 표준 및 스타일 가이드라인#
다음은 Secure 팀에 특화된 스타일 가이드라인입니다.
코드 스타일 및 포맷#
커밋하기 전에 goimports -local gitlab.com/gitlab-org를 사용하세요.
goimports는 Gofmt를 사용하여 Go 소스 코드를 자동으로 포맷하고, 임포트 라인을 포맷하며 누락된 것을 추가하고 참조되지 않는 것을 제거하는 도구입니다.
-local gitlab.com/gitlab-org 옵션을 사용하면 goimports가 로컬 참조 패키지를 외부 패키지와 별도로 그룹화합니다. 자세한 내용은 Go wiki의 Code Review Comments 페이지의 임포트 섹션을 참조하세요.
대부분의 에디터/IDE는 파일 저장 전후에 명령어를 실행할 수 있으며, 파일을 저장할 때마다 goimports -local gitlab.com/gitlab-org가 적용되도록 설정할 수 있습니다.
브랜치 이름 지정#
GitLab 브랜치 이름 규칙에 추가하여, 브랜치 이름에는 a-z, 0-9 또는 - 문자만 사용하세요. 이 제한은 브랜치 이름에 슬래시 /와 같은 특정 문자가 포함되어 있으면 go get이 예상대로 작동하지 않기 때문입니다:
$ go get -u gitlab.com/gitlab-org/security-products/analyzers/report/v3@some-user/some-feature
go get: gitlab.com/gitlab-org/security-products/analyzers/report/v3@some-user/some-feature: invalid version: version "some-user/some-feature" invalid: disallowed version string
브랜치 이름에 슬래시가 포함되어 있으면, 유연성이 떨어지는 커밋 SHA를 대신 참조해야 합니다. 예를 들어:
$ go get -u gitlab.com/gitlab-org/security-products/analyzers/report/v3@5c9a4279fa1263755718cf069d54ba8051287954
go: downloading gitlab.com/gitlab-org/security-products/analyzers/report/v3 v3.15.3-0.20221012172609-5c9a4279fa12
...
슬라이스 초기화#
슬라이스를 초기화할 때, 추가 할당을 방지하기 위해 가능하면 용량을 제공하세요.
하지 마세요:
var s2 []string
for _, val := range s1 {
s2 = append(s2, val)
}
하세요:
s2 := make([]string, 0, len(s1))
for _, val := range s1 {
s2 = append(s2, val)
}
새 슬라이스를 생성할 때 make에 용량이 전달되지 않으면, append는 값을 담을 수 없을 때 슬라이스의 백킹 배열을 계속 크기 조정합니다. 용량을 제공하면 할당을 최소화할 수 있습니다. prealloc golangci-lint 규칙이 이를 자동으로 확인하도록 권장합니다.
분석기 테스트#
일반적인 Secure 분석기는 SAST/DAST 스캐너 보고서를 GitLab Security Reports로 변환하는 convert 함수를 가지고 있습니다.
convert 함수에 대한 테스트를 작성할 때, 분석기 리포지터리의 루트에 있는 testdata 디렉터리를 사용하는 테스트 픽스처를 활용해야 합니다. testdata 디렉터리에는 두 개의 하위 디렉터리가 있어야 합니다: expect와 reports. reports 디렉터리에는 테스트 설정 중에 convert 함수로 전달되는 샘플 SAST/DAST 스캐너 보고서가 포함되어야 합니다. expect 디렉터리에는 convert가 반환하는 예상 GitLab Security Report가 포함되어야 합니다. 시크릿 탐지에 대한 예시를 참조하세요.
스캐너 보고서가 35줄 미만으로 작은 경우, testdata 디렉터리를 사용하는 대신 보고서를 인라인으로 작성해도 됩니다.
테스트 Diff#
테스트에서 큰 구조체를 비교할 때 go-cmp 패키지를 사용해야 합니다. 테스트 로그에 두 구조체 전체를 출력하는 대신, 두 구조체가 다른 특정 diff를 출력하는 것이 가능합니다. 다음은 작은 예시입니다:
package main
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
type Foo struct {
Desc Bar
Point Baz
}
type Bar struct {
A string
B string
}
type Baz struct {
X int
Y int
}
func TestHelloWorld(t *testing.T) {
want := Foo{
Desc: Bar{A: "a", B: "b"},
Point: Baz{X: 1, Y: 2},
}
got := Foo{
Desc: Bar{A: "a", B: "b"},
Point: Baz{X: 2, Y: 2},
}
t.Log("reflect comparison:")
if !reflect.DeepEqual(got, want) {
t.Errorf("Wrong result. want:\n%v\nGot:\n%v", want, got)
}
t.Log("cmp comparison:")
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Wrong result. (-want +got):\n%s", diff)
}
}
출력을 통해 큰 구조체를 비교할 때 go-cmp가 왜 훨씬 우수한지 알 수 있습니다. 이 작은 차이로는 차이를 발견할 수 있지만, 데이터가 커질수록 빠르게 다루기 어려워집니다.
main_test.go:36: reflect comparison:
main_test.go:38: Wrong result. want:
{{a b} {1 2}}
Got:
{{a b} {2 2}}
main_test.go:41: cmp comparison:
main_test.go:43: Wrong result. (-want +got):
main.Foo{
Desc: {A: "a", B: "b"},
Point: main.Baz{
- X: 1,
+ X: 2,
Y: 2,
},
}