
こんにちは。よっしーです(^^)
本日は、Go言語を効果的に使うためのガイドラインについて解説しています。
背景
Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。
メソッド(Methods)
ポインタ vs 値(Pointers vs. Values)
ByteSize
で見たように、メソッドは任意の名前付き型(ポインタやインターフェースを除く)に対して定義できます。レシーバは構造体である必要はありません。
上記のスライスの議論で、Append
関数を書きました。これをスライスのメソッドとして定義することもできます。そのためには、まずメソッドをバインドできる名前付き型を宣言し、次にその型の値をメソッドのレシーバにします。
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// 上で定義したAppend関数とまったく同じ本体
}
これでも、メソッドが更新されたスライスを返す必要があります。メソッドがByteSlice
へのポインタをレシーバとして受け取るように再定義することで、この不格好さを取り除くことができます。そうすれば、メソッドが呼び出し元のスライスを上書きできます。
func (p *ByteSlice) Append(data []byte) {
slice := *p
// 上記と同じ本体、ただしreturnなし
*p = slice
}
実際、さらに良くすることができます。関数を標準的なWrite
メソッドのように修正すると、
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// 再び上記と同様
*p = slice
return len(data), nil
}
そうすると、*ByteSlice
型が標準インターフェースio.Writer
を満たすようになり、これは便利です。例えば、これに出力することができます。
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
*ByteSlice
のみがio.Writer
を満たすため、ByteSlice
のアドレスを渡します。レシーバにおけるポインタ vs 値の規則は、値メソッドはポインタと値の両方で呼び出せますが、ポインタメソッドはポインタでのみ呼び出せるということです。
この規則が生まれる理由は、ポインタメソッドがレシーバを変更できるからです。値でそれらを呼び出すと、メソッドは値のコピーを受け取ることになり、すべての変更が破棄されてしまいます。そのため、言語はこの間違いを許可しません。ただし、便利な例外があります。値がアドレス可能な場合、言語は自動的にアドレス演算子を挿入することで、値でポインタメソッドを呼び出すという一般的なケースを処理します。私たちの例では、変数b
はアドレス可能なので、単にb.Write
でそのWrite
メソッドを呼び出すことができます。コンパイラが自動的にそれを(&b).Write
に書き換えてくれます。
ちなみに、バイトのスライスでWrite
を使うという考え方は、bytes.Buffer
の実装の中核となっています。
解説
1. メソッドの基本
// 独自の型を定義
type ByteSlice []byte
// 値レシーバのメソッド
func (slice ByteSlice) Append(data []byte) []byte {
// 元のsliceは変更されない
// 新しいsliceを返す必要がある
}
2. ポインタレシーバの利点
// ポインタレシーバのメソッド
func (p *ByteSlice) Append(data []byte) {
// *p でポインタが指す実際の値にアクセス
// 元のsliceを直接変更できる
// 戻り値が不要
}
3. 重要な規則
- 値メソッド: ポインタでも値でも呼び出せる
- ポインタメソッド: ポインタでのみ呼び出せる(原則)
4. 便利な例外
var b ByteSlice
b.Write(data) // これは実際には (&b).Write(data) として処理される
Goコンパイラが自動的にアドレスを取得してくれるので、書きやすくなっています。
5. 実用例
io.Writer
インターフェースを実装することで、標準ライブラリの関数(fmt.Fprintf
など)と組み合わせて使えるようになります。
このパターンはbytes.Buffer
でも使われており、Goの標準的な設計パターンの一つです。
おわりに
本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント