帰ってきた「しっぽのさきっちょ」

関数とポインタ — プログラミング言語 Go

いまさらな内容なのだが覚え書きとして記しておく。

Go 言語における Calling Sequence

まずは簡単な足し算の関数を定義してみる。

func add(x int, y int) int {
	return x + y
}

add に続く括弧内が引数を定義していて,括弧の後ろの int は返り値の型1 を示している。 add() 関数を呼び出すには以下のように記述する。

package main

import "fmt"

func add(x int, y int) int {
	return x + y
}

func main() {
	ans := add(42, 13)
	fmt.Println(ans)
}

xy は同じ int 型なので以下のように記述することもできる。

func add(x, y int) int {
	return x + y
}

返り値を組(tuple)で定義することもできる。

func split(sum int) (int, int) {
	x = sum * 4 / 9
	y = sum - x
	return x, y
}

また返り値は以下のように名前をつけることもできる。

func add(x, y int) (ans int) {
	ans = x + y
	return
}

最後の return がないとコンパイル・エラーになるので注意。 この書き方は defer 構文と組み合わせるときに威力を発揮する。

package main

import "fmt"

func main() {
	err := r()
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Normal End.")
	}
}

func r() (err error) {
	defer func() {
		if rec := recover(); rec != nil {
			err = fmt.Errorf("Recovered from: %v", rec)
		}
	}()

	f()
	err = nil
	return
}

func f() {
	panic("Panic!")
}

このコード2 では r() 関数内で panic を捕まえ, 返り値の err に値をセットしなおしている。

Go 言語の引数は「値渡し」

Go 言語の引数は基本的に「値渡し(call by value)」である。 たとえば先程の足し算を

func add(x, y int) int {
	x += y
	return x
}

と定義した場合でも

package main

import "fmt"

func add(x, y int) int {
	x += y
	return x
}

func main() {
    x := 42
    y := 13
	ans := add(x, y)
	fmt.Printf("%d + %d = %d\n", x, y, ans) //output: 42 + 13 = 55
}

呼び出し元で add() 関数の引数に渡した instance は関数実行後も変化しない。 このため「値渡し」は thread safe なコードに向いている。 たとえば value object を構成する際には関連する関数は「値渡し」のほうが安全である。 ただし関数呼び出し時に instance の値が常にコピーされるため3,サイズの大きな instance の場合は呼び出し時のコストが高くなる。

引数を「参照渡し(call by reference)」にしたい場合はポインタを使う。 つまり instance のポインタ値を渡すのである。

package main

import "fmt"

func add(x, y *int) int {
	*x += *y
	return *x
}

func main() {
	x := 42
	y := 13
	ans := add(&x, &y)
	fmt.Printf("%d + %d = %d\n", x, y, ans) //output: 55 + 13 = 55
}

このコードでは add() 関数実行後の x の値が変更されている。 内部状態を持つ instance を引数に指定する場合は参照渡しにする必要がある。 しかし引数を参照渡しにすると関数実行が thread safe でなくなる可能性がある。 また引数の値が nil の場合も考慮する必要がある。

ちなみに Go 言語では通常の方法ではポインタ演算ができない。 たとえば,ついうっかり

func add(x, y *int) int {
	x += y
	return *x
}

とか書いてしまっても

invalid operation: x += y (operator + not defined on pointer)

とコンパイル・エラーになる。 ポインタ演算が必要な場合は unsafe パッケージを使う。

Slice, Map, Channel は常に「参照渡し」

slice, map, channel は組み込み型だが内部状態を持つ4。 したがって,これらの型の instance を引数に渡す場合はつねに「参照渡し」になる(つまり instance のコピーは発生しない)。

package main

import "fmt"

func setItem(ary map[int]int, index, item int) {
	ary[index] = item
}

func main() {
    ary := map[int]int{0: 0}
	fmt.Println(ary) //output: map[0:0]
	setItem(ary, 0, 1)
	fmt.Println(ary) //output: map[0:1]
	setItem(ary, 10, 10)
	fmt.Println(ary) //output: map[0:1 10:10]
}

