LoginSignup
20
10

More than 1 year has passed since last update.

Google Go Style Guideを読んでみよう! - Style Decisions編

Last updated at Posted at 2022-12-18

はじめに

みなさん、こんにちは!
Go大好きエンジニアのくろたくです。

ちょっと前になりますが、GoogleからGoのスタイルガイドが発表されましたね🙌

今更ながら、アドカレの機会を利用してスタイルガイドを読んでアウトプットしようと考えました。

とはいえ、スタイルガイドの内容全てを読んで一気にアウトプットするとなると相当な時間と労力がかかります。そこで、この記事は1度アウトプットしたら終わりというものにするのではなく、書き足していくこと前提で書こうと決めました。
アップデートを見逃さないように、LGTM、ストックしておくことをオススメします👍

Go Style Guildeを読んでみようかなと思ってもらえるきっかけになれたら嬉しいです!
それでは、CyberAgent AI tech studio | Go Advent Calendar 2022の18日目の記事スタートです🎉

自分に合ったスタイルガイドを見つけよう!

Go Style Guideのトップページには、3種類の文書が存在することと、それぞれの文書にどんなことが書かれているかがまとめられています。

文書 対象読者 書かれている内容
Style Guide 全ての人 スタイルの原則(明瞭性、単純性、簡潔性、保守性、一貫性)について。それぞれの説明と事例を交えた深堀り。
Style Decisions Goのレビュワーとなるメンター(Readability Mentors) スタイルの原則を達成するためにGoにおいて意識するべきコードの書き方をまとめたもの。
Best Practices 興味のある人 Goで開発する際に出くわすことの多いコード例を取り上げ、各ケースごとにどのようにスタイルガイドを適用させていくべきか書かれている。

※この記事では、2番目のStyle Decisionsの内容をまとめようと思っています。
※comming soon...となっているセクションについては追記されていく想定です。
※優先順位については筆者の独断と偏見で、重要だと思ったところから順に書いています。
※このセクション優先的に書いてほしい!という要望があればコメント欄やTwitterにてお知らせください。

Naming

Underscores - アンダースコア(_)について

下記の例外を除いては基本的に使ってはだめ。

  1. 自動生成されたコードのパッケージ名
  2. テストコード(*_test.go)内の関数名
  3. OSやcgoを制御するような低レイヤーライブラリにおける、識別子を再利用する場合

Package names - package名について

小文字のアルファベットのみで構成されている必要がある。
例:
tabwriter → ⭕️
tabWriter → ❌
TabWriter → ❌
tab_writer → ❌

よく使われるローカル変数名は避ける。
例:
countusercount

情報量の多いパッケージ名は避ける。
例:
util, utility, common, helper

Best practicesドキュメントより参照したutilパッケージの例

// Good:
db := spannertest.NewDatabaseFromFile(...)
// Bad:
db := test.NewDatabaseFromFile(...)

Reciever names - Receiver名について

Receiver名は、

  • 短く(だいたい1〜2文字くらいの長さが目安)
  • 型名の略称
  • レシーバーごとに名前が違うのはNG

であるべき。

悪い例 良い例
func (tray Tray) func (t Tray)
func (info *ResearchInfo) func (ri *ResearchInfo)
func (this *ReportWriter) func (w *ReportWriter)
func (self *Scanner) func (s *Scanner)

Constant names - 定数名について

定数もGoの基本原則とされているMixedCapsに従って、キャメルケースで書く。

大文字から始めるか小文字から始めるかで他のパッケージから使われるグローバル定数か、パッケージ内のみで利用できるローカル変数かを区別する。

// Good:
const MaxPacketSize = 512

const (
    ExecuteBit = 1 << iota
    WriteBit
    ReadBit
)
// Bad:
const MAX_PACKET_SIZE = 512
const kMaxBufferSize = 1024
const KMaxUsersPergroup = 500

定数には、値自体ではなく、役割に基づいて名前をつけること。
もしも、定義しようとしている定数がその値とは別の意味や役割を持たないのであれば、定数として定義する必要はない。

// Bad:
const Twelve = 12

const (
    UserNameColumn = "username"
    GroupColumn    = "group"
)

Initialisms - 頭字語(URLID)について

URLID などのように、各単語の頭文字からなる略である場合は、それらを同じケース(大文字なら全て大文字)で表す必要がある。

