CLOVER🍀

That was when it all began.

Goの構造体を使う時の初期化方法と結果を確認する

これは、なにをしたくて書いたもの?

Goの構造体の初期化方法はいろいろありますが、どれがどう違うんだっけ?というのをよく忘れるのでメモしておきます。

環境

今回の環境は、こちらです。

$ go version
go version go1.16 linux/amd64

確認用のモジュールの作成。

$ go mod init struct-initialize
go: creating new go.mod: module struct-initialize

確認には、テストコードを使いましょう。

$ go get github.com/stretchr/testify

go.mod

module struct-initialize

go 1.16

require github.com/stretchr/testify v1.7.0 // indirect

仕様

まずは、構造体に関する言語仕様を見てみます。

The Go Programming Language Specification / Struct types

こちらには、初期化に関することは書いていないようです。

組み込み関数である、newに関する記述をちょっと見てみましょう。この関数を使うと、初期化したうえでポインタが返ります。

The built-in function new takes a type T, allocates storage for a variable of that type at run time, and returns a value of type *T pointing to it. The variable is initialized as described in the section on initial values.

The Go Programming Language Specification / Allocation

こちらから、初期化に関する記述にリンクがありました。

so for instance each element of an array of structs will have its fields zeroed if no value is specified.

The Go Programming Language Specification / The zero value

まあ、各フィールドが"ゼロ"として初期化されることが書かれているわけですが。

他にも{}を使ったりといった方法があるはずですが。

こちらでしょうか。Composite literalと呼ぶみたいです。

Composite literals construct values for structs, arrays, slices, and maps and create a new value each time they are evaluated.

The Go Programming Language Specification / Composite literals

さらにここに&を使うと、リテラルで構造体を初期化したうえでポインタを取得する、ということになるみたいですね。

Taking the address of a composite literal generates a pointer to a unique variable initialized with the literal's value.

だいたいわかった気がします。仕様を押さえるのはこのくらいにして、あとは実際の動きを見てみましょう。

確認する

こんな感じで、テストコードの雛形を用意。

main_test.go

package main

import (
    "fmt"
    "testing"

    "github.com/stretchr/testify/assert"
)

// ここにテストなどを書く

確認対象の構造体を用意します。

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

では、確認していきましょう。

構造体をvarで宣言。The zero valueにあたりますね。

func TestDeclareVar(t *testing.T) {
    var p Person

    assert.Equal(t, "main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

Composite literalsを使った場合。フィールドは指定しません。

func TestCompositeLiteral(t *testing.T) {
    p := Person{}

    assert.Equal(t, "main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

こうすると、varで宣言しただけと同じですね。

Composite literalsを使い、フィールドまで指定した場合。

func TestCompositeLiteralFields(t *testing.T) {
    p := Person{FirstName: "カツオ", LastName: "磯野", Age: 11}

    assert.Equal(t, "main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "{FirstName:カツオ LastName:磯野 Age:11}", fmt.Sprintf("%+v", p))
}

ポインタ型で宣言した場合。

func TestDeclarePointerVar(t *testing.T) {
    var p *Person

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "<nil>", fmt.Sprintf("%+v", p))
}

この場合、型はポインタ型になるわけですが、値はnilになります。

これは、The zero valueでポインタ型はnilになると書かれているからですね。

nil for pointers

new関数を使った場合。Allocationですね。

func TestPointerNew(t *testing.T) {
    p := new(Person)

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "&{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

Composite Literalを使い、かつフィールドを指定していない場合と差がありませんね。

Composite literalsを使い、ポインタを取得する場合。フィールドは指定しません。

func TestCompositeLiteralTakePointer(t *testing.T) {
    p := &Person{}

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "&{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

最後は、Composite literalsを使ってポインタを取得しつつ、フィールドも指定する場合。

func TestCompositeLiteralFieldsTakePointer(t *testing.T) {
    p := &Person{FirstName: "カツオ", LastName: "磯野", Age: 11}

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "&{FirstName:カツオ LastName:磯野 Age:11}", fmt.Sprintf("%+v", p))
}

こんな感じでしょうか。

Composite Literalを使った場合に、指定しなかったフィールドがどのような初期値を持つかは、やっぱりThe zero valueを
見ましょう。

false for booleans, 0 for numeric types, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps.

オマケ

今回作成したソースコード全体も載せておきます。

main_test.go

package main

import (
    "fmt"
    "testing"

    "github.com/stretchr/testify/assert"
)

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func TestDeclareVar(t *testing.T) {
    var p Person

    assert.Equal(t, "main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

func TestCompositeLiteral(t *testing.T) {
    p := Person{}

    assert.Equal(t, "main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

func TestCompositeLiteralFields(t *testing.T) {
    p := Person{FirstName: "カツオ", LastName: "磯野", Age: 11}

    assert.Equal(t, "main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "{FirstName:カツオ LastName:磯野 Age:11}", fmt.Sprintf("%+v", p))
}

func TestDeclarePointerVar(t *testing.T) {
    var p *Person

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "<nil>", fmt.Sprintf("%+v", p))
}

func TestPointerNew(t *testing.T) {
    p := new(Person)

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "&{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

func TestCompositeLiteralTakePointer(t *testing.T) {
    p := &Person{}

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "&{FirstName: LastName: Age:0}", fmt.Sprintf("%+v", p))
}

func TestCompositeLiteralFieldsTakePointer(t *testing.T) {
    p := &Person{FirstName: "カツオ", LastName: "磯野", Age: 11}

    assert.Equal(t, "*main.Person", fmt.Sprintf("%T", p))
    assert.Equal(t, "&{FirstName:カツオ LastName:磯野 Age:11}", fmt.Sprintf("%+v", p))
}