지나치게 긴 라인 피하기

수평으로 스크롤하거나 문서를 지나치게 회전할 필요가 있는 코드 라인을 사용하지 않도록합니다.

라인 길이를 99자로 제한하는 것을 권장합니다. 저기선 이 한계 이전에 라인을 나누는 것이 좋지만, 이것은 엄격한 규칙은 아닙니다. 코드가 이 한계를 초과해도 괜찮습니다.

일관성

이 문서에 기술된 표준 중 일부는 주관적인 판단, 시나리오 또는 맥락에 기반합니다. 그러나 가장 중요한 측면은 일관성을 유지하는 것입니다.

일관된 코드는 유지 보수가 쉽고, 더 합리적이며, 학습 비용이 적게 들며, 새로운 규칙이 나타나거나 오류가 발생했을 때 이주 또는 업데이트, 오류 수정이 쉽습니다.

반면에 코드베이스에 여러 가지 다른 또는 상충하는 코드 스타일을 포함하면 유지 비용이 증가하고, 불확실성과 인지적 편향이 증가합니다. 이러한 것들은 직접적으로 더 느린 속도, 고통스러운 코드 리뷰 및 증가하는 버그의 수로 이어집니다.

이러한 표준을 코드베이스에 적용할 때, 패키지(또는 더 큰 단위) 수준에서 변경하는 것이 좋습니다. 하위 패키지 수준에서 여러 스타일을 적용하는 것은 위의 우려 사항을 위반합니다.

유사한 선언을 그룹화

Go 언어는 유사한 선언을 그룹화하는 것을 지원합니다.

비추천:

import "a"
import "b"

권장:

import (
  "a"
  "b"
)

이는 상수, 변수 및 유형 선언에도 적용됩니다.

비추천:

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

권장:

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

관련된 선언만 함께 그룹화하고 관련없는 선언을 그룹화하지 않도록합니다.

비추천:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

권장:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

그룹화를 사용하는 위치에는 제한이 없습니다. 예를 들어, 함수 내에서도 사용할 수 있습니다.

비추천:

func f() string {
  red := color.New(0xff0000)
  green := color.New(0x00ff00)
  blue := color.New(0x0000ff)

  ...
}

권장:

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

예외: 변수 선언이 다른 변수와 인접한 경우, 특히 함수-로컬 선언 내에서인 경우에도 이를 함께 그룹화해야합니다. 관련 없는 변수가 함께 선언되어도 이를 수행해야합니다.

비추천:

func (c *client) request() {
  caller := c.name
  format := "json"
  timeout := 5*time.Second
  var err error
  // ...
}

권장:

func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )
  // ...
}

Import 그룹화

Import는 두 가지 범주로 그룹화되어야 합니다:

  • 표준 라이브러리
  • 다른 라이브러리

기본적으로 goimports에서 적용하는 그룹화입니다. 비추천:

import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

권장:

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

패키지 이름

패키지 이름을 지을 때는 다음 규칙을 따르십시오:

  • 모든 글자는 소문자입니다. 대문자나 밑줄을 사용하지 않습니다.
  • 대부분의 경우, 가져올 때 이름을 변경할 필요가 없습니다.
  • 짧고 간결합니다. 사용된 모든 곳에서 이름이 완전하게 정규화되는 것을 기억하십시오.
  • 복수형을 피합니다. 예를 들어 net/urls 대신에 net/url을 사용합니다.
  • "common," "util," "shared," 또는 "lib"를 사용하지 않습니다. 이러한 것들은 정보를 제공하지 않습니다.

함수 명명

저희는 함수명에 MixedCaps를 사용하는 Go 커뮤니티 관례를 준수합니다. 함수명에 언더스코어가 포함되어있는 테스트 케이스의 관련 그룹화에서는 예외가 있습니다. 즉, TestMyFunction_WhatIsBeingTested와 같이 함수명에 언더스코어가 포함될 수 있습니다.

Import 별칭

패키지 이름이 임포트 경로의 마지막 요소와 일치하지 않을 경우에는 임포트 별칭을 사용해야 합니다.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

다른 모든 경우에는 바로가기 별칭은 임포트 충돌이 직접적으로 있는 경우를 제외하고 피해야 합니다. 권장하지 않음:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

추천:

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