ただし固定の配列や string5 の instance は「値」として振る舞うため6,引数に指定した場合も「値渡し」になる。 slice とは挙動が異なるためテキトーなコードを書いていると混乱しやすい。

package main

import "fmt"

func setItem(ary [4]int, index, item int) {
	ary[index] = item
}

func main() {
	ary := [4]int{0, 1, 2, 3}
	fmt.Println(ary) //output: [0 1 2 3]
	setItem(ary, 1, 10)
	fmt.Println(ary) //output: [0 1 2 3]
	ary[2] = 200
	fmt.Println(ary) //output: [0 1 200 3]
}

固定配列や string 型を「参照渡し」にしたい場合はやはりポインタ値を渡す。

package main

import "fmt"

func setItem(ary *[4]int, index, item int) {
	(*ary)[index] = item
}

func main() {
	ary := [4]int{0, 1, 2, 3}
	fmt.Println(ary) //output: [0 1 2 3]
	setItem(&ary, 1, 10)
	fmt.Println(ary) //output: [0 10 2 3]
}

実際には string 型の instance は「不変(immutable)」なので「参照渡し」が必要な局面はほとんど無いと思われる7。 固定配列は不変ではないが,配列を操作するのであれば固定配列ではなく slice のほうが扱いやすい。 たとえば上のコードでは ary := []int{0, 1, 2, 3} と初期化すれば slice として扱える。

Method Receiver

ある型に関数を関連付ける場合は method receiver を使う。

type Vertex struct {
	X int
	Y int
}

func (v Vertex) Add(dv Vertex) Vertex {
	v.X += dv.X
	v.Y += dv.Y
	return v
}

(v Vertex) の部分が method receiver である。 Add() 関数を呼び出すには以下のように記述する。

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func (v Vertex) String() string {
	return fmt.Sprint("X = ", v.X, ", Y = ", v.Y)
}

func (v Vertex) Add(dv Vertex) Vertex {
	v.X += dv.X
	v.Y += dv.Y
	return v
}

func main() {
	v := Vertex{X: 1, Y: 2}
	vv := v.Add(Vertex{X: 3, Y: 4})
	fmt.Println(v)  //output: X = 1, Y = 2
	fmt.Println(vv) //output: X = 4, Y = 6
}

関数の calling sequence としては v.Add(dv)Vertex.Add(v, dv) は等価である。 つまり vAdd() 関数の0番目の引数として振る舞い,「値渡し」でセットされる。

Method receiver の型をポインタ型にすれば「参照渡し」にできる。

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func (v Vertex) String() string {
	return fmt.Sprint("X = ", v.X, ", Y = ", v.Y)
}

func (v *Vertex) Add(dv Vertex) {
	if v == nil {
		return
	}
	v.X += dv.X
	v.Y += dv.Y
}

func main() {
	v := &Vertex{X: 1, Y: 2}
	v.Add(Vertex{X: 3, Y: 4})
	fmt.Println(v) //output: X = 4, Y = 6
}

この場合も calling sequence としては v.Add(dv)(*Vertex).Add(v, dv) は等価である。

Method Receiver の暗黙的変換

Method receiver を「値渡し」にした場合,呼び出し元の instance がポインタ型であっても暗黙的に「値渡し」に変換される。

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func (v Vertex) String() string {
	return fmt.Sprint("X = ", v.X, ", Y = ", v.Y)
}

func (v Vertex) Add(dv Vertex) Vertex {
	v.X += dv.X
	v.Y += dv.Y
	return v
}

func main() {
	v := &Vertex{X: 1, Y: 2} //pointer
	vv := v.Add(Vertex{X: 3, Y: 4})
	fmt.Println(v)  //output: X = 1, Y = 2
	fmt.Println(vv) //output: X = 4, Y = 6
}

Method receiver を「参照渡し」にした場合も暗黙的に「参照渡し」に変換される。

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func (v Vertex) String() string {
	return fmt.Sprint("X = ", v.X, ", Y = ", v.Y)
}