例:
URLUrl
appIDappId

下記を参考にするとわかりやすいと思います。

Initialism(s) Scope Correct Incorrect
XML API Exported XMLAPI XmlApi, XMLApi, XmlAPI, XMLapi
XML API Unexported xmlAPI xmlapi, xmlApi
iOS Exported IOS Ios, IoS
iOS Unexported iOS ios
gRPC Exported GRPC Grpc
gRPC Unexported gRPC grpc
DDoS Exported DDoS DDOS, Ddos
DDoS Unexported ddos dDoS, dDOS

Getters

関数名に基本概念としてGetという単語が含まれていない限りGetを付与するべきでない。
※基本概念としてGetという単語が含まれている例: HTTP GET

// Bad
func GetCounts() int

// Good
func Counts() int

関数が複雑な計算の実行やリモートコールを伴う場合は、Getの代わりにComputeFetchなどの別の単語を使用する。目的としては、関数呼び出しに時間がかかり、ブロックや失敗する可能性があることを関数の使用者に伝える目的がある。

Variable names - 変数名について

変数名の長さは、対象とするスコープの大きさに比例し、そのスコープ内で使用される回数に反比例するはず。
つまり、スコープが広くなるほど、変数名は長くなり、使用頻度が低ければ変数名は短くなるということ。

ファイルスコープで宣言された変数は複数の単語を必要とするかもしれないが、単一のインナーブロックにスコープされた変数はコードを明確に保ち、余計な情報を省略するために1単語または、1~2文字で済むかもしれない。

▼ ファイルスコープの例

user.go
var (
    Users []*User = {
        {...}
    }
)

▼ 単一のインナーブロックスコープの例

for i range users {
    fmt.Pringln(users[i])
}

スコープの範囲について、目安は下記。

  • 小範囲:1~7行程度の小さな操作を1~2回行うもの
  • 中程度のスコープ:8~15行程度の小さな操作を数回、または大きな操作を1回行うもの
  • 大きなスコープ:1つまたはいくつかの大きな操作が行われるもので、例えば15~25行のもの
  • 非常に大きな範囲:1ページ以上にまたがるもの(例えば25行以上)

他の判断軸として、概念の具体性がある。
例えば、使用するデータベースが1つだけだと仮定すると、dbのような短い変数名は、スコープが非常に大きい場合でも明確なままである可能性が高い。
このように、変数名が短くとも、その変数が表す概念が一意に定まる場合であれば短い変数名も許容される。

