본문 바로가기
golang

Go google style guides - decisions

by marble25 2023. 12. 9.

이전에 했던 style guide보다는 좀 더 서술적이고 예시가 많은 문서이다.

참고 문서: https://google.github.io/styleguide/go/decisions

Naming

  • Underscore를 피하자. 다음 케이스만 예외로 둔다.
    • *_test.go 안의 Test, Benchmark and Example function
    • OS와 직접 통신하는 system library
  • util, utility, common, helper와 같은 무의미한 패키지 이름을 피하자.
  • Receiver 이름은 짧고, 축약어를 많이 사용하라.
    • func (tray Tray) → func (t Tray)
    • func (info *ResearchInfo) → func (ri *ResearchInfo)
  • 상수 이름은 다른 이름들처럼 MixedCaps로 하자.
  • // Good: const MaxPacketSize = 512 const ( ExecuteBit = 1 << iota WriteBit ReadBit )
  • 축약어는 모두 대문자로 사용하자.
    • ex: XMLAPI, IOS(public), ios(private), GRPC(public), gRPC(private)
  • Getter에는 get prefix를 사용하지 말자.
    • ex) GetCounts 대신 Counts
      • 더 복잡한 computation이 필요한 함수라면 Compute나 Fetch같은 다른 단어를 사용하자.
  • 변수 이름
    • 변수 이름 길이는 사용되는 scope에 비례해야 한다는 일반적인 규칙이 있다.
      • A small scope is one in which one or two small operations are performed, say 1-7 lines.
      • A medium scope is a few small or one large operation, say 8-15 lines.
      • A large scope is one or a few large operations, say 15-25 lines.
      • A very large scope is anything that spans more than a page (say, more than 25 lines).
    • 일반적으로:
      • count나 option같은 single word는 좋은 시작점이다.
      • 모호함을 줄이기 위해 단어가 추가될 수 있다. ex) userCount, projectCount
      • Typing을 줄이기 위해 글자를 빼지 말자. ex) Sbx보다는 Sandbox가 낫다.
      • Type과 Type을 내포한 단어를 사용하지 말자.
        • userCount가 numUsers나 usersInt보다 낫다.
        • users가 userSlice보다 낫다.
        • 한 scope에 값에 대한 2개의 버전이 있다면 type-like qualifier도 가능하다. ex) ageString을 입력으로 받고, age를 parse된 값으로 가질 수 있다.
        • 컨텍스트 내에서 의미가 명확하면 단어를 빼라. UserCount 메소드에서, userCount 로컬 변수보다는 count, users, 또는 c가 낫다.
    • 한 글자 변수
      • Method receiver에서는 한두 글자 이름이 선호된다.
      • 친숙한 이름은 괜찮다.
        • io.Reader나 *http.Request 에 r
      • Loop에서 indices(i)나 coordinates(x, y)를 가리키는 경우
      • Loop 안에서만 사용되는 변수 ex) for _, n := range nodes { ... }
  • 반복
    • context를 통해 알 수 있는 정보는 최대한 줄이자.
    • // Bad: // In package "ads/targeting/revenue/reporting" type AdsTargetingRevenueReport struct{} func (p *Project) ProjectName() string // Good: // In package "ads/targeting/revenue/reporting" type Report struct{} func (p *Project) Name() string

Commentary

  • 좁은 화면에서도 보일 수 있을 정도로 작성하자. 60~70자 정도로 줄이고, punctuation 단위로 line break를 하자.
  • Top level 메소드나 변수들은 doc comment가 있어야 한다. 이 comment는 완전한 문장으로 이루어져야 한다.
  • Package는 의도된 사용법을 문서화해야 한다. Runnable example을 제공해보자.
  • Named result parameter는 같은 타입이 여러 개 있는 등 꼭 필요한 경우에만 사용하자.
  • // Good: func (n *Node) Parent1() *Node func (n *Node) Parent2() (*Node, error) // Good: func (n *Node) Children() (left, right *Node, err error) // Bad: func (n *Node) Parent1() (node *Node) func (n *Node) Parent2() (node *Node, err error)
  • Naked return(named variable이 존재할 때, return에 argument를 포함하지 않는 것)는 작은 함수에서만 하자. 규모가 커지면 명확성을 잃을 수 있기 때문이다.
  • func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return }

