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

関数とポインタ — プログラミング言語 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)

とコンパイル・エラーになる。 したがって通常は「Go 言語のポインタは nullable 参照4 と同じ」と考えてよい。

ちなみにポインタ演算が必要な場合は unsafe パッケージを使う。

Slice, Map, Channel は「参照渡し」として振る舞う

slice, map, channel は組み込み型だが内部状態を持つ5。 これらの型の instance を引数に渡す場合は「参照渡し」として振る舞う6

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]
}

ただし固定の配列や string7 の instance は「値」として振る舞うため8,引数に指定した場合も「値渡し」になる。 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)」なので「参照渡し」が必要な局面はほとんど無いと思われる9。 固定配列は不変ではないが,配列を操作するのであれば固定配列ではなく slice のほうが扱いやすい。 たとえば上のコードでは slc := ary[:] といった感じにキャストするか最初から 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. nullable 参照は「null を許容する参照」くらいの意味。 Go 言語なら nil 値。 Go 言語は「null 安全(null safty)」ではないので null 参照(=無効な参照)の始末について instance を参照する側が責務を負うことになる。(参考: 「null 安全」について[return]
  5. slice, map, channel は内部状態を持つため new() 関数ではなく make() 関数で instance を生成する。 [return]
  6. このうち slice については特殊な振る舞いをする。詳しくは「配列と Slice」を参照のこと。 [return]
  7. string 型の実体は []byte 型である。 [return]
  8. たとえば固定の配列や string 型の instance は nil 値を持たない。 string 型のゼロ値は空文字列である。 [return]
  9. このような需要としては文字列操作で「NULL 状態」が必要な場合であろう。たとえば DBMS にアクセスする場合は NULL 状態を扱う必要がある。なお Go 言語のコア・パッケージには database/sql があり NullString を使うことにより NULL 状態を扱える。このように NULL 状態を扱う必要がある場合は,直にポインタ操作するのではなく,何らかの value object を用意してカプセル化するほうが安全である。 [return]