ローカル変数の名前は、その値がどこで作られた火よりも、その変数が何を含んでいて、現在の文脈でどのように使われているかを反映するべき。という前提を起きつつ、下記に一般的なアプローチを示す。

  • 一語の変数名はファーストチョイスとして良い場合が多い(例:count, options
  • 似たような変数の違いについて、単語を追加することで曖昧さをなくすことができる(例:userCount, prodjectCount
  • 短い変数名が良いからといって、単純に文字を削除して略語化することは避けること。(NG:Sandboxsbx
  • 変数名には型に関する情報を省くこと
    • numUsers,usersInt → ⭕️ userCount
    • userSlice → ⭕️ users
    • 例外として、入力値としてはstring型で受け取ったageをintへパースする場合などについては、inputの変数名としてageStringなどを使うことは許容される
  • 周囲の文脈から明らかな単語は省略する。
    • 例えば、UserCountメソッドの実装では、userCountというローカル変数はおそらく冗長である。

1文字の変数名について、下記のような場合に使うと良い。

  • メソッドのレシーバー変数には、1文字または2文字の名前を付けるのが好ましい。
  • 一般的な型のために馴染みのある変数名を使う場合
    • rio.Reader*http.Request を表します。
    • wio.Writer または http.ResponseWriter です。
  • 整数型のループ変数では、特にインデックス(iなど)や座標(xやyなど)には一文字の識別子を使うことができる
  • スコープが短い場合は、for _, n := range nodes { ... }のように省略形もループ識別子として使用できる。

Repetition - 変数の冗長化に対する対策

変数名が長くなりすぎないように、不必要に文脈や型名を繰り返すのは避けましょう。

Package vs. exported symbol name

  • widget.NewWidget → ⭕️ widget.New
  • widget.NewWidgetWithName → ⭕️ widget.NewWithName
  • db.LoadFromDatabase → ⭕️ db.Load
  • goatteleportutil.CountGoatsTeleported → ⭕️ gtutil.CountGoatsTeleported or goatteleport.Count
  • myteampb.MyTeamMethodRequest → ⭕️ mtpb.MyTeamMethodRequest or myteampb.MethodRequest

Variable name vs. type

  • var numUsers int → ⭕️ var users int
  • var nameString int → ⭕️ var name string
  • var primaryProject int → ⭕️ var primary *Project

パースすることが前提になっているユーザーからの入力値などの変数名では型名を変数名に入れて良いこととする。

// Good
limitStr := r.FormValue("limit")
limit, err := strconv.Atoi(limitStr)

External context vs. local names

名前に周囲の文脈の情報を含めると、多くの場合余計なノイズになる。
パッケージ名、メソッド名、型名、関数名、インポートパス、そしてファイル名さえも変数名からは省略するべきでしょう。

▼ In package "ads/targeting/revenue/reporting"

// Bad:
type AdsTargetingRevenueReport struct{}

func (p *Project) ProjectName() string
// Good:
type Report struct{}

func (p *Project) Name() string

▼ In package "sqldb"

// Bad:
type DBConnection struct{}
// Good:
type Connection struct{}

▼ In package "ads/targeting/revenue/reporting"

// Bad:
func Process(in *pb.FooProto) *Report {
    adsTargetingID := in.GetAdsTargetingID()
}
// Good:
func Process(in *pb.FooProto) *Report {
    id := in.GetAdsTargetingID()
}

文脈や用法から明らかな名称についてもできるだけ省略するようにしよう。

// Bad:
func (db *DB) UserCount() (userCount int, err error) {
    var userCountInt64 int64
    if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil {
        return 0, fmt.Errorf("failed to load user count: %s", dbLoadError)
    }
    userCount = int(userCountInt64)
    return userCount, nil
}
// Good:
func (db *DB) UserCount() (int, error) {
    var count int64
    if err := db.Load("count(distinct users)", &count); err != nil {
        return 0, fmt.Errorf("failed to load user count: %s", err)
    }
    return int(count), nil
}

Commentary

Comment line length - コメントの文字数について

comming soon...

Doc comments - ドキュメント用コメントについて

comming soon...

Comment sentences - コメント文について

comming soon...

Examples - 例

comming soon...

Named result parameters - 名前付き結果パラメータについて

comming soon...

Package comments - パッケージの説明文について

comming soon...

Imports

Import renaming - インポートのエイリアス化について

comming soon...

Import grouping - インポートのグループ化について

comming soon...

Import "blank"(import _) - blankインポートについて

comming soon...

Import "dot"(import .) - dotインポートについて

comming soon...

Errors

Returning errors - エラーのreturnについて

関数が失敗する可能性を含んでいる場合、返り値にerrorを指定する。
慣習上、errorは最後の返り値に指定されることが多いので、それに従う。

// Good:
func Good() error { /* ... */ }

errorの返り値としてnilを返すことは、失敗する可能性のある処理に成功したことを知らせる一般的な方法である。
もしエラーが発生してerr != nilの状態になった場合は、呼び出し側でエラー以外の戻り値を未指定として扱う必要がある。

// Good:
func GoodLookup() (*Result, error) {
    res, err := doSomeThing()
    if err != nil {
        return nil, err
    }
    return res, nil
}

エラーを返す関数は、基本的にerror型を利用するようにしましょう。

// Bad:
func Bad() *os.PathError { /*...*/ }

Error strings - エラー文言について

エラー文言は、固有名詞や頭字語で始まらない限りは大文字から始めてはいけない。
また、文章のように句読点で終わらせてはいけない。

// Bad:
err := fmt.Errorf("Something bad happened.")
// Good:
err := fmt.Errorf("something bad happened")

一方、エラーメッセージ(ロギング、テスト失敗、APIレスポンス、その他のUI上で表示されるメッセージなど)では、大文字から始まる必要がある。

// Good:
log.Infof("Operation aborted: %v", err)
log.Errorf("Operation aborted: %v", err)
t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err)

Handle errors - エラーハンドリングについて

エラーに遭遇したコードは、エラーをどのように処理するか意図的に選択する必要がある。
通常、_変数を利用してエラーを無視することは得策とは言えない。
関数がエラーを返した場合は、以下のいずれかを実行する必要がある。

  • エラーを直ちに処理して対処する。
  • エラーを呼び出し元に返す。
  • 例外的な状況においては、log.Fatalまたはpanicを呼び出す。

ライブラリによりエラーが起きることがないことを保証されている場合( (*bytes.Buffer).Writeなど)の、エラーを無視できる稀な状況では、付随するコメントでなぜそれが安全なのかを説明する必要がある。

// Good:
var b *bytes.Buffer

n, _ := b.Write(p) // never returns a non-nil error

In-band errors - OSにおけるexit codeについて

comming soon...

Indent error flow - エラー処理と正常処理の書き方について

関数を呼び出した後は、必ずエラー処理をしてからコードを進めるようにすること。
これにより、読み手が正常なパスを素早く見つける手助けになる。

エラーが起きない場合の正常なパスで実行されるコードは、ifブロックの後に表示されるべきで、else節のなかでインデントされるべきではない。

// 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
}

関数の返り値を後の処理でも利用したい場合は、if文のブロックスコープに変数を閉じ込めてしまう書き方は避けるべきでしょう。

// Good:
x, err := f()
if err != nil {
  // error handling
  return
}
// lots of code that uses x
// across multiple lines
// Bad:
if x, err := f(); err != nil {
  // error handling
  return
} else {
  // lots of code that uses x
  // across multiple lines
}

Language

Literal formatting - リテラル構文について

Goには強力な複合リテラル構文があり、深くネストした複雑な値を1つの式で表現することができる。
可能な限りフィールドごとに値を構築するのではなく、リテラル構文を使用しましょう。
リテラルをより読みやすくするための追加ルールを下記に示す。

Field names

構造体への代入をする時は、フィールド名を指定して代入しよう。

// Good:
good := otherpkg.Type{A: 42}
// Bad:
// https://pkg.go.dev/encoding/csv#Reader
r := csv.Reader{',', '#', 4, false, false, false, false}

Matcing braces

構造体の括弧{}のインデントは揃えよう。

// Good:
good := []*Type{{Key: "value"}}
// Good:
good := []*Type{
    {Key: "multi"},
    {Key: "line"},
}
// Bad:
bad := []*Type{
    {Key: "multi"},
    {Key: "line"}}
// Bad:
bad := []*Type{
    {
        Key: "value"},
}

Cuddled braces

スライスや配列の構造体で中括弧の間の括弧を削除する場合は下記の場合のみに限る。

  • インデントが揃っている場合
  • 内部の値(フィールド)もリテラルやプロトビルダーであること(変数や他の式ではないこと)
// Good:
good := []*Type{
    { // Not cuddled
        Field: "value",
    },
    {
        Field: "value",
    },
}
// Good:
good := []*Type{{ // Cuddled correctly
    Field: "value",
}, {
    Field: "value",
}}
// Good:
good := []*Type{
    first, // Can't be cuddled
    {Field: "second"},
}
// Good:
okay := []*pb.Type{pb.Type_builder{
    Field: "first", // Proto Builders may be cuddled to save vertical space
}.Build(), pb.Type_builder{
    Field: "second",
}.Build()}
// Bad:
bad := []*Type{
    first,
    {
        Field: "second",
    }}

Repeated type names

スライスやマップの構造体定義において、繰り返される型名宣言は省略しよう。
型名を明示的に示した方が良い場合は、プロジェクトでは一般的ではない複雑な型を扱うときや、繰り返される型名が離れた行にあり、読み手に文脈を思い出させる必要がある時のみ。

// Good:
good := []*Type{
    {A: 42},
    {A: 43},
}
// Bad:
repetitive := []*Type{
    &Type{A: 42},
    &Type{A: 43},
}
// Good:
good := map[Type1]*Type2{
    {A: 1}: {B: 2},
    {A: 3}: {B: 4},
}
// Bad:
repetitive := map[Type1]*Type2{
    Type1{A: 1}: &Type2{B: 2},
    Type1{A: 3}: &Type2{B: 4},
}

💡Tips: 上記のルールに従って、繰り返される型名を削除したい場合は、gofmt -sを実行すると良いでしょう。

Zero-value fields

ゼロ値を代入したいフィールドは、構造体リテラルの明確性が損なわれない場合においては省略することができる。
ゼロ値フィールドの省略により、代入されるフィールドのみに注目が集まるようになる。

// Bad:
import (
  "github.com/golang/leveldb"
  "github.com/golang/leveldb/db"
)

ldb := leveldb.Open("/my/table", &db.Options{
    BlockSize: 1<<16,
    ErrorIfDBExists: true,

    // These fields all have their zero values.
    BlockRestartInterval: 0,
    Comparer: nil,
    Compression: nil,
    FileSystem: nil,
    FilterPolicy: nil,
    MaxOpenFiles: 0,
    WriteBufferSize: 0,
    VerifyChecksums: false,
})
// Good:
import (
  "github.com/golang/leveldb"
  "github.com/golang/leveldb/db"
)

ldb := leveldb.Open("/my/table", &db.Options{
    BlockSize: 1<<16,
    ErrorIfDBExists: true,
})

Nil slices - nil値を返すsliceについて

nilと空のスライスの間に機能的な違いは存在しない。
lencapのような組み込み関数はnilスライスでも期待通りに動作する。

// Good:
import "fmt"

var s []int         // nil

fmt.Println(s)      // []
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
for range s {...}   // no-operation

s = append(s, 42)
fmt.Println(s)      // [42]

空のスライスをローカル変数として宣言する場合(特に、戻り値になる予定の場合)は、呼び出し側によるバグのリスクを減らすためにnilの初期化を優先するようにすること。

// Good:
var t []string
// Bad:
t := []string{}

nilなのか、空のスライスなのかをクライアントに判別させるようなAPIは実装しないこと。

// Good:
// Ping pings its targets.
// Returns hosts that successfully responded.
func Ping(hosts []string) ([]string, error) { ... }
// Bad:
// Ping pings its targets and returns a list of hosts
// that successfully responded. Can be empty if the input was empty.
// nil signifies that a system error occurred.
func Ping(hosts []string) []string { ... }

スライスのチェックには、len()を使って比較すること。
空っぽかどうかを== nilでチェックすることは避けましょう。

// 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())

【FYI】
なぜ、空スライスかどうかのチェックを== nilで行ってはいけないかについては、下記の記事が参考になると思います。
[Go]なぜsliceの空チェックで「nil」ではなく「長さ」でチェックするのか

Indentation confusion - インデントが招く混乱について

ifforなどのコードブロック内の記述と条件式などの行が同一にならないようにしよう。

// Bad:
if longCondition1 && longCondition2 &&
    // Conditions 3 and 4 have the same indentation as the code within the if.
    longCondition3 && longCondition4 {
    log.Info("all conditions met")
}

Function formatting - 関数のフォーマットについて

関数についても、Indentation confusionで述べたように、引数が長い場合であっても1行で記述した方が良いでしょう。
インデントが変わることにより、ブロック内の処理と区別することが難しくなる。

// Bad:
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
    foo4, foo5, foo6 int) {
    foo7 := bar(foo1)
    // ...
}

