背景
会社の同僚とGoのドキュメントを読んでいく活動をしております。
とはいえただその会に参加するだけでは頭を素通りしそうなので、個人でもガッツリ読んでいこうと思います。
ここではEffective Goの翻訳もどきを実施します。
今回読むもの
本題
Introduction
Goは新しい言語です。
既存の言語からアイディアを借りているものの、Goは珍しい性質を持っています。それによってGoプログラムは関係する言語とは異なる効果的なものになります。
C++やJavaのプログラムからGoへの単純な翻訳は満足のいくような結果をえられないでしょう。JavaプログラムはJavaで書かれて、Goでは書かれていないからです。
一方、Goの観点から問題を考えると、成功するがまったく異なるプログラムが作成される可能性があります。
言い換えると、Goを上手に書くには、Goの性質とイディオムを理解する必要があります。
ネーミングやフォーマット、プログラム構築といったGoプログラムの確立された慣習を知ることも重要です。
これにより、あなたの書いたプログラムは他のプログラマーが理解しやすくなるでしょう。
このドキュメントは明確に書くためのTipsとGoのイディオムを提示します。
最初に読むべき「language specification」「Tour of Go」「How to Write Go Code」の補強をします
Examples
Go package sourcesはコアライブラリとしてだけではなく、Goの使用方法の例を提供することを目的としています。
さらにパッケージの多くは、Webサイト上で実行できる、自己完結型の実行可能ファイルを保持しております。
問題へのアプローチ方法、どのように実装するべきかの疑問がありましたら、ライブラリのドキュメント・コード・例が役に立つでしょう。
Formatting
フォーマットの問題はよく論争になりますが、それほど重要ではありません。
人々は異なるフォーマットに順応できますが、そうならない方が良いですが、全員が同じスタイルを使用すればトピックに費やす時間が短くすみます。
問題は、長い規範的なスタイルガイドなしでこのユートピアにアプローチする方法です。
Commentary
Goでは/* */でブロックコメント、//でラインコメントを書くことができます。
ラインコメントが標準であり、ブロックコメントは主にパッケージコメントとして使用されます。
大量のコードを無効化させる際にもブロックコメントは有効です。
プログラム(Webサーバ)のgodocはGoソースファイルを処理して、パッケージの内容に関するドキュメントを作成します。
トップレベルの宣言の前に書かれるコメントは、そのアイテムの説明として、宣言とともにドキュメントに抽出されます。
これらのコメントの性質とスタイルによって、godocで生成されるドキュメントの品質が決まります。
全てのパッケージにパッケージコメント(パッケージ句の前にあるブロックコメント)を書く必要があります。
複数のファイルからなるパッケージは、そのうち一つのファイルにのみパッケージコメントを書けば良いです。
パッケージコメントではパッケージの紹介とパッケージ全体に関する情報を提供しなければなりません。
godocの最初に記載され、詳細なドキュメントを設定する必要があります。
パッケージがシンプルなのであれば、パッケージコメントも短く書けます。
コメントには追加の書式設定は必要ありません。
生成されたドキュメントは、固定幅のフォントで表示されない恐れもあります。そのため間隔による整形はしないでください。gofmtと同様に、godocがそれを処理します。
コメントは未解釈のプレインテキストです。そのためHTMLや他の_this_のような注釈はそのまま出力され、使用できません。
godocが行う調整の一つは、プログラムスニペットに適した固定幅フォントでインデントが実施されたテキストを表示することです。
fmt packageのパッケージコメントはこの性質を利用しています。
Names
Semicolons
Control structures
If
Redeclaration and reassignment
For
Switch
Type switch
Functions
Data
Allocation with new
Goではnewとmakeの二つの方法でプリミティブを生成できます。
両者は異なる物であり、異なる型に適用されるためとても紛らわしいですが、そのルールは単純です。
まずはnewに関してお話ししましょう。
newはメモリを割り当てる組み込み関数ですが、他の言語でのnewとは異なり、メモリの初期化を行いません。ただzeroにするだけです。
つまり、new(T)ではゼロ化されたストレージをT型の新しいアイテムに割当、そのアドレス(*T)を返します。
Go言語では、new(T)はT型のゼロ値へのポインターを返します。
newによって返却されるメモリはゼロ値であるため、データ構造を設計する時、各型のゼロ値を初期化することなく使えるよう調整すると便利です。
これはデータ構造の利用者がnewを実行するだけですぐに作業できることを意味します。
例えば、bytes.Bufferのドキュメントでは、以下のように書かれています。 参考(ドキュメント)参考(実装)
「Bufferのゼロ値はすぐに利用できる空のBufferです。」
同様にsync.Mutexには明示的なコンストラクターやinitメソッドはありません。参考(ドキュメント)参考(実装)
代わりにsync.Mutexのゼロ値はロックされていないmutexとして定義されています。
ゼロ値が有用であるプロパティーは推移的に動作します。
以下の型宣言で考えてみましょう。
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer型の値も、割当や宣言だけですぐに利用することができます。
次のコードでは、pとvの両方ともこれ以上の特に何もしなくても正しく動作します。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
Constructors and composite literals
ゼロ値では不十分で、初期化コンストラクタが必要になることがあります。
以下にosパッケージから派生した例を示します。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd //ファイル記述子
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
NewFileはファイル記述子とファイル名を受け取って、それを持つ新しいファイルを返します。
ここにはいくつもの冗長な部分があります。
これは複合リテラルを使用して単純化できます。
複合リテラルとは、評価される度に新しいインスタンスを生成する式です。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
C言語とは異なり、ローカル変数のアドレスを返して構わないことに注意してください。
関数が値を返却した後も、変数に関連づけられたストレージは生き残っています。
実際、複合リテラルのアドレスを取得すると、評価される度に新しいインスタンスが確保されるので、最後の二行を組み合わせることができます。
(こんな感じで↓)
return &File{fd, name, nil, 0}
複合リテラルのフィールドは順番通りに配置され、全て記載されなくてはいけません。
しかしながら、明示的にfield:valueのペアとしてラベル付をすることで、イニシャライザは任意の順で記載することができ、記載されていない物はそれぞれのゼロ値として初期化されます。
したがって、次のように記載できます。
return &File{fd: fd, name: name}
特殊なケースとして、複合リテラルにフィールドが全く含まれていない場合、ゼロ値の型が作成されます。
つまり、new(File)と&File{}は同値です。
複合リテラルは、配列・スライス・Mapも作ることができ、フィールド・ラベルは適切にインデックスやMapのキーになります。
以下の例において、Enone・Eio・Einvalの値がそれぞれ個別の変数であれば、それらの値に関係なく初期化できます。
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
参考)https://play.golang.org/p/hcKalEJzTE
Allocation with make
割り当ての話に戻します。
組み込み関数のmake(T, args)はnew(T)とは使用目的が異なります。
makeはスライス・Map・channelのみを作成することができ、初期化された(ゼロ値でない)T型の値(*Tでない)を返却します。
このようにnew(T)と区別されるのは、これら3つの型は隠されたデータ構造体への参照であり、使用する前に初期化をする必要があるためです。
例えばスライスは、データ(配列内)へのポインタ・長さ・キャパシティを含む3要素から構成されており、それらの要素が初期化されるまではスライスはnilです。
スライス・Map・チャンネルの場合、makeは内部データ構造を初期化し、値を準備します。
make([]int, 10, 100)
上記のように記載した場合、100個のintの配列を確保し、配列の最初の10要素を指し、長さ10、容量100のスライス構造体を作成します。
(スライスを作成するとき、キャパシティを省略できます。詳細はスライスの章を参照してください)
対照的に、new([]int)は新たにメモリを割り当てられたゼロスライス構造体へのポインタ、つまり、nilスライスへのポインタを返却します。
以下はnewとmakeの違いの例示です。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
makeはmap・スライス・チャンネルにのみ適用され、ポインタは返さないことを覚えておいてください。
明示的なポインタを取得するためには、newを使用してメモリを割り当てるか、変数のアドレスを明示的に取得してください。
Arrays
Slices
Two-dimensional slices
maps
Printing
Append
Initialization
Methods
Interfaces and other types
The blank identifier
Embedding
Go言語は、他の言語にあるような、サブクラスによる型駆動の概念を提供しません。
しかし、構造体やインタフェースに型を組み込むことによって実装の一部を借用することができます。
インタフェースの組み込みはとてもシンプルです。
io.Reader、io.Writerインタフェースに関して前回説明しました。
(バイト列を読み出し・書き出しするためのインターフェースです)
ここにそれらの定義があります。
// Reader 基本的なReadメソッドをラップするインターフェース
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer 基本的なWriteメソッドをラップするインターフェース
type Writer interface {
Write(p []byte) (n int, err error)
}
ioパッケージは、メソッドを実装できるオブジェクトを指定する、いくつかの他のインタフェースもエクスポートします。
例えば、ReadとWriteの両方を含むインタフェースであるio.ReadWriterがあります。
私たちは、二つのメソッドを明示的にリストすることで、io.ReadWriterを指定することができます。
// ReadWriter Read/Writeのメソッドを明示的にリストすることによって定義
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
しかし2つのインタフェースを埋め込んで、新しいインタフェースを形成する方が簡単で刺激的です。
(こんな感じ)
// ReadWriter はReaderインタフェースとWriterインタフェースを組み込んだインタフェースです
type ReadWriter interface {
Reader
Writer
}
ReadWriterはReaderが実行することと、Writerが実行することを、実行できます。
これは組み込みインタフェースの結合です。
インタフェースに組み込むことができるのはインタフェースのみです。
⭐️インタフェースの継承
同じ基本的な考え方が構造体にも当てはまりますが、より広範囲な意味合いがあります。
bufioパッケージには、bufio.Readerとbufio.Writerの二つの構造体タイプがあり、それぞれioパッケージに類似したインタフェースを実装しています。
またbufioは、readerとwriterを構造体に組み込むことで、buffered readerとbuffered writerを実装します。構造体内のタイプをリストしますが、フィールド名は付けません。
// ReadWriter はReaderとWriterのポインターを格納します。
// io.ReadWriterを実装します。
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
組み込まれた要素は構造体へのポインタであり、もちろん、使用する前に有効な構造体を指すように初期化する必要があります。
ReadWriter構造体は次のように、名称を指定することができます。
type ReadWriter struct {
reader *Reader
writer *Writer
}
ただし、フィールドのメソッドを使うためには、そしてioインタフェースを満たすためには、以下のように転送メソッドを提供する必要もあります。
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
構造体を直接埋め込むことにより、重複した定義を回避することができます。
組み込んだ型のメソッドは自由に使うことができます。
つまり、bufio.ReadWriterはbufio.Readerとbufio.Writerメソッドを持つだけでなく、 io.Reader, io.Writer, io.ReadWriterの3つのインタフェース全てを満たすことができます。
重要なサブクラスが異なる組み込み方法があります。
型を組み込むとき、その型のメソッドは外部型のメソッドになりますが、呼び出されるとメソッドのレシーバは外部型ではなく内部型になります。
この例では、bufio.ReadWriterのreadメソッドが呼び出されると、上記の転送メソッドとまったく同じ効果があります。レシーバーは、ReadWriter自体ではなく、ReadWriterのリーダーフィールドです。
埋め込みはシンプルで便利になり得ます。
この例は、
type Job struct {
Command string
*log.Logger
}
上記実装によって、Job型はPrint, Printf, Printlnおよび、*log.Loggerのメソッドが含まれるようになりました。
Loggerにフィールド名をつけることもできますが、そうする必要はありません。
そして、一度初期化されると、Jobにログを記録できます。
job.Println("starting now...")
LoggerはJob構造体の通常のフィールドであるため、次のようにJobのコンストラクター内で通常の方法で初期化できます。
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
また、複合リテラルを使用して
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
⭐️https://play.golang.org/p/rCdTBMS-L-_1
組み込みフィールドを直接参照する必要がある場合は、ReadWriter構造体のReadメソッドで行ったように、パッケージ修飾子を無視したフィールドのタイプ名がフィールド名として機能します。
ここで、Job変数の*log.Loggerへアクセスをする必要がある場合は、job.Loggerを記述します。これはLoggerのメソッドを改良する場合に役立ちます。
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
型を埋め込むと名前の競合の問題が発生しますが、解決方法はシンプルです。
まず、Xフィールド(Xメソッド)はより深くネストされた部分にある他のアイテムXを非表示しにします。
もしlog.LoggerにCommandと言うフィールド・メソッドが含まれている場合、JobのCommandフィールドがそれを支配します。
次に、同じ名前が同じネストレベルにある場合、通常はエラーです。
Job構造体に他のLoggerと言うフィールドやメソッドが含まれている場合、log.Logerを埋め込むのは誤りです。
しかしながら、重複した名前が型定義以外のプログラムで言及されていない場合は問題ありません。
この資格は外部から埋め込まれた型に加えられた変更に対するある程度の保護を提供します。
どちらのフィールドも使用されていない場合に、別のサブタイプの別のフィールドと競合するフィールドが追加されても問題はありません。