Go 표준 및 스타일 가이드

이 문서는 Go 언어를 사용하는 GitLab 프로젝트에 대한 다양한 지침과 모범 사례를 설명합니다.

GitLab은 Ruby on Rails 위에 구축되었지만, 우리는 Go를 사용하여 논리적인 경우에 프로젝트에 사용합니다. Go는 매우 강력한 언어로, 여러 이점을 가지고 있으며, 많은 IO(디스크/네트워크 액세스), HTTP 요청, 병렬 처리 등이 많은 프로젝트에 가장 적합합니다. GitLab에는 Ruby on Rails와 Go가 모두 있기 때문에 두 언어 중 어느 것이 해당 작업에 가장 적합한지 신중하게 평가해야 합니다.

이 페이지는 다양한 경험을 기반으로 우리의 Go 지침을 정의하고 구성하는 데 목적을 두고 있습니다. 몇몇 프로젝트는 서로 다른 표준으로 시작되었고, 그 특이 사항은 각각의 README.md 또는 PROCESS.md 파일에 설명되어 있습니다.

Go 언어 버전

Go 업그레이드 문서는 GoLab이 Go 이진 지원을 관리하고 배포하는 방법에 대한 개요를 제공합니다.

GitLab 컴포넌트가 더 높은 버전의 Go를 필요로 하는 경우, 업그레이드 프로세스를 따라서 고객, 팀 또는 컴포넌트에 불이익이 없도록 해야 합니다.

가끔, 개별 프로젝트는 또한 다중 Go 버전을 지원하게 될 수 있습니다.

의존성 관리

Go는 의존성 관리를 위해 소스 기반 전략을 사용합니다. 의존성은 소스 리포지터리에서 소스로 다운로드됩니다. 이는 의존성이 소스 리포지터리와 별도인 패키지 리포지터리에서 아티팩트로 다운로드되는 일반적인 아티팩트 기반 전략과 다릅니다.

1.11 이전의 Go에는 버전 관리를 위한 1급 지원이 없었습니다. 이 버전에서 Go 모듈과 의미적 버전 지원이 소개되었습니다. 1.12에서는 모듈 프록시와 체크섬 데이터베이스가 도입되었는데, 이것들은 클라이언트와 소스 버전 제어 시스템 간의 중간 단계 역할을 할 수 있으며, 의존성 다운로드의 무결성을 검증하는 데 사용될 수 있습니다.

자세한 내용은 Go에서의 의존성 관리를 참조하십시오.

코드 리뷰

우리는 Go 코드 리뷰 의견의 일반 원칙을 따릅니다.

리뷰어와 유지보수자는 다음에 주의해야 합니다:

  • defer 함수: 필요할 때 존재 여부 확인, 그리고 err 확인 후에
  • 매개변수로 의존성 주입
  • JSON으로 marshaling할 때 void 구조체 (null 대신 [] 생성)

보안

보안은 GitLab에서의 최우선 순위입니다. 코드 리뷰 중에는 다음과 같은 가능한 보안 위반에 주의해야 합니다:

  • text/template 사용 시 XSS
  • Gorilla를 사용하여 CSRF 보호
  • 알려진 취약점 없는 Go 버전 사용
  • 비밀 토큰 누설 금지
  • SQL 삽입

프로젝트에서 SASTDependency Scanning을 실행하고(gosec 분석기는 적어도), 보안 요구 사항을 따르도록 합니다.

웹 서버는 Secure와 같은 미들웨어를 활용할 수 있습니다.

리뷰어 찾기