大量の引数を関数に指定したい場合に使える、引数を短くするテクニックは、Best practicesを参考にすると良いでしょう。
以下、Best practicesより引用した2つの回避策

Option structure
引数を1つの構造体にまとめてしまえば良いよね!という発想。

// Bad:
func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) {
    // ...
}
// Good:
type ReplicationOptions struct {
    Config              *replicator.Config
    PrimaryRegions      []string
    ReadonlyRegions     []string
    ReplicateExisting   bool
    OverwritePolicies   bool
    ReplicationInterval time.Duration
    CopyWorkers         int
    HealthWatcher       health.Watcher
}

func EnableReplication(ctx context.Context, opts ReplicationOptions) {
    // ...
}

Variadic options
いわゆる可変長引数の実装。
注意書きでも書かれているが、コードの記述量は増えてしまうため、可変長引数を導入するメリットがオーバーヘッドを上回る場合のみ効果的とされている。

// Bad:
func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) {
  ...
}
// Good:
type replicationOptions struct {
    readonlyCells       []string
    replicateExisting   bool
    overwritePolicies   bool
    replicationInterval time.Duration
    copyWorkers         int
    healthWatcher       health.Watcher
}

// A ReplicationOption configures EnableReplication.
type ReplicationOption func(*replicationOptions)

