それが僕には楽しかったんです。

ミドルウェアとかやってます

Go 再入門 ~基本的なデータ構造周り~

はじめに

どうも、最近自分と同い年のプロダクトに触れて死ぬほど苦しんだけんつです。
ここ数ヶ月ずっと MySQL の検証や DBMS の勉強をしてきて思ったのだが、コード書いてない。
読む機会は履いて捨てるほどあるが、コード書くことなさすぎてめちゃくちゃ書けなくなっていた。昨日書いてみてこんなに書けないかと驚いた。
なので、リハビリをかねてハイパー中途半端に知っているぐらいの Go の再入門やろうと思います。
未読だった Effective Go で「ここわかんねーわ」とか「お、これは学びだな」と、思ったところをやっていきます。

golang.org

今日はデータ構造周り。

Allocation with new()

Go にはメモリ割当を行うための組み込み関数として new と make がある。まずは new から。
new は単純に、ある型 T に対して new(T) と使用すると、メモリ領域を初期化せずに zero-value を持たせた上で確保しそのポインタを返すという挙動を取る。
例えば次のようなコードを実行する場合、実行結果は以下の通りになる。

package main

import "fmt"

type Person struct {
	Age  int
	Name string
}

func main() {
	p := new(Person)
	fmt.Printf("%+v\n", p)

	num := new(int)
	fmt.Println(*num)
}
❯ go run alloc_new.go
&{Age:0 Name:}
0

これが例えば、Effective Go にあるような sync.Mutex や bytes.Buffer にあるようなものだとこうなる。

package main

import (
	"fmt"
	"sync"
	"bytes"
)

type SyncedBuffer struct {
	Lock sync.Mutex
	Buffer bytes.Buffer
}

func main() {
	p := new(SyncedBuffer)
	fmt.Printf("%+v\n", p)
	
}
❯ go run alloc_new2.go
&{Lock:{state:0 sema:0} Buffer:{buf:[] off:0 lastRead:0}}

zero value によってすぐに利用できる構造体も存在する。
ちなみに var hoge T で変数を宣言した場合、それはポインタではないが new と同じように zero value をもつ。
初期化を伴わない*1と言われることが多い。

Allocation with make()

次は組み込み関数の make。make(T, args) という形を取るが、T に当てはまる型は slice, map, channel だけという特徴がある。
また new は返す値がポインタなのに対してこっちはポインタではない。またこちらは初期化を伴う。zero value と初期化の違いがわかりにくいので、中でもわかりやすい slice を例にする。
make を使って int を要素にもつスライスを作成するコードとその実行結果をあげる。

package main

import "fmt"

func main() {
	s := make([]int, 10, 100)
	fmt.Println(s)
	fmt.Printf("len(%d)\n", len(s))
	fmt.Printf("cap(%d)\n", cap(s))
}
❯ go run alloc_make.go
[0 0 0 0 0 0 0 0 0 0]
len(10)
cap(100)

実行結果からわかるように make を使用してスライスを作成した場合は array, len, cap *2の初期化を伴う。
これが new, make の違いとなる。

Arrays

配列はメモリの詳細なレイアウトを決める時に役立ち、割当を回避することができる場合もあるが基本的にはスライスのビルディングブロッックになっている。
また C の配列と比較して以下の違いを持っている。

  • 配列は値。配列に配列を渡すと全ての値がコピーされる。
  • 関数へ渡す時はポインタではなくコピーが渡される
  • サイズも含めて配列は一つの型として扱われる。[3]int, [2]int は別の型として扱われる。

Slices

Slice は Array のラッパーで連続するデータに対して、汎用的で強力なインターフェースを提供している。
行列のように明確な次元をもつ必要が無い限り、大抵は Slice を利用する。Slice は元になる配列への参照を持っていて、あるスライスを別のスライスに代入すると二つのスライスは単一の配列に対する参照を持つこととなっている。
これは関数の引数にする場合も同様に動作することに注意が必要。

例えば以下のコードを実行すると、次のような結果が得られる。

package main

import "fmt"

func main() {
	a := [10]int{1,2,3,4,5,6,7,8,9,10}
	s1 := a[:]
	s2 := a[2:]
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)

	s1 = s2
	s2[0] = 1
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)
}
❯ go run slice.go
[1 2 3 4 5 6 7 8 9 10]
[1 2 3 4 5 6 7 8 9 10]
[3 4 5 6 7 8 9 10]
[1 2 1 4 5 6 7 8 9 10]
[1 4 5 6 7 8 9 10]
[1 4 5 6 7 8 9 10]

おわりに

この調子で進めていきたい

*1:Go の変数は宣言だけだったとしても zero value が必ず入るためわかりにくいが、初期化とは区別されているみたい

*2:src/runtime/slice.go - The Go Programming Language