우리의 많은 프로젝트들은 전문 유지관들을 두기에는 너무 작습니다. 따라서 우리는 GitLab에서 Go 리뷰어의 공유 풀을 가지고 있습니다. 리뷰어를 찾으려면 핸드북의 “GitLab” 프로젝트의 “Go” 섹션(https://handbook.gitlab.com/handbook/engineering/projects/#gitlab_reviewers_go)을 사용하십시오.

이 디렉터리에 자신을 추가하려면 다음을 team.yml(https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/team.yml) 파일의 프로필에 추가하고 당신의 매니저에게 리뷰 및 Merge해달라고 요청하십시오.

projects:
  gitlab: reviewer go

코드 스타일 및 형식

  • 전역 변수를 피하십시오, 심지어 패키지에서도. 이렇게 하면 패키지가 여러 번 포함될 때 부작용이 발생합니다.
  • 커밋하기 전에 goimports를 사용하십시오. goimports는 누락된 import 라인을 추가하거나 참조되지 않는 라인을 제거하는 것을 포함하여 Go 소스 코드를 자동으로 포맷팅하는 도구입니다.

    대부분의 편집기/IDE는 파일을 저장하기 전/후에 명령을 실행할 수 있습니다. goimports를 실행하도록 설정하여 모든 파일에 적용할 수 있습니다.

  • 소스 파일의 첫 번째 호출자 메서드 아래에 비공개 메서드를 배치하십시오.

자동 리팅

caution
registry.gitlab.com/gitlab-org/gitlab-build-images:golangci-lint-alpine의 사용은 16.10부터 사용 중단되었습니다.

golangci-lint의 상위 버전을 사용하십시오. 기본적으로 활성화/비활성화된 린터 디렉터리을 확인하십시오.

Go 프로젝트는 다음과 같은 GitLab CI/CD 작업을 포함해야 합니다:

variables:
  GOLANGCI_LINT_VERSION: 'v1.56.2'
lint:
  image: golangci/golangci-lint:$GOLANGCI_LINT_VERSION
  stage: test
  script:
    - 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의 모든 옵션은 이 예제에 나와 있습니다.

재귀적 포함이 가능해지면 이런식의 분석기와 같은 작업 템플릿을 공유할 수 있게 됩니다.

Go GitLab 리턴 프로그램은 gitlab-org/language-tools/go/linters 네임스페이스에서 관리됩니다.

도움말 텍스트 스타일 가이드

만약 Go 프로젝트가 사용자를 위한 도움말 텍스트를 생성한다면, gitaly 프로젝트의 도움말 텍스트 스타일 가이드에서 제공된 조언을 따르는 것을 고려해 보십시오.

의존성

의존성은 최소한으로 유지되어야 합니다. 새로운 의존성의 도입은 승인 가이드라인에 따라 Merge Request에서 논의되어야 합니다. 새로운 의존성의 보안 상태 및 라이선스 호환성을 보장하기 위해 모든 프로젝트에 Dependency Scanning을 활성화해야 합니다.

모듈

Go 1.11 이후로 Go Modules이라는 표준 의존성 시스템이 사용 가능합니다. 이것은 재현 가능한 빌드를 위해 의존성을 정의하고 잠글 수 있는 방법을 제공합니다. 가능한 경우 이를 사용해야 합니다.

Go 모듈이 사용되고 있을 때에는 vendor/ 디렉터리가 있으면 안 됩니다. 대신에, Go는 프로젝트를 빌드하기 위해 필요한 의존성을 자동으로 다운로드합니다. 이는 루비 프로젝트의 Bundler와 의존성 처리 방식과 일치하며, Merge Request을 리뷰하기가 더 쉽습니다.

다른 프로젝트의 CI 실행에 의해 의존성으로서 Go 프로젝트를 빌드하는 경우와 같이 어떤 경우에는 vendor/ 디렉터리를 제거하면 코드가 반복해서 다운로드되어 네트워크 문제로 인해 일시적인 문제가 발생할 수 있습니다. 이러한 경우에는 의존성을 캐싱하기 위해 다운로드한 코드를 캐싱해야 합니다.

Go 이전 버전(v1.11.4 이하)에서 모듈 체크섬 버그이 있었으므로 checksum mismatch 오류를 피하기 위해 반드시 이 버전 이상을 사용하십시오.

ORM

GitLab에서는 객체 관계 매핑 라이브러리(ORM)를 사용하지 않습니다(단, Ruby on Rails의 ActiveRecord는 예외입니다). 프로젝트는 서비스를 사용하여 구조화될 수 있습니다. PostgreSQL 데이터베이스와 상호 작용하기에는 pgx가 충분합니다.

마이그레이션

호스팅된 데이터베이스를 관리해야 하는 특별한 경우, ActiveRecord가 제공하는 마이그레이션 시스템을 사용해야 합니다. postgres 컨테이너에서 사용할 수 있도록 설계된 Journey와 같은 간단한 라이브러리는 새 버전에 대한 데이터 마이그레이션을 자동으로 수행하는 새로운 Pod로 배포할 수 있습니다.

테스트

테스트 프레임워크

시작하는 데 이미 모든 것을 제공하는 표준 라이브러리가 있기 때문에 특정 라이브러리나 프레임워크를 사용해서는 안됩니다. 더 정교한 테스트 도구가 필요한 경우, 다음 외부 의존성은 특정 라이브러리나 프레임워크를 사용하기로 결정할 때 고려할 가치가 있을 수 있습니다.

Subtests

가능한 경우 서브테스트를 사용하여 코드 가독성과 테스트 출력을 개선하세요.

테스트에서 더 나은 출력

테스트에서 예상 및 실제 값 비교시, 구조체, 오류, 텍스트 큰 부분 또는 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) // 예상 값이 먼저오고 그 다음에 실제 값이 온다는 점에 유의하세요 ("차이" 의미론)
    })
}

테이블 주도 테스트

