Go 코드 리뷰 코멘트

Go Code Review Comments
Go 코드 리뷰 코멘트

1 Gofmt[ | ]

코드에 대해, 대부분의 기계적 스타일 문제를 자동으로 고쳐주는 gofmt을 실행하자. 거의 모든 Go 코드는 gofmt를 사용한다. 이 문서의 나머지 부분에서는 비기계적 스타일 포인트를 다룬다.

대안적으로 goimports를 사용할 수 있는데, 이는 gofmt의 상위집합이며, 필요에 따라 import 라인을 추가 및 제거해준다.

2 Comment Sentences[ | ]

https://golang.org/doc/effective_go.html#commentary 도 참고하자. 선언을 문서화하는 주석은 약간 중복인 것 같더라도 완전한 문장이어야 한다. 그렇게 하면 godoc 문서를 추출할 때 형식이 잘 만들어진다. 주석은 설명되는 것의 이름으로 시작하고 마침표로 끝나야 한다:

// Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

3 Contexts[ | ]

context.Context 타입의 값은 API 및 프로세스 경계를 넘어 보안 크리덴셜, 추적 정보, 데드라인 및 취소 시그널을 전달한다. Go 프로그램은 들어오는 RPC 및 HTTP 요청에서 나가는 요청으로 전체 함수 호출 체인을 따라 명시적으로 컨텍스트를 전달한다.

컨텍스트를 사용하는 대부분의 함수는 컨텍스트를 첫 번째 매개변수로 받아야 한다:

func F(ctx context.Context, /* other arguments */) {}

전혀 요청에 관여되지 않는 함수는 context.Background()를 사용할 수도 있지만, 불필요하다고 생각되는 경우라도 Context 전달 차원에서 오류가 발생한다. 일반적인 경우에는 컨텍스트를 전달하도록 하고, context.Background()는 그 외의 다른 방법이 잘못되었다는 합당한 이유가 있는 경우에만 사용하자.

구조체 타입에 Context 멤버를 추가하지 말자. 대신 전달해야 하는 해당 타입의 각 메소드에 ctx 매개변수를 추가하자. 단 한 가지 예외는, 메소드의 시그니처가 표준 라이브러리 또는 타사 라이브러리의 인터페이스와 일치해야 하는 경우뿐이다.

커스텀 컨텍스트 타입을 만들거나, 함수 시그니처에서 컨텍스트 이외의 인터페이스를 사용하지 말자.

전달할 애플리케이션 데이터가 있다면 매개변수, 리시버, 전역에 넣고, 꼭 필요한 경우에만 컨텍스트 값에 넣는다.

컨텍스트는 변경할 수 없으므로(immutable), 동일한 데드라인, 취소 시그널, 크리덴셜, 부모 추적 등을 공유하는 여러 호출에 같은 ctx를 전달하는 것은 괜찮다.

4 Copying[ | ]

예상치 못한 별칭 지정(aliasing)을 방지하려면, 다른 패키지에서 구조체를 복사할 때 주의가 필요하다. 예를 들어, bytes.Buffer 타입은 []byte 슬라이스를 포함하는데, Buffer를 복사하면 복사본의 슬라이스가 원본 배열의 별칭으로 지정되어. 이후 메소드 호출시에 의도하지 않은 효과가 발생할 수 있다.

일반적으로, 메소드가 포인터 타입 *T와 연계된 경우 T 타입의 값은 복사하지 않도록 한다.

5 Crypto Rand[ | ]

키를 생성할 때 math/rand 패키지를 사용하지 말자(한번만 사용하고 버리는 경우라도). 시드되지 않은 생성기는 완전 예측 가능하다. time.Nanoseconds()로 시드하면 엔트로피가 약간 있긴 하다. 어쨌든 그것 대신 crypto/rand의 Reader를 사용하고, 텍스트가 필요한 경우에는 16진수 또는 base64로 출력하자:

import (
	"crypto/rand"
	// "encoding/base64"
	// "encoding/hex"
	"fmt"
)

