Golang 코딩 표준 기본 지침

defer를 사용하여 리소스 해제

파일이나 락과 같은 리소스를 해제하기 위해 defer를 사용합니다.

비권장 사항:

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// 여러 반환 분기가 있는 경우 언락을 잊기 쉽습니다

권장 사항:

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// 더 읽기 쉽습니다

defer의 오버헤드는 매우 낮기 때문에 함수 실행 시간이 나노초 수준이라는 것을 증명할 수 있는 경우에만 피해야 합니다. defer를 사용하여 가독성을 향상시키는 것은 비용이 무시할 수 있기 때문에 가치가 있습니다. 이는 다른 계산의 리소스 소비가 defer의 것을 훨씬 능가하는 복잡한 메서드에서 특히 적용됩니다.

채널 크기는 1 또는 버퍼가 없어야 합니다

일반적으로 채널은 크기가 1이거나 버퍼가 없어야 합니다. 기본적으로 채널은 버퍼가 없는 크기 0입니다. 다른 크기는 엄격히 검토되어야 합니다. 채널 크기를 어떻게 결정할지, 고부하에서 채널 쓰기를 방지하는 방법 및 차단되었을 때 시스템 논리에 어떤 변화가 발생하는지를 고려해야 합니다.

비권장 사항:

// 어떤 상황에서도 처리할 수 있어야 합니다!
c := make(chan int, 64)

권장 사항:

// 크기: 1
c := make(chan int, 1) // 또는
// 버퍼가 없는 채널, 크기는 0
c := make(chan int)

열거형은 1부터 시작합니다

Go에서 열거형을 도입하는 표준 방법은 자신만의 타입을 선언하고 iota를 사용하는 const 그룹을 선언하는 것입니다. 변수의 기본값이 0이므로 열거형은 일반적으로 0이 아닌 값으로 시작해야 합니다.

비권장 사항:

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2

권장 사항:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

어떤 경우에는 0부터 시작하는 것이 합리적일 수 있습니다(0부터 시작하는 열거형), 예를 들어, 0 값이 이상적인 기본 동작일 때.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

atomic 사용

원시 타입(int32, int64 등)에 대해 sync/atomic 패키지의 atomic 작업을 사용하여 변수를 읽거나 수정하는 작업을 해야 합니다.

go.uber.org/atomic는 이러한 작업에 타입 안전성을 추가하여 기존 타입을 숨깁니다. 또한 편리한 atomic.Bool 타입을 포함하고 있습니다.

비권장 접근 방식:

type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // 이미 실행 중...
     return
  }
  // Foo 시작
}

func (f *foo) isRunning() bool {
  return f.running == 1  // 경쟁 상태!
}

권장 접근 방식:

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // 이미 실행 중...
     return
  }
  // Foo 시작
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

변경 가능한 전역 변수 피하기

전역 변수를 변경하지 않도록 의존성 주입 접근 방식을 사용하십시오. 이는 함수 포인터 및 다른 값 타입에도 적용됩니다.

비권장 접근 방식 1:

// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}

권장 접근 방식 1:

// sign.go
type signer struct {
  now func() time.Time
}
func newSigner() *signer {
  return &signer{
    now: time.Now,
  }
}
func (s *signer) Sign(msg string) string {
  now := s.now()
  return signWithTime(msg, now)
}

비권장 접근 방식 2:

// sign_test.go
func TestSign(t *testing.T) {
  oldTimeNow := _timeNow
  _timeNow = func() time.Time {
    return someFixedTime
  }
  defer func() { _timeNow = oldTimeNow }()
  assert.Equal(t, want, sign(give))
}

권장 접근 방식 2:

// sign_test.go
func TestSigner(t *testing.T) {
  s := newSigner()
  s.now = func() time.Time {
    return someFixedTime
  }
  assert.Equal(t, want, s.Sign(give))
}

사전 선언된 식별자 사용 피하기

Go 언어 사양서는 Go 프로젝트에서 사용하지 말아야 하는 여러 사전 선언된 식별자를 기술하고 있습니다. 이러한 사전 선언된 식별자는 현재 범위 (또는 중첩된 범위)에서 원래 식별자를 숨길 수 있어 코드 혼란을 유발할 수 있으므로 다른 컨텍스트의 이름으로 재사용해서는 안됩니다. 최선의 경우 컴파일러는 오류를 발생시킬 수 있고, 최악의 경우 해당 코드는 복구하기 어려운 오류를 발생시킬 수 있습니다.

비권장 방식 1:

