255
134

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ゼロ値を使おう #golang

Posted at

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

コンポジット型のゼロ値

配列や構造体などの複数のデータ型が集まったコンポジット型のゼロ値について考えます。

配列のゼロ値は、すべての要素がゼロ値である配列です。
例えば、次のように要素が3int型配列のゼロ値はすべての要素が0である配列になります。

package main

import "fmt"

func main() {
  var ns [3]int
  fmt.Println(ns)
}

このコードを実行すると次のような結果が表示されます。

[0 0 0]

一方、構造体のゼロ値はすべてのフィールドがゼロ値である値です。
例えば、次のようにint型のフィールドNint型の配列で要素数が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, "は存在しない")
    }
  }
}

スライスをゼロ値のまま扱う

スライスについてもゼロ値のまま扱えるように設計されています。
スライスは組込み関数であるlencapappendなどの引数に渡すことができます。
これらの関数では、次のように引数のスライスがゼロ値である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.Writerio.Readerとして渡す際に出会うことが多いでしょう。

var buf bytes.Buffer

// bufはio.Writerを実装していないからコンパイルエラー
fmt.Fprintf(buf, "Hello")

// &bufはio.Writerを実装している
fmt.Fprintf(&buf, "Hello")

まとめ

この記事ではゼロ値について基礎的な話からゼロ値で扱える型の設計方法まで解説を行いました。
うまくゼロ値で扱うことによってシンプルでスッキリとしてコードになるでしょう。

ぜひ読者のみなさんもゼロ値を正しく理解し、積極的に使ってみてください。

255
134
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
255
134

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?