func Key() string {
	buf := make([]byte, 16)
	_, err := rand.Read(buf)
	if err != nil {
		panic(err)  // out of randomness, should never happen
	}
	return fmt.Sprintf("%x", buf)
	// or hex.EncodeToString(buf)
	// or base64.StdEncoding.EncodeToString(buf)
}

6 Declaring Empty Slices[ | ]

빈 슬라이스를 선언할 때는, 다음 2가지 중 앞의 것이 좋다.

var t []string
t := []string{}

앞의 것은 nil 슬라이스 값을 선언하지만, 뒤의 것은 nil은 아니지만 길이는 0이다. lencap은 모두 0이라서 기능적으로는 동일해 보이지만, nil 슬라이스가 더 선호되는 스타일이다.

다만 JSON 객체를 인코딩할 때처럼, nil은 아니지만 길이가 0인 슬라이스가 선호되는 특수한 상황도 있다(nil 슬라이스는 null로 인코딩되고, []string{}은 JSON 배열 []로 인코딩된다).

인터페이스를 설계할 때는, nil 슬라이스와, nil이 아닌 길이가 0인 슬라이스를 구별하지 않도록 하자. 미묘한 프로그래밍 오류가 발생할 수 있다.

Go에서의 nil에 대한 자세한 내용은 Francesc Campoy의 이야기 Nil 이해하기를 참조하자.

7 Doc Comments[ | ]

모든 최상위, exported 이름에는 문서 주석(doc comment)이 있어야 한다(중요한 unexported type이나 함수 선언도 마찬가지) 주석 규칙에 대한 자세한 내용은 https://golang.org/doc/effective_go.html#commentary 를 참고하자.

8 Don't Panic[ | ]

https://golang.org/doc/effective_go.html#errors 를 참고하자. 일반적인 오류 처리에는 panic을 사용하지 말고, error와 여러 return 값을 사용하자.

9 Error Strings[ | ]

오류 문자열은 일반적으로 다른 컨텍스트에 따라 출력되기 때문에, (고유명사 또는 두문자어로 시작하지 않는 한) 대문자로 시작하거나 마침표로 끝나면 안된다. 즉, fmt.Errorf("Something bad")가 아니라 fmt.Errorf("something bad")를 사용하여, log.Printf("Reading %s: %v", filename, err) 형식에서 메시지 중간에 불필요한 대문자가 없도록 한다. 다만 이것은 - 암묵적으로 줄 단위 중심이며 다른 메시지 내부에 조합되지 않는 - 로깅에는 적용되지 않는다.

10 Examples[ | ]

새 패키지를 추가할 때에는, 의도하는 사용법 예시(실행 가능한 예시, 또는 완전한 호출 절차를 보여주는 간단한 테스트)를 포함시키자.

테스트 가능한 Example() 함수에 대해서도 읽어 보자.

11 Goroutine Lifetimes[ | ]

고루틴을 생성할 때에는 언제 어디서 종료될지를 분명히 하자.

고루틴은 채널 송신 또는 수신을 차단하면 누수될 수 있다. 가비지 콜렉터는 채널이 차단되어 연결할 수 없는 경우에도 고루틴을 종료시키지 않는다.

또한 고루틴은 누수되지 않더라도 불필요하게 떠 있으면, 다른 미묘하고 진단하기 어려운 문제가 발생할 수 있다. 닫힌 채널로 송신하면 패닉이 발생할 수 있다. "결과가 필요 없어진 이후라도" 사용중인 입력을 변경하면 데이터 경합이 발생할 수 있다. 그리고 고루틴을 임의로 오랫동안 실행중인 상태로 두면 예기치 못한 메모리 사용으로 이어질 수 있다.

고루틴 수명이 분명하도록 동시성 코드를 단순하게 유지하자. 만약 그것이 어렵다면 고루틴이 언제 왜 종료되는지를 문서화하자.

12 Handle Errors[ | ]

https://golang.org/doc/effective_go.html#errors 를 참고하자. _ 변수를 사용하여 오류를 무시하지 말자. 함수가 오류를 반환하면 함수가 성공했는지 확인하자. 그 오류를 처리하고 반환하자. 정말 예외적인 경우에는 패닉일 수 있다.