Imports

  • Import는 name collision을 피하기 위해서만 rename되어야 한다. 단, generated protocol buffer packages는 underscore를 피하기 위해 rename될 수 있다.
  • // Good: import ( fspb "path/to/package/foo_service_go_proto" ) // Good: import ( core "github.com/kubernetes/api/core/v1" meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1" )
  • 만약 common local name과 충돌하는 package를 import하고 싶다면, pkg suffix를 붙여보자. ex) urlpkg
  • Import는 2개의 그룹으로 구성되어야 한다.
    • Standard library packages
    • Other (project and vendored) packages
    // Good:
    package main
    
    import (
        "fmt"
        "hash/adler32"
        "os"
    
        "github.com/dsnet/compress/flate"
        "golang.org/x/text/encoding"
        "google.golang.org/protobuf/proto"
        foopb "myproj/foo/proto/proto"
        _ "myproj/rpc/protocols/dial"
        _ "myproj/security/auth/authhooks"
    )
    
  • Blank import(import _)는 main package에서만 사용하자. Dependency control을 어렵게 한다.
  • Dot import(import .)는 사용하지 말자.

Errors

  • 함수가 fail할 수 있음을 error를 통해 나타내자. 컨벤션은, error를 result parameter의 마지막에 넣는 것이다.
  • Error string은 capitalize하지 말고, punctuation으로 끝나서는 안된다. 다른 error와 concat 될 수 있기 때문이다.
  • // Bad: err := fmt.Errorf("Something bad happened.") // Good: err := fmt.Errorf("something bad happened")
  • Error를 리턴하는 함수에서 error를 _로 처리하는 것은 일반적으로 좋지 않다. 3가지 중 하나를 하자:
    • 에러를 적절히 처리
    • 호출자에게 다시 error 리턴
    • 예외적인 상황에서만) log.Fatal이나 panic 호출
  • Error는 다른 코드를 처리하기 전에 처리하자.
  • // Good: if err != nil { // error handling return // or continue, etc. } // normal code // Bad: if err != nil { // error handling } else { // normal code that looks abnormal due to indentation }