// ReadonlyCells adds additional cells that should additionally
// contain read-only replicas of the data.
//
// Passing this option multiple times will add additional
// read-only cells.
//
// Default: none
func ReadonlyCells(cells ...string) ReplicationOption {
    return func(opts *replicationOptions) {
        opts.readonlyCells = append(opts.readonlyCells, cells...)
    }
}

// ReplicateExisting controls whether files that already exist in the
// primary cells will be replicated.  Otherwise, only newly-added
// files will be candidates for replication.
//
// Passing this option again will overwrite earlier values.
//
// Default: false
func ReplicateExisting(enabled bool) ReplicationOption {
    return func(opts *replicationOptions) {
        opts.replicateExisting = enabled
    }
}

// ... other options ...

// DefaultReplicationOptions control the default values before
// applying options passed to EnableReplication.
var DefaultReplicationOptions = []ReplicationOption{
    OverwritePolicies(true),
    ReplicationInterval(12 * time.Hour),
    CopyWorkers(10),
}

func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) {
    var options replicationOptions
    for _, opt := range DefaultReplicationOptions {
        opt(&options)
    }
    for _, opt := range opts {
        opt(&options)
    }
}

//  呼び出し側
func foo(ctx context.Context) {
    // Complex call:
    storage.EnableReplication(ctx, config, []string{"po", "is", "ea"},
        storage.ReadonlyCells("ix", "gg"),
        storage.OverwritePolicies(true),
        storage.ReplicationInterval(1*time.Hour),
        storage.CopyWorkers(100),
        storage.HealthWatcher(watcher),
    )

    // Simple call:
    storage.EnableReplication(ctx, config, []string{"po", "is", "ea"})
}