13 Imports[ | ]

이름 충돌을 피하기 위한 경우를 제외하고는 import 이름을 바꾸지 말자. 좋은 패키지 이름은 이름을 바꿀 필요가 없다. 충돌이 발생하는 경우 로컬 또는 해당 프로젝트에 가장 가까운 import 이름을 바꾸는 것이 좋다.

import는 그룹으로 구성되며, 그 사이에는 빈 줄이 있다. 표준 라이브러리 패키지는 항상 첫번째 그룹에 있다.

package main

import (
	"fmt"
	"hash/adler32"
	"os"

	"appengine/foo"
	"appengine/user"

	"github.com/foo/bar"
	"rsc.io/goversion/version"
)

goimports가 이렇게 해줄 것이다.

14 Import Blank[ | ]

사이드 이펙트를 위해 import하는 패키지(import _ "pkg" 구문 사용)는 실제로 그 패키지가 필요한 프로그램 main 패키지나 테스트에서만 import해야 한다.

15 Import Dot[ | ]

import . 형식은 순환 종속성으로 인해 테스트 중인 패키지의 일부가 될 수 없는 테스트에서 유용할 수 있다.

package foo_test

import (
	"bar/testutil" // also imports "foo"
	. "foo"
)

이 경우 테스트 파일은 foo를 가져오는 bar/testutil을 사용하기 때문에 foo 패키지에 포함될 수 없다. 그래서 우리는 'import'를 사용한다. 파일이 foo 패키지의 일부가 아닌 것처럼 가장하도록 하는 형식이다. 이 경우를 제외하고는 import . 을 사용하지 말자. Quux같은 이름은 현재 패키지 또는 import한 패키지의 최상위 식별자인지 여부가 명확하지 않기 때문에 프로그램을 읽기가 훨씬 더 어렵다.

16 In-Band Errors[ | ]

C와 같은 언어에서 함수는 -1 또는 null과 같은 값을 반환하여 오류 또는 누락된 결과를 알리는 것이 일반적이다.

// Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string

// Failing to check for an in-band error value can lead to bugs:
Parse(Lookup(key))  // returns "parse failure for value" instead of "no value for key"

여러 반환 값에 대한 Go의 지원은 더 나은 솔루션을 제공한다. 클라이언트가 대역 내 오류 값을 확인하도록 요구하는 대신 함수는 다른 반환 값이 유효한지 여부를 나타내는 추가 값을 반환해야 한다. 이 반환 값은 오류이거나 설명이 필요하지 않은 경우 불리언일 수 있는데, 마지막 반환 값이어야 한다.

// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

이렇게 하면 호출자가 결과를 잘못 사용하는 것을 방지할 수 있다:

Parse(Lookup(key))  // compile-time error

더 견고하고 읽기 쉬운 코드를 권장한다:

value, ok := Lookup(key)
if !ok {
	return fmt.Errorf("no value for %q", key)
}
return Parse(value)

이 규칙은 export된 함수에 적용되지만 export되지 않은 함수에도 유용하다.

nil, "", 0, -1과 같은 반환 값이 함수에 대한 유효한 결과일 때, 즉 호출자가 다른 값과 다르게 처리할 필요가 없을 때는 괜찮다.

"strings" 패키지에 있는 것과 같은 일부 표준 라이브러리 함수는 대역 내 오류 값을 반환한다. 이것은 프로그래머의 좀더 많은 노력을 요구하는 대신 문자열 조작 코드를 크게 단순화시킨다. 일반적으로 Go 코드는 오류에 대한 추가 값을 반환해야 한다.

17 Indent Error Flow[ | ]

정상 코드 경로는 들여쓰기를 최소화하고, 오류 처리는 들여쓰기하여 먼저 처리하자. 이렇게 하면 정상 경로를 시각적으로 빠르게 훑어볼 수 있으므로 코드 가독성이 향상된다. 예를 들어 다음과 같이 쓰지 말자:

if err != nil {
	// error handling
} else {
	// normal code
}

