ゼロ値を使おう #golang
October 13, 2018
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
Goの変数は必ず初期化される
組込み型のゼロ値
Goの変数は必ず初期化されることをご存知でしょうか?
例えば、int
型の場合は0
で、string
型の場合は""
で初期化されています。
次のように、代入せずにfmt.Printf
で表示させてみましょう。
package main
import "fmt"
func main() {
var n int
var f float64
var s string
var b bool
fmt.Printf("%#v %#v %#v %#v", n, f, s, b)
}
このコードを実行すると次のような結果が表示されます。
0 0 "" false
組込み型のゼロ値をまとめると次のようになります。
型 | ゼロ値 |
---|---|
int ,int8 ,int16 ,int32 ,int64 |
0 |
uint ,uint8 ,uint16 ,uint32 ,uint64 |
0 |
byte ,rune ,uintptr |
0 |
float32 ,float64 |
0 |
complex64 ,complex128 |
0 |
bool |
false |
string |
"" |
error |
nil |
コンポジット型のゼロ値
配列や構造体などの複数のデータ型が集まったコンポジット型のゼロ値について考えます。
配列のゼロ値は、すべての要素がゼロ値である配列です。
例えば、次のように要素が3
でint
型配列のゼロ値はすべての要素が0
である配列になります。
package main
import "fmt"
func main() {
var ns [3]int
fmt.Println(ns)
}
このコードを実行すると次のような結果が表示されます。
[0 0 0]
一方、構造体のゼロ値はすべてのフィールドがゼロ値である値です。
例えば、次のようにint
型のフィールドN
とint
型の配列で要素数が3のフィールドNS
がある場合は、
それぞれがゼロ値で初期化された構造体の値になります。
package main
import "fmt"
func main() {
type Hoge struct {
N int
NS [3]int
}
var h Hoge
fmt.Printf("%#v", h)
}
このコードを実行すると次のような結果が表示されます。
main.Hoge{N:0, NS:[3]int{0, 0, 0}}
なお、構造体の埋め込みについてもフィールド名の無い匿名フィールドであって、フィールドであることには変わらないため、次のように明示的に初期化しなくてもゼロ値になります。
package main
import "fmt"
func main() {
type Hoge struct {
N int
}
type Fuga struct {
int
Hoge
}
var f Fuga
fmt.Printf("%#v", f)
}
このコードを実行すると次のような結果が表示されます。
main.Fuga{int:0, Hoge:main.Hoge{N:0}}
ゼロ値がnil
である型
組込み関数のmake
で初期化を行う型はゼロ値がnil
です。
スライス、チャネル、マップなどがそれに該当します。
package main
import "fmt"
func main() {
var ch chan int
var ns []int
var m map[string]int
fmt.Printf("%#v %#v %#v", ch, ns, m)
}
このコードを実行すると次のような結果が表示されます。
(chan int)(nil) []int(nil) map[string]int(nil)
make
関数で初期化する型以外でもいくつかゼロ値がnil
である型が存在します。
package main
import "fmt"
func main() {
var f func()
var ptr *int
fmt.Printf("%#v %#v", f, ptr)
}
このコードを実行すると次のような結果が表示されます。
(func())(nil) (*int)(nil)
なお、The Go Playgroundで実行すると次のように、go vet
がエラーを出しますがここでは気にしなくても良いです。
prog.go:8: Printf format %#v arg f is a func value, not called
ゼロ値のまま扱う
組込み型をゼロ値のまま扱う
次のように、int
型を使ってカウントを行う際や合計を求める際に初期値として0
を設定するコードを見かけることがあります。
package main
import "fmt"
func main() {
count := 0
count++
count++
fmt.Println(count)
}
しかし、int
型の変数はわざわざ0
を代入して初期化を行わなくてもゼロ値である0
で初期化されています。
そのため、次のように変数定義を行うだけで問題ありません。
package main
import "fmt"
func main() {
var count int
count++
count++
fmt.Println(count)
}
ゼロ値という概念があることは非常に重要で強力です。
次のようにint
型を値として扱うマップを考えると、その効果を感じることができるでしょう。
package main
import "fmt"
func main() {
words := []string{"dog", "cat", "dog", "fish", "cat"}
wc := map[string]int{}
for _, w := range words {
wc[w]++
}
fmt.Println(wc)
}
このコードを実行すると次のような結果が表示されます。
map[dog:2 cat:2 fish:1]
変数wc
は単語ごとのカウントを取るための変数です。
キーが単語でバリューがその単語の出現回数となっています。
変数wc
は最初に空のマップが代入され、その後は特にキー毎に初期値を入れるような処理はされていません。
Goのマップはキーが存在しない場合にゼロ値を返すため、明示的な初期化が必要ないからです。
wc["dog"]
のようにアクセスした場合に、マップにキー"dog"
が存在していない場合はゼロ値が返ってきます。
int
型のゼロ値は0
であるため、そこにwc["dog"]++
のように加算を行っても問題はありません。
bool
型についてもゼロ値であるfalse
を上手く使うことで無駄な初期化を省いてシンプルに記述することができます。
次の例では、マップのバリューにbool
型を使って単語リスト中に単語が存在するかを表す変数wc
を定義しています。
前述の通り、マップはキーが存在しない場合にバリューの型のゼロ値を返すため、bool
型の場合はfalse
になります。
これをうまく使うことで、マップにキーが存在しないという状態をゼロ値(false
)で表すことが可能になっています。
package main
import "fmt"
func main() {
words := []string{"dog", "cat", "dog", "fish", "cat"}
wc := map[string]bool{}
for _, w := range words {
wc[w] = true
}
for _, w := range []string{"dog", "pig"} {
if wc[w] {
fmt.Println(w, "は存在する")
} else {
fmt.Println(w, "は存在しない")
}
}
}
スライスをゼロ値のまま扱う
スライスについてもゼロ値のまま扱えるように設計されています。
スライスは組込み関数であるlen
やcap
、append
などの引数に渡すことができます。
これらの関数では、次のように引数のスライスがゼロ値であるnil
でも動作するように実装されています。
package main
import "fmt"
func main() {
var ns []int
fmt.Printf("%#v %d %d\n", ns, len(ns), cap(ns))
ns = append(ns, 10, 20)
fmt.Println(ns)
}
このコードを実行すると次のような結果が表示されます。
[]int(nil) 0 0
[10 20]
ゼロ値で扱える型
Goではゼロ値のまま扱えるように工夫されている型があります。
例えば、sync
パッケージで定義されているsync.Mutex
型は、
次のようにゼロ値のまま使えるように作られています。
package main
import "sync"
func main() {
var mu sync.Mutex
mu.Lock()
mu.Unlock()
}
また、sync
パッケージで提供されている多くの型はゼロ値で使えるようになっています。
sync.WaitGroup
も次のようにゼロ値で使えるように作られています。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("done 1")
}()
go func() {
defer wg.Done()
fmt.Println("done 1")
}()
wg.Wait()
fmt.Println("main done")
}
ゼロ値のまま扱えるようにする
必要な時に初期化を行う
sync
パッケージで用意されている型のように、ゼロ値にままで扱えるようにするためには少し工夫が必要です。
ゼロ値で扱えるようにするということは、New
のプリフィックスが付くような初期化用の関数を用意せずに初期化をする必要があります。
例えば、次のような単語をカウントする型であるWordCounter
型を実装することを考えてみましょう。
WordCounter
型は内部にマップを持ちます。
マップはゼロ値(nil
)のまま扱うことができないため、初期化をする必要があります。
マップの初期化はNewWordCounter
関数で行い、戻り値として*WordCounter
型の値を返します。
また、*WordCounter
型はメソッドとして単語ごとの出現数のカウントを行うCount
メソッドと
単語ごとの出現数を返すGet
メソッドを持ちます。
package main
import "fmt"
type WordCounter struct {
m map[string]int
}
func NewWordCounter() *WordCounter {
return &WordCounter{
m: map[string]int{},
}
}
func (wc *WordCounter) Count(s string) int {
wc.m[s]++
return wc.m[s]
}
func (wc *WordCounter) Get(s string) int {
return wc.m[s]
}
func main() {
wc := NewWordCounter()
words := []string{"dog", "cat", "dog", "fish", "cat"}
for _, w := range words {
wc.Count(w)
}
fmt.Println("dog", wc.Get("dog"))
}
このコードを実行すると次のような結果が表示されます。
dog 2
ゼロ値で扱えるようにするためには、NewWordCounter
関数で行っていた処理を各メソッドで行う必要があります。
例えば、最も単純な方法としては次のように、関数の先頭でフィールドが初期化されているか確認して初期化を行うというものがあります。
func (wc *WordCounter) Count(s string) int {
if wc.m == nil {
wc.m = map[string]int{}
}
wc.m[s]++
return wc.m[s]
}
しかし、この方法では初期化が複雑になった場合や初期化するフィールドが複数存在する場合には、コードが煩雑になってしまいます。
そこで、次のようにsync.Once
型を使い、初期化を一度だけ行うようにすると初期化コードをスッキリと書くことができます。
package main
import (
"fmt"
"sync"
)
type WordCounter struct {
initOnce sync.Once
m map[string]int
}
func (wc *WordCounter) init() {
wc.m = map[string]int{}
}
func (wc *WordCounter) Count(s string) int {
wc.initOnce.Do(wc.init)
wc.m[s]++
return wc.m[s]
}
func (wc *WordCounter) Get(s string) int {
wc.initOnce.Do(wc.init)
return wc.m[s]
}
func main() {
var wc WordCounter
words := []string{"dog", "cat", "dog", "fish", "cat"}
for _, w := range words {
wc.Count(w)
}
fmt.Println("dog", wc.Get("dog"))
}
*WordCounter
型のメソッドとして新たにinit
メソッドが追加されています。
また、フィールドとしてsync.Once
型のinitOnce
フィールドも追加されています。
sync.Once
型は、Do
メソッドを呼び出しを1度だけに限定している型です。
Do
メソッドに渡した関数を1度だけ実行し、その後、いくらDo
メソッドを呼び出しても関数は実行されることはありません。
そのため、Count
メソッドやGet
メソッドの先頭で、wc.initOnce.Do(wc.init)
のように呼び出すことで、
初期化を一度だけ実行してやることができます。
なお、sync.Once
型もゼロ値で扱えるように設計されているため、明示的な初期化が必要ありません。
このように、うまくゼロ値で扱えるようにすると他の型からも利用しやすくなります。
ゼロ値とメソッド
ゼロ値で扱えるような構造体型を作った場合に、メソッドのレシーバはポインタにすべきでしょうか?
Code Review Commentsにもあるように、基本的にはレシーバはポインタにすべきです。 もちろん、ゼロ値で扱えるようにした構造体型についても同様です。
*T
型をレシーバに持つメソッドを定義した場合に、T
型の変数を使っても簡単に呼び出せれるように工夫がされています。
例えば、*T
型にメソッドM
が定義されている場合に、T
型の変数t
を使って、t.M()
のように呼び出すことが可能です。
この場合、t.M()
は(&t).M()
と記述したように扱われます。
実際に、前述のコードでは次のようにWordCounter
型の変数に対して、*WordCounter
型をレシーバに取るメソッドである、Count
メソッドとGet
メソッドを呼び出しています。
func main() {
var wc WordCounter
words := []string{"dog", "cat", "dog", "fish", "cat"}
for _, w := range words {
wc.Count(w) // (&wc).Count(w)と同様
}
fmt.Println("dog", wc.Get("dog")) // (&wc).Get("dog")と同様
}
しかし、いくら簡単に呼び出せるように記述できるからといって、T
型のメソッドセットには、*T
型のメソッドセットは含まれていません(言語仕様で定義されています)。
そのため、*T
型がインタフェースであるI
型を実装している場合、T
型は実装していることにはなりません。
この話は、次のようにゼロ値で扱えるbyte.Buffer
型の値をio.Writer
やio.Reader
として渡す際に出会うことが多いでしょう。
var buf bytes.Buffer
// bufはio.Writerを実装していないからコンパイルエラー
fmt.Fprintf(buf, "Hello")
// &bufはio.Writerを実装している
fmt.Fprintf(&buf, "Hello")
まとめ
この記事ではゼロ値について基礎的な話からゼロ値で扱える型の設計方法まで解説を行いました。 うまくゼロ値で扱うことによってシンプルでスッキリとしてコードになるでしょう。
ぜひ読者のみなさんもゼロ値を正しく理解し、積極的に使ってみてください。