Language

  • Nil slice
    • 대부분의 경우에는 nil과 empty slice간에 차이는 없다. Empty slice를 선언할 때에는 nil initialization을 하자.
    • // Good: var t []string // Bad: t := []string{}
    • Interface 작성시에는 nil slice와 zero-length slice 간에 차이를 만드는 것을 피하자.
    • // Good: // describeInts describes s with the given prefix, unless s is empty. func describeInts(prefix string, s []int) { if len(s) == 0 { return } fmt.Println(prefix, s) } // Bad: func maybeInts() []int { /* ... */ } // describeInts describes s with the given prefix; pass nil to skip completely. func describeInts(prefix string, s []int) { // The behavior of this function unintentionally changes depending on what // maybeInts() returns in 'empty' cases (nil or []int{}). if s == nil { return } fmt.Println(prefix, s) } describeInts("Here are some ints:", maybeInts())
  • Function argument가 너무 많으면 structure로 묶자.
  • // Good: good := foo.Call(long, CallOptions{ Names: list, Of: of, The: parameters, Func: all, Args: on, Now: separate, Visible: lines, }) // Bad: bad := foo.Call( long, list, of, parameters, all, on, separate, lines, )
  • Function과 Method 호출은 line break가 없어야 한다.
  • // Good: good := foo.Call(long, list, of, parameters, all, on, one, line) // Bad: bad := foo.Call(long, list, of, parameters, with, arbitrary, line, breaks)
  • If 문도 line break가 없어야 한다. 너무 길어진다면, 조건문을 변수화하자.
  • // Bad: // The second if statement is aligned with the code within the if block, causing // indentation confusion. if db.CurrentStatusIs(db.InTransaction) && db.ValuesEqual(db.TransactionKey(), row.Key()) { return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) } // Good: inTransaction := db.CurrentStatusIs(db.InTransaction) keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key()) if inTransaction && keysMatch { return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) }
  • Yoda style 조건문을 지양하라.
  • // Bad: if "foo" == result { // ... } // Good: if result == "foo" { // ... }
  • Struct를 복사할 때에는 주의하라. 예를 들어, Buffer를 복사하면 복사본의 slice는 원본의 alias이므로, 예상치 못한 결과를 일으킬 수 있다.
  • Don’t Panic! Panic 사용을 지양해라.
  • Goroutine을 만들때, 언제 종료될지 명확히 해라. Close된 channel로 데이터를 보내는 것은 panic을 발생시킨다. 이는 Context를 이용해서 제어될 수 있다.
  • // Good: func (w *Worker) Run(ctx context.Context) error { // ... for item := range w.q { // process returns at latest when the context is cancelled. go process(ctx, item) } // ... } // Bad: func (w *Worker) Run() { // ... for item := range w.q { // process returns when it finishes, if ever, possibly not cleanly // handling a state transition or termination of the Go program itself. go process(item) } // ... }
  • 몇 바이트를 줄이기 위해 pointer value 를 함수로 넘기지 말자. 단, large struct같은 경우에는 적용되지 않는다.
  • Receiver Type
    • Receiver가 slice면 value를 사용하라.
    • // Good: type Buffer []byte func (b Buffer) Len() int { return len(b) }
    • Receiver를 수정하면 pointer를 사용하라.
    • // Good: type Counter int func (c *Counter) Inc() { *c++ }
    • Receiver가 안전하게 복사될 수 없는 field를 포함하고 있다면, pointer를 사용하라. ex) mutex
    • Receiver가 “large” struct나 array이면 pointer를 사용하라.
    • Receiver가 struct나 array이고 element가 변경된다면 pointer를 사용하라. 독자에게 명확하게 의도를 전달할 수 있다.
    • Receiver가 integer나 string처럼 built-in type이면 수정될 수 없으므로 value를 사용하라.
    • // Good: type User string func (u User) String() { return string(u) }
    • Receiver가 map, function, channel이면 value를 사용하라.
    • 어떤 것을 사용할지 잘 모르겠으면 pointer를 사용하라.
  • Switch문에서 break를 사용하지 마라.
  • Async function보다 Sync function을 선호해야 한다.
  • Type alias(type T1 = T2)를 새 소스코드로 migrate하는 경우가 아니라면 지양해라.
  • %q를 애용하자. String을 쌍따옴표 사이에 출력해준다.
  • // Good: fmt.Printf("value %q looks like English text", someText)
  • Go 1.18부터 any 가 추가되었다. interface{} 보다 any를 선호해야 한다.

Common libraries

  • Function이나 method를 호출할 때, context.Context는 언제나 첫 파라미터여야 한다.예외는:
    • HTTP handler에서는 req.Context()를 사용한다.
    • Streaming RPC method는 stream의 context를 사용한다.
    • Entrypoint function에서는 context.Background()를 사용한다.
      • ex) main, init,
  • func F(ctx context.Context /* other arguments */) {}
  • Custom context를 사용하지 마라.
  • Key 생성시 math/rand 대신 crypto/rand 를 사용하라. math/rand 는 predictable하다.
  • Slice같은 복잡한 자료구조를 비교할 때에는, cmp.Equal을 사용하자. cmp.Diff는 object간 차이를 human readable format으로 얻을 수 있다.

'golang' 카테고리의 다른 글

[TIL] golang promauto metric register시 panic  (0) 2023.12.21
[TIL] golang buffer 복사  (1) 2023.12.21
Go google style guide - overview & guide  (1) 2023.11.19
Go garbage collection  (1) 2023.10.30
[TIL] Go buffer read  (0) 2023.10.27