대신, 다음과 같이 쓰자:

if err != nil {
	// error handling
	return // or continue, etc.
}
// normal code

다음과 같이 if 문에 초기화 문이 있는 경우라면:

if x, err := f(); err != nil {
	// error handling
	return
} else {
	// use x
}

짧은 변수 선언을 별도의 행으로 분리시켜야 된다:

x, err := f()
if err != nil {
	// error handling
	return
}
// use x

18 Initialisms[ | ]

이름 내의 단어가 두문자어(예: "URL" 또는 "NATO")라면 대소문자를 일관되게 써야 한다. 예를 들어 "URL"은 "URL" 또는 "url"("urlPony" 또는 "URLPony"처럼)로 써야 하며 "Url"로 써써는 안 된다. 예를 들어 ServeHttp가 아닌 ServeHTTP이다. 두문자어 단어 여러 개를 가진 식별자는 예를 들어 "xmlHTTPRequest" 또는 "XMLHTTPRequest"와 같이 쓴다.

이 규칙은 "identifier"("ego", "superego"에 관한 "id"가 아닌, 거의 모든 경우)의 약어인 "ID"에도 적용되므로 "appId" 대신 "appID"라고 쓰자.

프로토콜 버퍼 컴파일러에 의해 생성된 코드는 이 규칙에서 제외된다. 사람이 작성한 코드는 기계가 작성한 코드보다 더 높은 표준을 따른다.

19 Interfaces[ | ]

Go 인터페이스는 일반적으로 해당 값을 구현하는 패키지가 아니라, 인터페이스 타입의 값을 사용하는 패키지에 속한다. 구현 패키지는 구체적인(주로 포인터 또는 구조체) 타입을 반환해야 한다. 이렇게 하면 광범위한 리팩토링 없이 구현에 새 메소드를 추가할 수 있다.

"mocking용" API의 구현자 측에 인터페이스를 정의하지 말자. 대신 실제 구현의 public API를 사용하여 테스트할 수 있도록 API를 설계하자.

사용하기 전에 인터페이스를 정의하지 말자. 실제 사용 예시가 없으면, 인터페이스를 포함해야 하는 메소드가 무엇인지는 고사하고, 그 인터페이스가 필요한 것인지도 확인하기가 너무 어렵다.

package consumer  // consumer.go

type Thinger interface { Thing() bool }

func Foo(t Thinger) string {  }
package consumer // consumer_test.go

type fakeThinger struct{  }
func (t fakeThinger) Thing() bool {  }

if Foo(fakeThinger{}) == "x" {  }
// DO NOT DO IT!!!
package producer

type Thinger interface { Thing() bool }

type defaultThinger struct{  }
func (t defaultThinger) Thing() bool {  }

func NewThinger() Thinger { return defaultThinger{  } }

대신 구체적인 타입을 반환하고 소비자가 생산자 구현을 mock하도록 한다.

package producer

type Thinger struct{  }
func (t Thinger) Thing() bool {  }

func NewThinger() Thinger { return Thinger{  } }

20 Line Length[ | ]

Go 코드에는 엄격한 줄 길이 제한은 없지만 불편할 정도로 긴 줄은 피하자. 비슷하게, 예를 들어 반복적인 경우 가독성이 더 좋다면 줄을 짧게 유지하기 위해 줄바꿈을 추가하지 말자.

사람들이 "부자연스럽게" 줄바꿈하는 대부분의 경우(일부 예외가 있긴 하지만, 함수 호출이나 함수 선언 도중에), 적절한 수의 매개변수와 합리적으로 짧은 변수 이름을 쓴다면 줄바꿈이 필요하지 않다.

다시 말해, 행의 길이 때문이 아니라 (일반적으로) 작성하는 내용의 의미 때문에 줄을 나눈다. 이것 때문에 너무 긴 행을 만들어진다면 이름이나 의미를 바꿔 좋은 결과를 얻을 수도 있다.