동일한 함수에 대해 여러 입력/출력 항목이 있는 경우 테이블 주도 테스트를 사용하는 것이 일반적으로 좋은 방법입니다. 아래는 테이블 주도 테스트 작성 시 따를 수 있는 몇 가지 지침입니다. 이러한 지침은 대부분 Go 표준 라이브러리 소스 코드에서 추출된 것입니다. 합리적할 때는 이러한 지침을 따르지 않아도 괜찮습니다.

테스트 케이스 정의

각 테이블 항목은 입력과 예상 결과뿐만 아니라 테스트 출력을 쉽게 읽을 수 있도록 테스트 이름과 같은 추가 정보가 포함된 완전한 테스트 케이스입니다.

  • 테스트 내부에 익명 구조체 슬라이스를 정의하세요(https://github.com/golang/go/blob/50bd1c4d4eb4fac8ddeb5f063c099daccfb71b26/src/encoding/csv/reader_test.go#L16).
  • 테스트 외부에 익명 구조체 슬라이스를 정의하세요(https://github.com/golang/go/blob/55d31e16c12c38d36811bdee65ac1f7772148250/src/cmd/go/internal/module/module_test.go#L9-L66).
  • 코드 재사용을 위해 명명된 구조체(https://github.com/golang/go/blob/2e0cd2aef5924e48e1ceb74e3d52e76c56dd34cc/src/cmd/go/internal/modfetch/coderepo_test.go#L54-L69).
  • map[string]struct{} 사용(https://github.com/golang/go/blob/6d5caf38e37bf9aeba3291f1f0b0081f934b1187/src/cmd/trace/annotations_test.go#L180-L235).

테스트 케이스 내용

이상적으로 각 테스트 케이스는 하위 테스트에 사용할 고유 식별자 필드를 가져야 합니다. Go 표준 라이브러리에서는 이를 일반적으로 name string 필드로 지정합니다. 테스트 케이스를 지정할 때 want/expect/actual을 사용하세요.

변수 이름

각 테이블 주도 테스트 맵/구조체 슬라이스는 tests로 이름 지을 수 있습니다. tests를 반복할 때 익명 구조체를 tt 또는 tc로 참조할 수 있습니다. 테스트 설명은name/testName/tn으로 지칭할 수 있습니다.

벤치마크

많은 IO 또는 복잡한 작업을 처리하는 프로그램은 시간에 따른 성능 일관성을 보장하기 위해 항상 벤치마크를 포함해야 합니다.

오류 처리

컨텍스트 추가

오류를 반환하기 전에 오류 앞에 컨텍스트를 추가하는 것은 프로그램이 오류 상태에 들어갔을 때 무엇을 시도했는지 이해할 수 있도록 도와줄 수 있습니다.

예:

// 오류 감싸기
return nil, fmt.Errorf("get cache %s: %w", f.Name, err)

// 컨텍스트만 추가
return nil, fmt.Errorf("saving cache %s: %v", f.Name, err)

컨텍스트를 추가할 때 고려해야 할 몇 가지 사항:

  • 호출자에게 내부 오류를 노출할지 여부를 결정하세요. 그렇다면 %w를 사용하고, 그렇지 않으면 %v를 사용할 수 있습니다.
  • “실패했습니다”, “오류가 있습니다”, “작동하지 않았습니다”와 같은 단어를 사용하지 마세요. 이것은 오류이므로 사용자는 이미 어떤 것이 실패했음을 알고 있으며, 결과적으로 “실패한 xx 실패한 xx 실패한 xx”와 같은 문자열을 갖게 될 수 있습니다. 대신 실패한 _무엇_을 설명하세요.
  • 오류 문자열을 대문자로 만들지 마세요. 마침표나 줄 바꿈으로 끝나도록 하지 마세요. 이를 확인하기 위해 golint를 사용할 수 있습니다.

표기

센티넬 오류를 사용할 때는 항상 ErrXxx와 같이 이름을 지정해야 합니다. 새로운 오류 유형을 만들 때는 항상 XxxError와 같이 이름을 붙여야 합니다.

오류 유형 확인

오류 동등성을 확인하려면 == 대신에 errors.Is를 사용하세요(Go 버전 >= 1.13). 특정 유형의 오류인지 확인하려면 형 단언을 사용하는 대신에 errors.As를 사용하세요(Go 버전 >= 1.13).

오류 처리 작업을 위한 참조

CLIs

모든 Go 프로그램은 명령줄에서 시작됩니다. cli는 명령행 앱을 생성하기 위한 편리한 패키지입니다. 이 패키지는 프로젝트가 데몬이건 간단한 CLI 도구이건 항상 사용해야 합니다. 플래그는 환경 변수에 직접 매핑될 수 있으며, 프로그램의 모든 가능한 명령행 상호 작용을 문서화하고 중앙 집중화할 수 있습니다. os.GetEnv를 사용하지 마세요. 코드 깊숙히 숨겨진 변수를 가져와야 하기 때문입니다.

라이브러리

LabKit

LabKit은 Go 서비스를 위한 공통 라이브러리를 보관하는 곳입니다. LabKit을 사용하는 예제는 workhorsegitaly를 참조하세요. LabKit은 세 가지 관련 기능을 공개합니다.

이는 Workhorse, Gitaly 및 다른 Go 서버에서 일관된 설정 메커니즘(즉, GITLAB_TRACING 환경 변수)을 유지하면서, 직접적으로 Opentracing을 사용하거나 Zipkin이나 Go kit의 추적 래퍼를 사용하도록 전환할 수 있음을 의미합니다. 이러한 변경은 응용 프로그램 코드를 수정하지 않고도 가능합니다.

구조화된 (JSON) 로깅

모든 이진 파일에는 로그를 검색하고 필터링하는 데 도움이 되는 구조화된 (JSON) 로깅이 이상적으로 있어야 합니다. LabKit은 Logrus를 추상화합니다. JSON 형식으로 구조화된 로깅을 사용하며, 모든 인프라가 그것을 가정하고 있기 때문에 Logrus를 사용할 때 기본 JSON 포매터를 사용하여 구조화된 로깅을 활성화할 수 있습니다. 이는 우리의 Ruby 애플리케이션에서 사용하는 로깅 유형을 따릅니다.

Logrus 사용 방법

Logrus 패키지를 사용할 때 몇 가지 지침이 있습니다:

  • 에러를 출력할 때는 WithError를 사용하세요. 예: logrus.WithError(err).Error("작업 실패").
  • 구조화된 로깅을 사용하는 경우 해당 코드 경로의 필드를 로그에 남길 수 있습니다. 예를 들어, logrus.WithField("file", "/app/go").Info("디렉터리 열기")와 같이 요청의 URI와 같은 필드를 기록할 수 있습니다. 여러 키를 로그에 남겨야 하는 경우 한 번 이상 WithField를 호출하는 대신 항상 WithFields를 사용하세요.

컨텍스트

데몬은 장기 실행 애플리케이션 이므로 취소를 관리하고 불필요한 리소스 사용을 피하며(DDoS 취약점으로 이어질 수 있음)는 메커니즘이 있어야 합니다. Go Context를 함수에 사용하고 첫 번째 매개변수로 전달해야 합니다.

Dockerfiles

모든 프로젝트는 리포지터리의 루트에 Dockerfile을 가져야 하며, 프로젝트를 빌드하고 실행해야 합니다. Go 프로그램은 정적 바이너리이므로 외부 의존성이 필요하지 않으며, 최종 이미지의 셸은 불필요합니다. Multistage builds를 사용하는 것이 좋습니다:

  • 사용자는 올바른 Go 버전 및 의존성으로 프로젝트를 빌드할 수 있습니다.
  • Scratch에서 파생된 작고 독립적인 이미지가 생성됩니다.

생성된 Docker 이미지는 이식 가능한 명령을 만들기 위해 Entrypoint에서 프로그램이 있어야 합니다. 이렇게 하면 누구나 이미지를 실행할 수 있으며 매개변수 없이 실행하는 경우(cli를 사용하는 경우) 도움말 메시지가 표시됩니다.

보안 팀 표준 및 스타일 가이드라인

다음은 Secure 팀에 특정한 몇 가지 스타일 가이드라인입니다.

코드 스타일 및 형식

커밋하기 전에 goimports -local gitlab.com/gitlab-org를 사용하세요. goimports는 import 라인을 포맷팅하고 누락된 것을 추가하고 참조되지 않는 것을 제거하는 것 외에도 Go 소스 코드를 자동으로 포맷팅하는 도구입니다. -local gitlab.com/gitlab-org 옵션을 사용하면 goimports는 로컬 참조 패키지를 외부 패키지와 별도로 그룹화합니다. 자세한 내용은 Go 위키의 Code Review Comments 페이지의 imports 섹션을 참조하세요. 대부분의 편집기/IDE는 파일을 저장하기 전/후 명령을 실행할 수 있으며, 모든 파일에 적용되도록 goimports -local gitlab.com/gitlab-org를 설정할 수 있습니다.

브랜치 이름 지정

GitLab 브랜치 이름 규칙에 추가하여 브랜치 이름에는 a-z, 0-9, - 문자만 사용하세요. 이 제한은 /와 같은 특정 문자가 포함된 경우 go get이 예상대로 작동하지 않기 때문에 취급합니다.

슬라이스 초기화

슬라이스를 초기화할 때 가능하면 용량을 제공하여 추가적인 할당을 피하세요.

해석된 원문 완료

개발 문서로 돌아가기.