함수 그룹화 및 순서

  • 함수는 호출되는 순서에 대략적으로 정렬되어야 합니다.
  • 동일한 파일 내에서의 함수는 수신자(receiver)로 그룹화되어야 합니다.

따라서, 노출된 함수는 파일의 가장 앞에 나와야 하며, struct, const, var 정의 뒤에 배치되어야 합니다.

newXYZ()/NewXYZ()는 타입 정의 이후에, 수신자의 나머지 메서드들 앞에 올 수 있습니다.

함수가 수신자에 의해 그룹화되므로, 일반적인 유틸리티 함수는 파일의 끝에 나와야 합니다. 권장하지 않음:

func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

추천:

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

중첩 줄이기

코드는 오류/특수 경우를 가능한 한 빨리 처리하여 중첩을 줄이고, 반환하거나 루프를 계속해야 합니다. 중첩을 줄이면 여러 수준의 코드 양이 줄어듭니다.

권장하지 않음:

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}

추천:

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

불필요한 else

변수가 if의 두 분기에서 모두 설정되는 경우, 단일 if 문으로 대체될 수 있습니다.

권장하지 않음:

var a int
if b {
  a = 100
} else {
  a = 10
}

추천:

a := 10
if b {
  a = 100
}

최상위 변수 선언

최상위에서는 표준 var 키워드를 사용합니다. 표현식의 유형이 표현식의 유형과 정확히 일치하지 않는 경우에만 유형을 지정하지 마십시오.

권장하지 않음:

var _s string = F()

func F() string { return "A" }

추천:

var _s = F()
// F가 명시적으로 문자열 유형을 반환하기 때문에 _s에 대해 유형을 명시적으로 지정할 필요가 없습니다.

func F() string { return "A" }

표현식의 유형이 필요한 유형과 정확하게 일치하지 않는 경우 유형을 지정합니다.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F는 myError 유형의 인스턴스를 반환하지만, 우리는 error 유형이 필요합니다

최상위 상수 및 변수에 대한 접두사 '_' 사용

공개되지 않은 최상위 varsconsts의 경우 사용 시 전역적인 성격을 명시적으로 나타내기 위해 언더스코어 _로 접두사를 붙입니다.

기본 근거: 최상위 변수와 상수는 패키지 수준 범위를 가집니다. 일반적인 이름을 사용하면 다른 파일에서 실수로 잘못된 값이 사용될 수 있습니다.

비권장:

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)

  // Bar()의 첫 번째 줄이 삭제되어도 컴파일 오류가 표시되지 않습니다.
}

권장:

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

예외:

구조체에 내장

내장 유형(예: mutex)은 구조체 내 필드 목록 상단에 배치되어야 하며, 내장된 필드와 일반 필드를 구분하기 위해 비어 있는 줄이 있어야 합니다.

비권장:

type Client struct {
  version int
  http.Client
}

권장:

type Client struct {
  http.Client

  version int
}

내장은 의미적으로 적절한 방식으로 기능을 추가하거나 향상시키는 등 구체적인 이점을 제공해야 합니다. 사용자에게 부정적인 영향을 미치지 않고 사용되어야 합니다(또한: 공개 구조체에 대한 유형 내부의 내장 피하기).

예외: 비공개 유형에서도 Mutex를 내장 필드로 사용해서는 안 됩니다. 또한: 제로 값 Mutex가 유효함.

내장 해서는 안 됩니다:

  • 단순한 미학적 또는 편의성의 목적으로 존재.
  • 외부 유형의 구성 또는 사용을 더 어렵게 만듭니다.
  • 외부 유형의 제로 값에 영향을 미칩니다. 외부 유형에 유용한 제로 값이 있는 경우, 내부 유형을 내장해도 여전히 유용한 제로 값이 있어야 합니다.
  • 내장된 내부 유형에서 관련 없는 함수나 필드를 노출시키는 부작용 발생.
  • 비공개 유형을 노출시킵니다.
  • 외부 유형의 복제 형태에 영향을 미칩니다.
  • 외부 유형의 API 또는 유형 의미론을 변경합니다.
  • 내부 유형을 비표준 형태로 내장합니다.
  • 외부 유형의 구현 세부 정보를 노출시킵니다.
  • 사용자가 내부 유형을 볼 수 있거나 제어할 수 있게 합니다.
  • 내부 함수의 일반 동작을 사용자가 예상하지 않을 방식으로 변경합니다.