실제로 이것은 함수의 길이에 대해서도 동일하다. "N행 이상의 함수를 만들자 말자"라는 규칙은 없지만, 너무 긴 함수과 너무 반복적인 작은 함수 같은 것에 대한 해결책은 함수 경계 부분을 바꾸는 것이지, 줄을 세는 것이 아니다.

21 Mixed Caps[ | ]

https://golang.org/doc/effective_go.html#mixed-caps를 참조하자. 이는 다른 언어의 규칙을 맞지 않는 경우에도 적용된다. 예를 들어 export되지 않은 상수는 MaxLength나 MAX_LENGTH가 아니라 maxLength이다.

또한 이니셜리즘을 참조하십시오.

22 Named Result Parameters[ | ]

godoc에서 어떻게 보이게 될지를 고려하자. 다음의 지명 결과 매개변수는:

func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}

godoc에서 반복적으로 보인다. 다음과 같이 하는 것이 더 낫다:

func (n *Node) Parent1() *Node {}
func (n *Node) Parent2() (*Node, error) {}

한편, 함수가 동일한 타입의 매개변수 2~3개를 반환하거나 컨텍스트에서 결과의 의미가 불분명한 경우, 일부 컨텍스트에서는 이름을 추가하는 것이 유용할 수 있다. 함수 내에서 var 선언을 피하려고 결과 매개변수의 이름을 지정하지는 말자. 불필요한 API 장황함을 대가로 작은 구현의 간결성을 얻는 것은 트레이드 오프이다.

func (f *Foo) Location() (float64, float64, error)

위의 것은 다음의 것보다 덜 명확하다:

// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

함수가 몇 줄뿐이면 네이키드 리턴도 괜찮다. 중간 크기의 함수가 되면 반환 값을 명시적으로 지정하자. 결론: 네이키드 리턴을 사용할 수 있다는 이유만으로 결과 매개변수의 이름을 지정하는 것은 가치가 없다. 문서의 명확성은 함수에서 한두 줄을 절약하는 것보다 항상 더 중요하다.

마지막으로, 어떤 경우에는 지연(deferred) 클로저에서 결과 매개변수를 변경하기 위해 결과 매개변수의 이름을 지정해야 한다. 그것은 항상 괜찮다.

23 Naked Returns[ | ]

인수가 없는 return 문은 지명 반환 값을 반환한다. 이를 "naked" 반환이라고 한다.

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

#Named Result Parameters를 참조하자.

24 Package Comments[ | ]

패키지 주석은, godoc이 제공하는 모든 주석과 마찬가지로, 빈 줄 없이 패키지 절에 가까이 붙어 있어야 한다.

// Package math provides basic constants and mathematical functions.
package math
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template

"package main" 주석의 경우, 바이너리 이름 뒤에 다른 스타일의 주석이 적합하다 (그리고 처음에 오는 경우 대문자로 표기할 수 있다). 예를 들어, seedgen 디렉토리에 있는 package main은 다음과 같이 쓸 수 있다:

// Binary seedgen ...
package main
// Command seedgen ...
package main
// Program seedgen ...
package main
// The seedgen command ...
package main
// The seedgen program ...
package main
// Seedgen ..
package main

위의 것들은 예시이며, 합리적인 변형은 가능하다.

패키지 주석은 공개적으로 보이는 것이고, 문장의 첫 단어를 대문자로 표시하는 것 등 적절한 영어로 써야 하므로, 소문자 단어로 문장을 시작하는 것은 허용되는 옵션이 아니다. 바이너리 이름이 첫 번째 단어라면, 명령줄 호출의 철자와 완전히 일치하지 않더라도 대문자로 써야 한다.

주석 규칙에 대한 자세한 내용은 https://golang.org/doc/effective_go.html#commentary 를 참조하자.

25 Package Names[ | ]

패키지에서 이름에 대한 모든 참조는 패키지 이름을 사용하여 이루어지므로 식별자에서 해당 이름을 생략할 수 있다. 예를 들어 chubby 패키지에서의 경우, ChubbyFile 타입이라고 하면 클라이언트에서는 chubby.ChubbyFile라고 써야 하는데 그럴 필요가 없다. 대신, 클라이언트가 chubby.File로 쓸 수 있는 File 타입이라는 이름으로 하면 된다. util, common, misc, api, types, interfaces 같은 의미 없는 패키지 이름은 쓰지 말자. 자세한 내용은 http://golang.org/doc/effective_go.html#package-nameshttp://blog.golang.org/package-names 를 참조하자.