var error string
// `error`가 내장 식별자를 암시적으로 가림

// 또는

func handleErrorMessage(error string) {
    // `error`가 내장 식별자를 암시적으로 가림
}

권장 방식 1:

var errorMessage string
// `error`는 이제 숨겨지지 않은 내장 식별자를 가리킴

// 또는

func handleErrorMessage(msg string) {
    // `error`는 이제 숨겨지지 않은 내장 식별자를 가리킴
}

비권장 방식 2:

type Foo struct {
    // 이러한 필드는 기술적으로 가려지지는 않지만, `error`나 `string` 문자열을 재정의하는 것은 애매해집니다.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` 및 `f.error`는 시각적으로 유사하게 보입니다.
    return f.error
}

func (f Foo) String() string {
    // `string` 및 `f.string`은 시각적으로 유사하게 보입니다.
    return f.string
}

권장 방식 2:

type Foo struct {
    // `error`와 `string`은 이제 명시적으로 표시됩니다.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

사전 선언된 식별자를 사용할 때 컴파일러는 오류를 생성하지 않지만, go vet과 같은 도구는 이러한 및 다른 함의적인 문제를 올바르게 가리킵니다.

init() 사용을 피하십시오

가능한 한 init() 사용을 피하십시오. init()를 피할 수 없거나 선호하는 경우 코드는 다음을 시도해야합니다.

  1. 프로그램 환경 또는 호출에 관계없이 완전성을 보장합니다.
  2. 다른 init() 함수의 순서나 부작용에 의존하지 않습니다. init()의 순서는 명시적이지만 코드가 변경될 수 있으므로 init() 함수 간의 관계는 코드를 부서지고 오류가 발생하기 쉬울 수 있습니다.
  3. 머신 정보, 환경 변수, 작업 디렉토리, 프로그램 매개변수/입력 등과 같은 전역 또는 환경 상태에 액세스하거나 조작하는 것을 피합니다.
  4. 파일 시스템, 네트워킹, 시스템 호출을 포함한 I/O를 피합니다.

이러한 요구 사항을 충족하지 않는 코드는 main() 호출의 일부로 속할 수도 있습니다(또는 프로그램 수명 주기의 다른 곳) 또는 main() 자체의 일부로 작성될 수 있습니다. 특히 다른 프로그램에서 사용하도록 의도된 라이브러리는 "init magic"을 수행하는 대신 완전성에 특별히 주의해야 합니다.

권장하지 않는 방법 1:

type Foo struct {
    // ...
}
var _defaultFoo Foo
func init() {
    _defaultFoo = Foo{
        // ...
    }
}

권장하는 방법 1:

var _defaultFoo = Foo{
    // ...
}
// 또는, 테스트 용이성을 위해:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

권장하지 않는 방법 2:

type Config struct {
    // ...
}
var _config Config
func init() {
    // 나쁨: 현재 디렉토리에 기반
    cwd, _ := os.Getwd()
    // 나쁨: I/O
    raw, _ := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

권장하는 방법 2:

type Config struct {
    // ...
}
func loadConfig() Config {
    cwd, err := os.Getwd()
    // 에러 처리
    raw, err := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // 에러 처리
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

위의 고려 사항에 따라 경우에 따라 init()가 더 선호되거나 필요할 수 있습니다.

  • 복잡한 표현식의 단일 할당으로 표현할 수 없는 경우
  • database/sql, 유형 레지스트리 등 삽입 가능한 후크

값을 추가할 때 슬라이스 용량 지정을 우선시하십시오

슬라이스를 추가할 때 make()에 용량 값을 지정하는 것을 우선시하십시오.

권장하지 않는 방법:

for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

권장하는 방법:

for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

구조체 직렬화 시 필드 태그 사용

JSON, YAML 또는 필드 태그를 기반으로하는 기타 형식으로 직렬화할 때 관련된 태그를 주석으로 사용해야합니다.

권장하지 않는 방법:

type Stock struct {
  Price int
  Name  string
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

권장하는 방법:

type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // Name을 Symbol로 안전하게 변경 가능.
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

이론적으로 구조체의 직렬화 형식은 다른 시스템 간의 계약입니다. 구조체의 직렬화 형식(필드 이름 포함)을 변경하면 이 계약이 깨질 수 있습니다. 태그에서 필드 이름을 지정함으로써 계약을 명확하게 만들고 필드의 리팩터링이나 이름 바꾸기를 통한 무작위 계약 위반을 방지하는 데 도움이 됩니다.