
こんにちは。よっしーです(^^)
本日は、Go言語を効果的に使うためのガイドラインについて解説しています。
背景
Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。
newによる割り当て(Allocation with new)
Goには2つの割り当てプリミティブがあります。組み込み関数のnew
とmake
です。これらは異なることを行い、異なる型に適用されるため、混乱を招く可能性がありますが、ルールは単純です。まずnew
について話しましょう。これはメモリを割り当てる組み込み関数ですが、他の一部の言語の同名の関数とは異なり、メモリを初期化せず、ゼロ化するだけです。つまり、new(T)
は型T
の新しい項目のためにゼロ化されたストレージを割り当て、その住所、つまり型*T
の値を返します。Go用語では、型T
の新しく割り当てられたゼロ値へのポインタを返します。
new
によって返されるメモリはゼロ化されているため、データ構造を設計する際に、各型のゼロ値がさらなる初期化なしに使用できるように配置すると役立ちます。これは、データ構造のユーザーがnew
でそれを作成し、すぐに作業に取り掛かることができることを意味します。例えば、bytes.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
解説
1. new関数の基本概念
new
はメモリを割り当ててゼロ値で初期化し、そのポインタを返す組み込み関数です。
基本的な使用法:
// int型のメモリを割り当て、ゼロ値(0)で初期化し、そのポインタを返す
p := new(int)
fmt.Println(*p) // 0が出力される
// string型のメモリを割り当て、ゼロ値("")で初期化し、そのポインタを返す
s := new(string)
fmt.Println(*s) // 空文字列が出力される
2. newの特徴
1. ゼロ化のみ、初期化なし:
- 他言語のコンストラクタとは異なる
- メモリを型のゼロ値で埋めるだけ
- カスタム初期化は行わない
2. ポインタを返す:
type Person struct {
Name string
Age int
}
p := new(Person) // *Person型を返す
fmt.Printf("%T\n", p) // *main.Person
fmt.Printf("%+v\n", *p) // {Name: Age:0}
3. Go各型のゼロ値
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // ""
var p *int // nil
var slice []int // nil
var m map[string]int // nil
var ch chan int // nil
4. ゼロ値が有用な設計の例
bytes.Buffer
の例:
var buf bytes.Buffer // ゼロ値でも使用可能
buf.WriteString("Hello")
fmt.Println(buf.String()) // "Hello"
// new()を使っても同様
bufPtr := new(bytes.Buffer)
bufPtr.WriteString("World")
fmt.Println(bufPtr.String()) // "World"
sync.Mutex
の例:
var mu sync.Mutex // ゼロ値でも使用可能
mu.Lock() // 正常に動作
defer mu.Unlock()
// new()を使っても同様
muPtr := new(sync.Mutex)
muPtr.Lock()
defer muPtr.Unlock()
5. 推移的なゼロ値の有用性
SyncedBuffer
の例では、各フィールドのゼロ値が有用なため、構造体全体のゼロ値も有用になります:
type SyncedBuffer struct {
lock sync.Mutex // ゼロ値:アンロック状態のmutex
buffer bytes.Buffer // ゼロ値:使用可能な空バッファー
}
// どちらも追加の初期化なしに使用可能
p := new(SyncedBuffer) // *SyncedBuffer型
var v SyncedBuffer // SyncedBuffer型
// 両方とも即座に使用可能
p.lock.Lock()
p.buffer.WriteString("data")
p.lock.Unlock()
v.lock.Lock()
v.buffer.WriteString("data")
v.lock.Unlock()
6. newの使用例と実践
基本的な使用:
// プリミティブ型
intPtr := new(int)
*intPtr = 42
fmt.Println(*intPtr) // 42
// 構造体
type Point struct {
X, Y int
}
point := new(Point)
point.X = 10
point.Y = 20
fmt.Printf("%+v\n", *point) // {X:10 Y:20}
関数からポインタを返す:
func NewPoint(x, y int) *Point {
p := new(Point)
p.X = x
p.Y = y
return p
}
// または
func NewPointLiteral(x, y int) *Point {
return &Point{X: x, Y: y} // より一般的
}
7. newと他の割り当て方法の比較
// 1. new()を使用
p1 := new(Point)
p1.X = 1
p1.Y = 2
// 2. 構造体リテラルのアドレス取得(より一般的)
p2 := &Point{X: 1, Y: 2}
// 3. ゼロ値での宣言
var p3 Point
p3.X = 1
p3.Y = 2
// p1とp2は*Point型、p3はPoint型
8. newの利点と考慮事項
利点:
- シンプルで一貫性のある割り当て方法
- ゼロ値の保証
- 型安全性
考慮事項:
- 構造体リテラル
&Type{}
の方が一般的 - 初期値を設定する場合は構造体リテラルが便利
- スライスやマップには
make
を使用
9. 実際の開発での使用パターン
// ゼロ値で十分な場合
func NewBuffer() *bytes.Buffer {
return new(bytes.Buffer) // 空バッファーを作成
}
// 初期値が必要な場合(構造体リテラルを推奨)
func NewServer(port int) *Server {
return &Server{
Port: port,
Started: time.Now(),
}
}
new
は Go言語の重要な機能ですが、実際の開発では構造体リテラルの方がよく使われます。ただし、ゼロ値が有用な型の設計思想は、Go言語全体の哲学として非常に重要です。
おわりに
本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

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