요약하면, 의도적이고 명확한 내장만을 사용해야 합니다. 좋은 판별 기준은 "내부 유형에서 모든 공개된 메서드/필드가 외부 유형에 직접 추가됩니까?"라는 것입니다. 만약 답이 일부 또는 아니오라면 내부 유형을 내장하지 말고 필드를 사용하십시오.

비권장:

type A struct {
    // 안 좋은 형태: A.Lock() 및 A.Unlock()이 이제 사용 가능
    // 기능적인 이점을 제공하지 않으며 A의 내부 세부 사항을 사용자가 제어할 수 있도록합니다.
    sync.Mutex
}

권장:

type countingWriteCloser struct {
    // 좋은 형태: 특정 목적을 위해 외부 수준에서 Write()가 제공되며, 내부 유형의 Write()에 작업을 위임합니다.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

지역 변수 선언

변수가 명시적으로 값을 설정하는 경우 짧은 변수 선언 형식인 (:=)를 사용해야 합니다.

비권장:

var s = "foo"

권장:

s := "foo"

그러나 경우에 따라 기본 값에 대해 var 키워드를 사용하는 것이 더 명확할 수 있습니다.

비권장:

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

권장:

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

nil은 유효한 슬라이스입니다

nil은 길이가 0인 유효한 슬라이스이며, 이는 다음을 의미합니다:

  • 길이가 0인 슬라이스를 명시적으로 반환해서는 안 됩니다. 대신 nil을 반환하세요.

비권장:

if x == "" {
  return []int{}
}

권장:

if x == "" {
  return nil
}
  • 슬라이스가 비어 있는지 확인하려면 항상 len(s) == 0를 사용하세요. nil 대신.

비권장:

func isEmpty(s []string) bool {
  return s == nil
}

권장:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • 제로 값 슬라이스 ( var로 선언된 슬라이스)는 make()를 호출하지 않고 즉시 사용할 수 있습니다.

비권장:

nums := []int{}
// or, nums := make([]int)

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

권장:

var nums []int

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

기억하세요, nil 슬라이스는 유효한 슬라이스이지만 길이가 0인 슬라이스와는 동일하지 않으며(하나는 nil이고 다른 하나는 아님), 서로 다른 상황에서 다르게 취급될 수 있습니다(예: 직렬화).

변수 범위를 좁히기

가능하다면 변수 범위를 좁히세요. 단, 중첩을 줄이는 규칙과 충돌하지 않아야 합니다.

비권장:

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

권장:

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

if 문 바깥에서 함수 호출의 결과를 사용해야 하는 경우 변수 범위를 좁히려고 하지 마세요.

비권장:

if data, err := os.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}

권장:

data, err := os.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

맨날 변수 사용은 지양하세요

함수 호출 시 모호한 맨날 변수는 가독성을 해치기도 합니다. 매개변수 이름의 의미가 명확하지 않을 때는 C 스타일 주석(/* ... */)을 매개변수에 추가하세요.

비권장:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

권장:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

위 예제에 대해, 더 나은 접근 방법은 매개변수의 bool 유형을 사용자 지정 유형으로 대체하는 것일 수 있습니다. 이렇게 하면 나중에 두 가지 상태(true/false) 이상을 지원할 수 있을 수 있습니다.

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // 나중에 StatusInProgress가 추가될 수도 있습니다.
)

func printInfo(name string, region Region, status Status)

이스케이핑을 피하려면 원시 문자열 리터럴을 사용하세요

Go는 원시 문자열 리터럴을 지원합니다. 이는 " ` "로 표시되며 이스케이핑이 필요한 상황에서는 이 접근 방식을 사용하여 수동으로 이스케이핑된 문자열을 대체해야 합니다.

이는 여러 줄에 걸칠 수 있으며 따옴표를 포함할 수 있습니다. 이러한 문자열을 사용하면 수동으로 이스케이핑된 문자열보다 더 쉽게 읽을 수 있습니다.

비권장:

wantError := "unknown name:\"test\""

권장:

wantError := `unknown error:"test"`

구조체 초기화

필드 이름을 사용하여 구조체 초기화