26 Pass Values[ | ]

몇 바이트를 절약하겠다고 포인터를 함수 인수로 전달하지는 말자. 함수가 인수 x*x로만 참조하는 경우, 인수는 포인터가 아니어야 한다. 이에 대한 일반적인 예시로는 문자열에 대한 포인터(*string), 인터페이스 값에 대한 포인터(*io.Reader)를 전달하는 경우가 있다. 두 경우 모두 값 자체는 고정된 크기이며 직접 전달할 수 있다. 다만 이 조언은 큰 구조체나 커질 수 있는 작은 구조체에는 적용되지 않는다.

27 Receiver Names[ | ]

메소드 리시버의 이름은 해당 정체성을 반영해야 하는데, 흔히 해당 타입의 한두 글자 약어로 충분하다(예: "Client"의 경우 "c" 또는 "cl"). 객체지향 언어에서 메소드에 특별한 의미를 부여하는 데 흔히 사용되는 식별자인 "me", "this", "self"와 같은 이름은 사용하지 말자. Go에서 메소드의 리시버는 또 하나의 매개변수일 뿐이므로 그에 맞게 이름을 정해야 한다. 역할이 분명하고 문서화 목적으로 사용되지 않으므로 그 이름은 메소드 인수의 이름만큼이나 자세할 필요는 없다. 그 유형의 모든 메소드의 거의 모든 행에 나오므로 매우 짧을 수 있다. 친숙함은 간결성을 허용한다. 일관성도 유지하자: 한 메소드에서 수신자 "c"라고 부른다면, 다른 메소드에서는 그것을 "cl"이라고 부르지 말자.

28 Receiver Type[ | ]

메소드에 값 리시버를 쓸지 포인터 리시버를 쓸지를 선택하는 것은 새로 Go를 시작하는 프로그래머에게 어려울 수 있다. 확실하지 않은 경우에는 포인터 리시버를 사용하자. 일반적으로 변경되지 않는 작은 구조체 또는 기본 타입의 값과 같이, 효율성 차원에서 값 리시버가 의미가 있는 경우가 있다. 관련된 가이드라인은 다음과 같다:

  • 리시버가 map, func 또는 chan인 경우 포인터를 사용하지 말자. 리시버가 슬라이스이고 메소드가 슬라이스를 재슬라이스하거나 재할당하지 않는 경우 포인터를 사용하지 말자.
  • 메소드가 리시버를 변경해야 하는 경우, 리시버는 포인터여야 한다.
  • 리시버가 sync.Mutex 또는 이와 유사한 동기화 필드를 포함하는 구조체인 경우, 리시버는 복사를 피하기 위해 포인터여야 한다.
  • 리시버가 큰 구조체 또는 배열인 경우, 포인터 리시버가 더 효율적이다. 큰 것이라면 얼마나 큰 것을 말하나? 모든 요소를 ​​메소드에 대한 인수로 전달하는 것과 동일하다고 가정한다. 너무 크다고 생각되면 리시버에도 너무 큰 것이다.
  • 함수 또는 메소드가 동시에 또는 이 메소드에서 호출될 때 리시버를 변경할 수 있는가? 값 타입은 메소드가 호출될 때 리시버의 복사본을 생성하므로 외부 업데이트가 이 리시버에 적용되지 않는다. 변경사항을 원래 리시버에서 볼 수 있어야 하는 경우, 리시버는 포인터여야 한다.
  • 리시버가 구조체, 배열, 슬라이스이고 그 요소 중 하나라도 변경될 수 있는 것에 대한 포인터인 경우 포인터 리시버를 사용하면, 보는 사람에게 그 의도를 명확하게 전달할 수 있다.
  • 리시버가 가변 필드나 포인터가 없는 값 타입(예: time.Time 타입)인 작은 배열 또는 구조체이거나 int 또는 string과 같은 단순한 기본 타입인 경우 값 리시버는 의미가 있다. 값 리시버는 생성될 수 있는 가비지의 양을 줄일 수 있다. 값이 값 메소드에 전달되면 힙에 할당하는 대신 스택 복사본을 사용할 수 있다(물론 컴파일러는 이러한 할당을 회피하기 위해 스마트하게 처리하려고 하지만 항상 성공적이지는 않다). 분석을 해보지 않은 채, 값 리시버 타입을 선택하지 말자.
  • 리시버 타입을 섞지 말자. 사용 가능한 모든 메소드에 대해 포인터 또는 구조체 타입을 선택한다.
  • 마지막으로, 확신이 없다면 포인터 리시버를 사용하자.

