Effective Goをゆるく読みすすめる
個人的にはgolang-cilintがすきだけどデフォルトだと厳しすぎる
www.DeepL.com/Translator(無料版)で翻訳しました。
Introduction
Go は新しい言語です。
既存の言語からアイデアを拝借していますが、効果的なGoプログラムは、その親戚で書かれたプログラムとは異なる特徴を持っています。
C++やJavaのプログラムを簡単にGoに翻訳しても、満足のいく結果は得られないでしょう。
一方、Goの観点から問題を考えると、成功したが全く異なるプログラムができあがる可能性があります。
言い換えれば、Goをうまく書くためには、Goの特性とイディオムを理解することが重要です。
また、他のGoプログラマーにも理解しやすいプログラムを書くために、
名前付け、
フォーマット、
プログラムの構築など、
Goでのプログラミングのために確立された慣習を知っておくことも重要です。
このドキュメントでは、明確で慣用的なGoコードを書くためのヒントを提供します。
言語仕様書、Goツアー、Goコードの書き方』を補強するものであり、これらはすべて最初に読むべきものです。
Example
Go パッケージのソースは、コアライブラリとしてだけでなく、言語の使用方法の例題としても機能することを意図しています。
困ったらコアライブラリを嫁
さらに、パッケージの多くには、golang.org のウェブサイトから直接実行できる、自己完結型の実行例が含まれています(必要であれば、"Example "という単語をクリックして開いてください)。
問題にどのようにアプローチするか、あるいはどのように実装するかについて質問がある場合、ライブラリ内のドキュメントやコード、サンプルが答えやアイデア、背景を提供してくれます。
つまり困ったらコアライブラリを嫁
Formatting
書式の問題は、最も論争の的になるが、最も影響が少ない問題である。
人々は異なる書式スタイルに適応することができますが、その必要がない方が良いですし、全員が同じスタイルに固執した方が話題に割く時間も少なくて済みます。
問題は、このユートピアにどうやってアプローチするかということです。
Goでは一風変わったアプローチをとり、ほとんどの書式設定の問題は機械に任せています。
gofmtプログラム(go fmtとしても利用可能で、ソースファイルレベルではなくパッケージレベルで動作します)は、Goプログラムを読み込んで、インデントと垂直配置の標準スタイルでソースを出力し、コメントを保持し、必要に応じてフォーマットを変更します。
もし、新しいレイアウトの状況をどのように扱うか知りたい場合は、gofmtを実行してみてください。
例として、構造体のフィールドにコメントを並べるのに時間をかける必要はありません。Gofmtはあなたのためにそれをしてくれます。宣言が与えられている場合
code:go
type T struct {
name string // name of the object
value int // its value
}
gofmt will line up the columns:
code:go
type T struct {
name string // name of the object
value int // its value
}
標準パッケージのすべての Go コードは gofmt でフォーマットされています。
いくつかの書式設定の詳細が残っています。簡単に説明します。
インデント インデントにはタブを使用し、gofmt はデフォルトでタブを表示します。
どうしても必要な場合のみスペースを使用してください。
行の長さ Goには行の長さの制限はありません。
パンチしたカードがはみ出してしまう心配はありません。
行が長すぎると感じる場合は、それを折り返し、余分なタブでインデントします。
括弧 Go は C や Java に比べて括弧を必要としません。
制御構造 (if、for、switch) の構文には括弧がありません。
また、演算子の優先順位の階層はより短く、より明確です。
code:go
x<<8 + y<<16
Commentary
Go は C スタイルの /* */ ブロックコメントと C++ スタイルの // 行コメントを提供します。ブロックコメントは主にパッケージコメントとして表示されますが、式の中やコードの大部分を無効化するのに便利です。
プログラムとウェブサーバである godoc は Go ソースファイルを処理して、パッケージの内容に関する文書を抽出します。
トップレベルの宣言の前に現れるコメントは、改行を挟まずに宣言と一緒に抽出され、項目の説明テキストとして機能します。
これらのコメントの性質とスタイルは、godoc が生成するドキュメントの質を決定します。
すべてのパッケージはパッケージコメントを持つべきです。
複数ファイルのパッケージでは、パッケージコメントは一つのファイルに存在すればよく、 どのファイルでもよいでしょう。パッケージコメントはパッケージを紹介し、パッケージ全体に関連する情報を提供しなければなりません。パッケージコメントは godoc ページの最初に現れ、その後の詳細なドキュメントを設定しなければなりません。
code:go
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term:
'^'
'$'
'.'
character
' '^' character-ranges '' '(' regexp ')'
*/
package regexp
パッケージがシンプルなものであれば、パッケージのコメントは簡潔にすることができます。
code:go
// Package path implements utility routines for
// manipulating slash-separated filename paths.
コメントには星のバナーのような余分な書式設定は必要ありません。
生成された出力は固定幅フォントで表示されないこともあるので、整列のための間隔に依存しないでください。
コメントは解釈されないプレーンテキストなので、HTMLや_this_のような他の注釈はそのまま再現されるので、使用すべきではありません。
godocが行う一つの調整は、プログラムのスニペットに適した固定幅のフォントでインデントされたテキストを表示することです。fmtパッケージのパッケージコメントでは、これを効果的に使用しています。
文脈によっては、godocはコメントを再フォーマットしないかもしれませんので、正しいスペル、句読点、文の構造、長い行を折りたたむなど、ストレートに見栄えが良いことを確認してください。
パッケージ内では、トップレベル宣言の直前にあるコメントは、その宣言の doc コメントとして機能します。プログラム内のすべてのエクスポートされた(大文字で書かれた)名前には必ずdocコメントを付けなければなりません。
docコメントは完全な文として機能するのがベストで、これにより様々な自動化されたプレゼンテーションが可能になります。最初の文は、宣言された名前で始まる一文の要約でなければなりません。
code:go
// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {
すべてのdocコメントが説明する項目の名前で始まる場合、goツールのdocサブコマンドを使ってgrepで出力を実行することができます。Compile "という名前を覚えていなくて、正規表現の解析関数を探していて、コマンドを実行したと想像してみてください。
$ go doc -all regexp | grep -i parse
パッケージ内のすべてのdocコメントが "This function... "で始まっていたら、 grepは名前を覚えるのに役立ちません。しかし、パッケージはそれぞれのdocコメントが名前で始まるので、次のようなものが表示され、探している単語を思い出すことができます。
code:shell
$ go doc -all regexp | grep -i parse
Compile parses a regular expression and returns, if successful, a Regexp
MustCompile is like Compile but panics if the expression cannot be parsed.
parsed. It simplifies safe initialization of global variables holding
$
Goの宣言構文では、宣言をグループ化することができます。1つのdocコメントで、関連する定数や変数のグループを紹介することができます。宣言全体が表示されるので、このようなコメントはしばしば意味のないものになることがあります。
code:go
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)
グループ化は、変数の集合がミューテックスで保護されていることなど、項目間の関係を示すこともできます。
code:go
var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)
Names
名前は他の言語と同じようにGoでも重要です。
名前は意味的にも効果があります。
パッケージの外で名前が見えるかどうかは、最初の文字が大文字かどうかで決まります。
そのため、Goプログラムの命名規則について少し時間をかけて説明する価値があります。
Package names
パッケージをインポートすると、パッケージ名がコンテンツのアクセサになります。import "bytes"
インポートするパッケージは bytes.Buffer について話すことができます。
パッケージを使う人全員が同じ名前を使ってその内容を参照できると便利です。
慣例では、パッケージ名は小文字でシングルワードの名前になります。
パッケージを使う人は誰もがその名前を入力することになるので、簡潔にしましょう。
また、事前に衝突を心配する必要はありません。
パッケージ名はインポートのためのデフォルトの名前でしかありません。
どのような場合でも、インポートのファイル名がどのパッケージを使用しているかを決定するので、混乱することはほとんどありません。
もう一つの慣習は、パッケージ名がソースディレクトリのベース名であるということです。
src/encoding/base64 のパッケージは "encoding/base64" としてインポートされますが、名前は base64 であって、encoding_base64やencodingBase64.ではありません。
パッケージのインポーターは名前を使ってその内容を参照するので、パッケージ内のエクスポートされた名前はその事実を使って吃驚を避けることができます。
これは、テストしているパッケージの外で実行しなければならないテストを簡略化することができますが、それ以外の場合は避けるべきです)。
例えば、bufio パッケージのバッファ付きリーダ型は BufReader ではなく Reader と呼ばれていますが、これはユーザーが bufio.Reader と見ているからで、これは明確で簡潔な名前です。
パッケージ名が文脈になっているから、よけいな名刺を減らせる
より簡潔に書ける
さらに、インポートされたエンティティは常にパッケージ名で指定されるため、bufio.Readerはio.Readerと競合しません。
import するのは bufioやioをインポートして使い分ける
同様に、ring.Ring の新しいインスタンスを作成する関数、つまり Go のコンストラクタの定義は、通常 NewRing と呼ばれるはずでしたが、Ring はパッケージによってエクスポートされる唯一の型であり、パッケージは ring と呼ばれているため、単に New と呼ばれ、パッケージのクライアントは ring.New と見ています。
Similarly, the function to make new instances of ring.Ring—which is the definition of a constructor in Go—would normally be called NewRing, but since Ring is the only type exported by the package, and since the package is called ring, it's called just New, which clients of the package see as ring.New. Use the package structure to help you choose good names.
ringパッケージにRing構造体のみしかないのであればNewRingじゃなくてNewでよいよ
シンプルに保ちましょう
パッケージの構造を使って、良い名前を選ぶのに役立ててください。
もう一つの短い例は once.Do; once.Do(setup) はよく読みますが、 once.DoOrWaitUntilDone(setup) と書いても改善されません。
長い名前をつけたからといって、自動的に読みやすくなるわけではありません。役に立つdocのコメントは、余計な長い名前よりも価値があることがよくあります。
Getters
Go はゲッターとセッターの自動サポートを提供していません。
自分でゲッターやセッターを提供することに問題はありませんし、そうすることが適切であることも多いのですが、ゲッターの名前にGetを入れることは慣用的でも必要でもありません。
owner(小文字、エクスポートされていない)というフィールドがある場合、ゲッターメソッドはGetOwnerではなく、Owner(大文字、エクスポートされている)と呼ばれるべきです。
エクスポートに大文字の名前を使用することで、フィールドとメソッドを区別するためのフックを提供します。
必要に応じてセッター関数は、おそらくSetOwnerと呼ばれるでしょう。どちらの名前も実際にはよく読めます。
code:go
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Interface names
慣例では、片メソッドインタフェースは、メソッド名に -er 接尾辞または同様の修飾子を付けてエージェント名詞を構成して命名されます。Reader、Writer、Formatter、CloseNotifier などです。
このような名前はいくつかありますが、それらの名前とそれらが捕捉する関数名を尊重することは生産的です。
Read, Write, Close, Flush, String などには標準的なシグネチャと意味があります。
混乱を避けるために、同じシグネチャと意味を持つものでない限り、メソッドにこれらの名前を付けないようにしましょう。
逆に、よく知られた型のメソッドと同じ意味を持つメソッドを実装している場合は、同じ名前とシグネチャを与えてください。
MixedCaps
最後に、Goでは、マルチワード名を書くときはアンダースコアではなく、MixedCapsまたはmixedCapsを使用するのが慣例です。
Smicolons
C言語と同様に、Goの形式文法では文の終了にセミコロンを使用しますが、C言語とは異なり、セミコロンはソースには表示されません。
その代わりに、レキサーは単純なルールを使用して、スキャン時に自動的にセミコロンを挿入するので、入力テキストにはほとんどセミコロンがありません。
ルールは次のようになります。改行の前の最後のトークンが識別子 (int や float64 などの単語を含む)、数値や文字列定数などの基本的なリテラル、またはトークン
code:go
break continue fallthrough return ++ -- ) }
レキサーは常にトークンの後にセミコロンを挿入します。これは、「改行が文を終了させる可能性のあるトークンの後に来る場合は、セミコロンを挿入する」と要約することができます。
セミコロンは終了中括弧の直前に省略することもできます。
code:go
go func() { for { dst <- <-src } }()
にはセミコロンは必要ありません。イディオマティックな Go プログラムでセミコロンが必要なのは、for ループ句など、イニシャライザ、条件、継続要素を分離するための場所だけです。また、そのような方法でコードを書く場合には、1 行の複数の文を区切るためにもセミコロンが必要になります。
セミコロン挿入ルールの一つの結果として、制御構造体(if, for, switch, select)の開始波括弧を次の行に置くことができないということがあります。そうすると、中括弧の前にセミコロンが挿入されてしまい、望ましくない結果を引き起こす可能性があります。このように書きます。
code:go
if i < f() {
g()
}
ではなく
code:go
if i < f() // wrong!
{ // wrong!
g()
}
-----
Control structures
Goの制御構造はC言語の制御構造に関連していますが、重要な点で違いがあります。
スイッチはより柔軟性があります。
if と switch は for のようなオプションの初期化文を受け入れます。
break と continue ステートメントは、何をブレークするか継続するかを識別するためのオプションのラベルを取ります。
構文も少し変わっています。括弧はなく、本文は常に中括弧で区切らなければなりません。
If
GoのIfサンプル:
code:go
if x > 0 {
return y
}
強制中括弧は、複数行にシンプルなif文を書くことを奨励します。
特に本文にreturnやbreakのような制御文が含まれている場合は特にそうするのが良いスタイルです。
if や switch は初期化文を受け付けているので、ローカル変数の設定に使われているのを見るのが一般的です。
code:go
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
Goライブラリでは、if文が次の文に流れない場合、つまり本文がbreak、continue、goto、returnで終わる場合、不要な他の文が省略されていることに気づくでしょう。
code:go
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
これは、コードが一連のエラー条件に対してガードしなければならない、よくある状況の例です。
このコードは、エラーが発生したときにそれを排除しながらページを流れる制御の流れを成功させれば、うまく読むことができます。
エラーケースはreturn文で終わることが多いので、結果としてコードにelse文は必要ありません。
code:go
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
Redeclaration and reassignment
余談です。前節の最後の例では、:=短い宣言形式がどのように機能するかを詳しく説明しています。
os.Openを呼び出す宣言は次のようになっています。
code:go
f, err := os.Open(name)
This statement declares two variables, f and err. A few lines later, the call to f.Stat reads,
code:go
d, err := f.Stat()
which looks as if it declares d and err. Notice, though, that err appears in both statements. This duplication is legal: err is declared by the first statement, but only re-assigned in the second. This means that the call to f.Stat uses the existing err variable declared above, and just gives it a new value.
In a := declaration a variable v may appear even if it has already been declared, provided:
this declaration is in the same scope as the existing declaration of v (if v is already declared in an outer scope, the declaration will create a new variable §),
the corresponding value in the initialization is assignable to v, and
there is at least one other variable that is created by the declaration.
This unusual property is pure pragmatism, making it easy to use a single err value, for example, in a long if-else chain. You'll see it used often.
§ It's worth noting here that in Go the scope of function parameters and return values is the same as the function body, even though they appear lexically outside the braces that enclose the body.
For
The Go for loop is similar to—but not the same as—C's. It unifies for and while and there is no do-while. There are three forms, only one of which has semicolons.
code:go
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
Short declarations make it easy to declare the index variable right in the loop.
code:go
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
If you're looping over an array, slice, string, or map, or reading from a channel, a range clause can manage the loop.
code:go
for key, value := range oldMap {
}
If you only need the first item in the range (the key or index), drop the second:
code:go
for key := range m {
if key.expired() {
delete(m, key)
}
}
If you only need the second item in the range (the value), use the blank identifier, an underscore, to discard the first:
sum := 0
for _, value := range array {
sum += value
}
The blank identifier has many uses, as described in a later section.
For strings, the range does more work for you, breaking out individual Unicode code points by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD. (The name (with associated builtin type) rune is Go terminology for a single Unicode code point. See the language specification for details.) The loop
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
prints
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7
Finally, Go has no comma operator and ++ and -- are statements not expressions. Thus if you want to run multiple variables in a for you should use parallel assignment (although that precludes ++ and --).
// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
}
Switch
Go's switch is more general than C's. The expressions need not be constants or even integers, the cases are evaluated top to bottom until a match is found, and if the switch has no expression it switches on true. It's therefore possible—and idiomatic—to write an if-else-if-else chain as a switch.
code:go
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
There is no automatic fall through, but cases can be presented in comma-separated lists.
code:go
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
Although they are not nearly as common in Go as some other C-like languages, break statements can be used to terminate a switch early. Sometimes, though, it's necessary to break out of a surrounding loop, not the switch, and in Go that can be accomplished by putting a label on the loop and "breaking" to that label. This example shows both uses.
code:go
Loop:
for n := 0; n < len(src); n += size {
switch {
if validateOnly {
break
}
size = 1
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(srcn + srcn+1<<shift) }
}
Of course, the continue statement also accepts an optional label but it applies only to loops.
To close this section, here's a comparison routine for byte slices that uses two switch statements:
code:go
// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
return 1
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
Type switch
A switch can also be used to discover the dynamic type of an interface variable. Such a type switch uses the syntax of a type assertion with the keyword type inside the parentheses. If the switch declares a variable in the expression, the variable will have the corresponding type in each clause. It's also idiomatic to reuse the name in such cases, in effect declaring a new variable with the same name but a different type in each case.
code:go
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}