구조체를 초기화할 때에는 거의 항상 필드 이름을 지정해야 합니다. 현재 이는 go vet에 의해 강제되고 있습니다.

비권장:

k := User{"John", "Doe", true}

권장:

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

예외: 필드가 3개 이하인 경우, 테스트 테이블에서 필드 이름을 생략할 수 있습니다.

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

구조체에서 제로 값 필드를 생략

명명된 필드를 사용하여 구조체를 초기화할 때, 유의미한 컨텍스트가 제공되지 않는 한 제로 값을 가진 필드를 무시해야 합니다. 즉, 이러한 필드들은 자동으로 제로 값으로 설정되도록 해야 합니다.

비권장:

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

권장:

user := User{
  FirstName: "John",
  LastName: "Doe",
}

이는 컨텍스트에서 기본값을 생략함으로써 읽기 장벽을 줄이는 데 도움이 됩니다. 유의미한 값만 지정하십시오.

필드 이름이 유의미한 컨텍스트를 제공하는 경우에는 제로 값을 포함하십시오. 예를 들어, 테이블 기반 테스트의 테스트 케이스는 필드 이름을 지정하는 것이 유리할 수 있습니다.

tests := []struct{
  give string
  want int
}{
  {give: "0", want: 0},
  // ...
}

제로 값 구조체에 대해 var 사용

구조체의 모든 필드가 선언에서 생략된 경우, 해당 구조체를 선언할 때 var를 사용하십시오.

비권장:

user := User{}

권장:

var user User

이는 제로 값 필드와 비제로 값 필드를 구분짓고, 비슷한 방식으로 빈 슬라이스를 선언할 때 선호하는 방식과 유사합니다.

구조체 참조 초기화

구조체 참조를 초기화할 때에는 구조체 초기화와 일관성을 유지하기 위해 new(T) 대신 &T{}를 사용하십니다.

비권장:

sval := T{Name: "foo"}

// 일관성 없음
sptr := new(T)
sptr.Name = "bar"

권장:

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

맵 초기화

빈 맵을 초기화하려면 make(..)를 사용하여 초기화하고, 이 맵은 프로그래밍적으로 채워집니다. 이는 맵의 초기화를 선언과 외형적으로 다르게 만들 뿐만 아니라, make 후에 크기 힌트를 추가하는 것도 편리하게 만들어줍니다.

비권장:

var (
  // m1은 읽기-쓰기 안전함; 
  // m2는 쓸 때 패닉이 발생함
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

권장:

var (
  // m1은 읽기-쓰기 안전함; 
  // m2는 쓸 때 패닉이 발생함
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

| 선언과 초기화가 매우 유사함. | 선언과 초기화가 매우 다름. |

가능한 경우, 초기화하는 동안 맵 용량 크기를 제공하고, 세부 정보를 위해 맵 용량 지정을 참조하십시오.

또한, 맵이 고정된 요소 목록을 포함하는 경우, 맵 리터럴을 사용하여 맵을 초기화하십시오.

비권장:

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3

권장:

m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

기본 가이드라인은 초기화하는 동안 고정된 집합의 요소를 추가하는 경우 맵 리터럴을 사용하는 것입니다. 그렇지 않으면 make를 사용하십시오(가능한 경우, 맵 용량을 지정하십시오).

Printf-스타일 함수를 위한 문자열 형식

Printf-스타일 함수의 형식 문자열을 함수 외부에서 선언하는 경우, 이를 const 상수로 설정하십시오.

이는 형식 문자열에 대한 정적 분석을 수행하기 위해 go vet에 도움이 됩니다.

비권장:

msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

권장:

const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Printf 스타일 함수명 지정

Printf-스타일 함수를 선언할 때는 go vet이 형식 문자열을 감지하고 확인할 수 있도록 해야 합니다.

이는 가능한 한 미리 정의된 Printf-스타일 함수 이름을 사용해야 함을 의미합니다. go vet은 기본적으로 이를 확인합니다. 자세한 정보는 Printf Family를 참조하십시오.

미리 정의된 이름을 사용할 수 없는 경우 선택한 이름을 f로 끝내야 합니다: Wrap 대신 Wrapf. go vet은 특정 Printf-스타일 이름을 확인하도록 요청할 수 있지만 해당 이름은 반드시 f로 끝나야 합니다.

go vet -printfuncs=wrapf,statusf