29 Synchronous Functions[ | ]

동기 함수(결과를 직접 반환하거나 반환하기 전에 콜백 또는 채널 작업을 완료하는 함수)를 비동기 함수보다 선호한다. 동기 함수는 호출 내에서 고루틴을 지역화하여 수명을 더 쉽게 추론하고 누수나 데이터 경합을 방지한다. 또 테스트하기도 더 쉽다. 호출한 쪽에서는 폴링이나 동기화 없이 입력을 전달하고 출력을 확인할 수 있다.

호출한 쪽에서 더 많은 동시성이 필요한 경우, 별도의 고루틴에서 함수를 호출함으로써 쉽게 추가할 수 있다. 그러나 호출한 쪽에서 불필요한 동시성을 제거하는 것은 매우 어렵다(때로는 불가능하다).

30 Useful Test Failures[ | ]

테스트는, 무엇이 잘못되었는지, 입력이 무엇인지, 실제로 받은 것, 기대한 것은 무엇이었는지 알려주는, 유용한 메시지와 함께 실패해야 한다. assertFoo 헬퍼를 많이 작성하고 싶을텐데, 헬퍼가 유용한 오류 메시지를 생성하는지 확인해보자. 실패한 테스트를 디버깅하는 사람이 당신도 아니고 당신의 팀도 아니라고 가정하자. 전형적인 Go 테스트는 다음과 같이 실패한다:

if got != tt.want {
	t.Errorf("Foo(%q) = %d; want %d", tt.in, got, tt.want) // 이 지점을 지나면 더 이상 아무것도 테스트 할 수 없는 경우에는 Fatalf 
}

여기서 순서는 actual != expected 이며, 메시지에서도 그렇게 한다. 일부 테스트 프레임워크는 0 != x, "expected 0, got x"와 같이 거꾸로 작성하도록 권장한다. Go에서는 그렇게 하지 않는다.

타이핑 수가 많은 것 같으면 테이블 기반 테스트를 작성하는 것도 좋다.

다른 입력으로 테스트 헬퍼를 사용할 때 실패한 테스트를 명확하게 하는 또 하나의 일반 테크닉은 각 호출하는 곳을 다른 TestFoo 함수로 래핑하여 그 이름으로 실패하도록 하는 것이다:

func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T)    { testHelper(t, []int{}) }

어느 경우든, 나중에 디버깅하는 사람에게 도움이 되는 메시지를 남기면서 실패하도록 할 책임은 당신에게 있다.

31 Variable Names[ | ]

Go의 변수 이름은 길지 않고 짧아야 한다. 한정된 스코프를 가진 지역 변수의 경우에 더 그렇다. lineCount보다 c가 낫고, sliceIndex보다 i가 낫다.

기본 규칙: 이름이 사용되는 곳이 선언에서 멀어지면 더 자세해야 한다. 메소드 리시버의 경우, 1~2개의 문자로 충분하다. 루프 인덱스나 reader와 같은 일반적인 변수명은 1개의 문자(i, r)일 수도 있다. 하지만 일반적이지 않은 것이나 전역변수는 더 자세한 이름이 필요하다.

32 같이 보기[ | ]

33 참고[ | ]

문서 댓글 ({{ doc_comments.length }})
{{ comment.name }} {{ comment.created | snstime }}