Conditionals and loops - ifforについて

if文の条件式に改行は入れるべきではない。(Indentation confusionでも述べた通り。)

// 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)
}

より短いコードで書かなければならないという制約がない場合であれば、下記のようにBool値の取得処理を切り出せば良い。

// 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)
}

また、事前にデータを抽出しておくことで条件式を短くすることも可能だったりする。

// Bad:
if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) {
    // ...
}
// Good:
uid := user.GetUniqueUserID()
if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) {
    // ...
}

ifのコードブロックの可読性を上げるために、{}のインデントを揃えたりはきちんと対応しましょう。

// Good:
if err := db.RunInTransaction(func(tx *db.TX) error {
    return tx.Execute(userUpdate, x, y, z)
}); err != nil {
    return fmt.Errorf("user update failed: %s", err)
}

// Good:
if _, err := client.Update(ctx, &upb.UserUpdateRequest{
    ID:   userID,
    User: user,
}); err != nil {
    return fmt.Errorf("user update failed: %s", err)
}

for文については、なるべく改行を入れないようにしましょう。

// Good:
for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ {
    // ...
}

switch文についても、改行を入れないようにしましょう。

// Good:
switch good := db.TransactionStatus(); good {
case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting:
    // ...
case db.TransactionCommitted, db.NoTransaction:
    // ...
default:
    // ...
}
// Bad:
switch bad := db.TransactionStatus(); bad {
case db.TransactionStarting,
    db.TransactionActive,
    db.TransactionWaiting:
    // ...
case db.TransactionCommitted,
    db.NoTransaction:
    // ...
default:
    // ...
}

case条件が長すぎる場合は、全ての条件式を改行した上で、コードブロック内の処理は1行空けてから記述すると良いでしょう。

// Good:
switch db.TransactionStatus() {
case
    db.TransactionStarting,
    db.TransactionActive,
    db.TransactionWaiting,
    db.TransactionCommitted:

    // ...
case db.NoTransaction:
    // ...
default:
    // ...
}

if文の条件式については、比較演算子(==<=)の左辺に変数を持ってきましょう。

// Good:
if result == "foo" {
  // ...
}
// Bad:
if "foo" == result {
  // ...
}

Copying - 値のコピーについて

予期せぬエイリアスを生んだり、バグの原因になったりするため、他のパッケージから構造体をコピーする際は注意が必要。

  • sync.Mutexのような同期オブジェクトはコピーNG
  • bytes.Buffer型も、コピーすると内部で保持しているsliceのエイリアスが作られてしまい、副作用をもたらす可能性がある。
// Bad:
mu := sync.Mutex{}
mu2 := mu
// Bad:
b1 := bytes.Buffer{}
b2 := b1

