지나치게 긴 라인 피하기
수평으로 스크롤하거나 문서를 지나치게 회전할 필요가 있는 코드 라인을 사용하지 않도록합니다.
라인 길이를 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 유형이 필요합니다
최상위 상수 및 변수에 대한 접두사 '_' 사용
공개되지 않은 최상위 vars
와 consts
의 경우 사용 시 전역적인 성격을 명시적으로 나타내기 위해 언더스코어 _
로 접두사를 붙입니다.
기본 근거: 최상위 변수와 상수는 패키지 수준 범위를 가집니다. 일반적인 이름을 사용하면 다른 파일에서 실수로 잘못된 값이 사용될 수 있습니다.
비권장:
// 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