func (v *Vertex) Add(dv Vertex) {
	if v == nil {
		return
	}
	v.X += dv.X
	v.Y += dv.Y
}

func main() {
	v := Vertex{X: 1, Y: 2} //not pointer
	v.Add(Vertex{X: 3, Y: 4})
	fmt.Println(v) //output: X = 4, Y = 6
}

Method Receiver の値が nil の場合

Method receiver の値が nil の場合はどうなるか。 まずは「値渡し」の場合。

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func (v Vertex) String() string {
	return fmt.Sprint("X = ", v.X, ", Y = ", v.Y)
}

func (v Vertex) Add(dv Vertex) Vertex {
	v.X += dv.X
	v.Y += dv.Y
	return v
}

func main() {
	v := (*Vertex)(nil) //nil
	vv := v.Add(Vertex{X: 3, Y: 4})
	fmt.Println(v)
	fmt.Println(vv)
}

この場合は Add() 関数呼び出し時に panic になる。

panic: runtime error: invalid memory address or nil pointer dereference

まぁこれは分かりやすいよね。 では「参照渡し」の場合はどうなるか。

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func (v Vertex) String() string {
	return fmt.Sprint("X = ", v.X, ", Y = ", v.Y)
}

func (v *Vertex) Add(dv Vertex) {
	if v == nil {
		return
	}
	v.X += dv.X
	v.Y += dv.Y
}

func main() {
	v := (*Vertex)(nil) //nil
	v.Add(Vertex{X: 3, Y: 4})
	fmt.Println(v) //output: <nil>
}

実は Add() 関数呼び出し時点では panic にはならない。 上のコードでは v に nil が渡される。 したがって Add() 関数内の条件文を削除すると

func (v *Vertex) Add(dv Vertex) {
	v.X += dv.X
	v.Y += dv.Y
}

v 内の要素を参照としたところで panic になる。 Method receiver を「参照渡し」にする場合は nil 値に注意する必要がある。

for-range 構文も「値渡し」

余談だが for-range 構文も「値渡し」(つまりコピーが発生する)なので注意が必要である。 たとえば以下のコードで

package main

import "fmt"

func main() {
	ary := []int{0, 1, 2, 3}
	fmt.Println(ary) //output: [0 1 2 3]
	for _, item := range ary {
		item += 10
	}
	fmt.Println(ary) //output: [0 1 2 3]
}

for-range 構文内の itemary 内の要素を指すのではなく要素のコピーである。 したがって item を操作しても ary には影響しない。 ary 内の要素を操作するのであれば素朴に

package main

import "fmt"

func main() {
	ary := []int{0, 1, 2, 3}
	fmt.Println(ary) //output: [0 1 2 3]
	for i := 0; i < len(ary); i++ {
		ary[i] += 10
	}
	fmt.Println(ary) //output: [10 11 12 13]
}

とするしかない。

ブックマーク

Go 言語に関するブックマーク集はこちら


  1. 型については「Go 言語における「オブジェクト」」を参照のこと。 [return]
  2. このコードについては「エラー・ハンドリングについて」で解説している。ちなみに panic を潰して error を返すのはエラー・ハンドリングとしてはいいやり方ではない。 [return]
  3. 値がどこにコピーされるかは型によって異なる。 string 以外の基本型は値がスタックに積まれる。 string および基本型以外はヒープ領域に値がコピーされそのポインタがスタックに積まれる。 [return]
  4. slice, map, channel は内部状態を持つため new() 関数ではなく make() 関数で instance を生成する。 [return]
  5. string 型の実体は []byte 型である。 [return]
  6. たとえば固定の配列や string 型の instance は nil 値を持たない。 string 型のゼロ値は空文字列である。 [return]
  7. このような需要としては文字列操作で「NULL 状態」が必要な場合であろう。たとえば DBMS にアクセスする場合は NULL 状態を扱う必要がある。なお Go 言語のコア・パッケージには database/sql があり NullString を使うことにより NULL 状態を扱える。このように NULL 状態を扱う必要がある場合は,直にポインタ操作するのではなく,何らかの value object を用意してカプセル化するほうが安全である。 [return]