レシーバーに指定する構造体のフィールドに、コピーされては困るフィールドが含まれている場合、一般的にポインタレシーバーにするべきである。

// Good:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func New() *Record {...}

func (r *Record) Process(...) {...}

func Consumer(r *Record) {...}

下記のように、値レシーバーにしてしまうと、r.bufフィールドがコピーされる結果になってしまう。

// Bad:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func (r Record) Process(...) {...} // Makes a copy of r.buf

func Consumer(r Record) {...} // Makes a copy of r.buf

Don't panic - パニックはNG

panic()を使わず、通常のエラーハンドリングをするか、return errしましょう。
mainパッケージ内で、プログラムを強制終了させなければならないような、状況になった場合(configが間違っている場合など)は、log.Exit()を使ってプログラムを終了させましょう。

Must functions - 失敗時にプログラムを停止させる関数について

失敗したらプログラム自体を終了させた方が良いような、セットアップヘルパー関数には、mustという単語をつけましょう。また、このような関数はユーザーの入力値エラーなどをハンドリングするようなケースではなく、必ずプログラムが立ち上がるタイミングで実行されるようにしましょう。

// Good:
func MustParse(version string) *Version {
    v, err := Parse(version)
    if err != nil {
        log.Fatalf("MustParse(%q) = _, %v", version, err)
    }
    return v
}

// Package level "constant". If we wanted to use `Parse`, we would have had to
// set the value in `init`.
var DefaultVersion = MustParse("1.2.3")

テーブル駆動テストのように、正解値である構造体を作る際などでも活用することが可能である。

// Good:
func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any {
  t.Helper()
  any, err := anypb.New(m)
  if err != nil {
    t.Fatalf("MustMarshalAny(t, m) = %v; want %v", err, nil)
  }
  return any
}

func TestCreateObject(t *testing.T) {
  tests := []struct{
    desc string
    data *anypb.Any
  }{
    {
      desc: "my test case",
      // Creating values directly within table driven test cases.
      data: mustMarshalAny(t, mypb.Object{}),
    },
    // ...
  }
  // ...
}

プログラムを終了させるような可能性のある関数は、基本的に下記の条件に当てはまるような関数では導入しない方が良い。

  • エラーを確実に捕捉するのが難しい
  • エラーをチェックすべきコンテキスト(例:リクエストハンドラの実装箇所など)
  • 通常のエラーハンドリングが可能な場所
// Bad:
func Version(o *servicepb.Object) (*version.Version, error) {
    // Return error instead of using Must functions.
    v := version.MustParse(o.GetVersionString())
    return dealiasVersion(v)
}

Goroutine lifetimes - Goroutineのライフタイムについて

comming soon...

Interfaces - Interfaceについて

comming soon...

Generics - Genericsについて

comming soon...

Pass values - 値渡しについて

comming soon...

Receiver type - レシーバーに指定する型について

comming soon...

Switch and break - Switch文について

comming soon...

Synchronous functions - 非同期関数について

comming soon...

Type aliases - type aliasについて

comming soon...

Use %q - fmt.Printfにおける%qについて

comming soon...

Use any - any型について

comming soon...

Common libraries

Flags

comming soon...

Logging

comming soon...

Contexts

comming soon...

crypto/rand

comming soon...

Useful test failures

Assertion libraries - アサーションライブラリについて

comming soon...

Identify the function - テスト対象関数の特定について

comming soon...

Identify the input - エラーの原因となったinputの特定について

comming soon...

Got before want - 実際に得られた結果は先に表示せよ

comming soon...

Full structure comparisons - 構造体の比較について

comming soon...

Compare stable results - 安定した結果の比較について

comming soon...

Keep going - 失敗しても突き進め

comming soon...

Equality comparison and diffs - 等価比較と差分について

comming soon...

Level of detail - 詳細のレベルについて

comming soon...

Print diffs - 比較結果のプリント方法について

comming soon...

Test error semantics - 関数の返すエラーの種類チェックについて

comming soon...

Test structure

Subtests - サブテストについて

comming soon...

Table-driven tests - テーブル駆動テストについて

comming soon...

Test helpers - テストヘルパー関数について

comming soon...

Test package - テストパッケージについて

comming soon...

Use package testing - testingライブラリを使いましょう

comming soon...

Non-decisions

20
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
10