読者です 読者をやめる 読者になる 読者になる

tocsatoの備忘録

ほぼほぼ 50 代のプログラマの備忘録。swift golang javascript css html5 nginx mysql などを最近使ってます。

スライス操作のおさらい - 2

スライス操作のおさらい - 1の続きで、様々なスライス操作のサンプルコードを挙げます。

スライスの引数に配列は渡せない

package main

import (
	"fmt"
)

func foo(b []byte) {
	fmt.Printf("[% 02x]\n", b)
}

func main() {
	x := [4]byte{100, 101, 102, 103}

	// この書き方はエラーになる (x は配列なので)
	// foo(x)

	// こちらはok
	foo(x[:])
}

スライスの長さを拡張する

func expand(b []byte, newlen int) []byte {
	if newlen <= cap(b) {
		// 同じ領域に空データを追加して拡張
		return append(b, make([]byte, newlen - len(b)))
	}

	// 新しい領域にコピーして拡張
	r := make([]byte, newlen)
	copy(r, b)
	return r
}

スライスの容量を拡張する

func grow(b []byte, newcap int) []byte {
	// 新しい領域にコピーして拡張
	r := make([]byte, 0, newcap)
	return append(r, b...)
}

スライスを結合する

func concat(a, b []byte) []byte{
	return append(a, b...)
}

スライスの一部を取り出す

func slice(b []byte, first, last int) []byte {
	if first < 0 {
		// first にマイナスを指定したら、最後から first 番目として扱う
		first = len(b) + first
	}
	if last <= 0 {
		// last = 0 は最後として扱う
		// last にマイナスを指定したら、最後から last 番目として扱う
		last = len(b) + last
	}
	if first > last {
		// first と last の関係が逆なら整える
		first, last = last, first
	}
	return b[first:last]
}

スライスでスタック

扱うデータが値なら、シンプルに書けます。

func push(b []byte, data byte) []byte {
	return append(b, data)
}

func pop(b []byte) ([]byte, byte) {
	if len(b) == 0 {
		// スタック b は空
		return b, 0
	}
	return b[:len(b)-1], b[len(b)-1]
}

扱うデータがポインタなどを扱う場合、明示的に参照をはずす必要があります。

func push(b []interface{}, data interface{}) []interface{} {
	return append(b, data)
}

func pop(b []interface{}) ([]interface{}, interface{}) {
	if len(b) == 0 {
		// スタック b は空
		return b, nil
	}
	// 暗黙の配列からデータへの参照をはずすため、明示的に nil を入れる
	data, b[len(b)-1] := b[len(b)-1], nil
	return b[:len(b)-1], data
}

スライスでキュー

スライスでキューを書くとき、少し気を使う必要があります。
単純なコードは次のようになります。

func enqueue(b []byte, data byte) []byte {
	return append(b, data)
}

func dequeue(b []byte) ([]byte, byte) {
	if len(b) == 0 {
		// キュー b は空
		return b, 0
	}
	return b[1:], b[0]
}

このコードは一見正常に動作します。
しかし dequeue() に関係なく、追加の度に暗黙の配列が拡張されるという問題があります。
dequeue() のときに返されるスライスの容量を確認してみます。

func main() {
	b := make([]byte, 0, 4)

	// 追加
	b = enqueue(b, 1)
	fmt.Printf("len:%d, cap:%d\n", len(b), cap(b))		// len:1, cap:4

	b, _ = dequeue(b)
	fmt.Printf("len:%d, cap:%d\n", len(b), cap(b))		// len:0, cap:3
}

dequeue() の結果、スライスの容量が戻っていないことが分かります。
一時的なキューであったり、一気に enqueue() の後に一気に dequeue() とする場合は、問題ではありません。むしろ積極的に行うべきです。

ただし、enqueue(), dequeue() を繰り返して行う場合などは、dequeue() でスライスの容量をキチンと戻す必要があります。

func enqueue(b []byte, data byte) []byte {
	return append(b, data)
}

func dequeue(b []byte) ([]byte, byte) {
	if len(b) == 0 {
		// キュー b は空
		return b, 0
	}

	// 先頭のデータを取得する
	data := b[0]
	// 要素をすべて先頭に移動する
	copy(b, b[1:])
	// 必要なら最後のデータの参照をはずす
	// b[len(b)-1] = nil

	return b[:len(b)-1], data
}

スライスの途中に追加する

func insert(b []byte, pos int, x []byte) []byte {
	r := make([]byte, len(b) + len(x))
	copy(r, b[:pos])
	copy(r[pos:], x)
	copy(r[pos + len(x):], b[pos:])
	return r
}

少しトリッキーに次のようにも書けます。

func insert(b []byte, pos int, x []byte) []byte {
	r := make([]byte, len(b) + len(x))
	copy(r, b[:pos])
	copy(r[pos + copy(r[pos:], x):], b[pos:])
	return r
}

1回のメモリ確保、3回の copy() であることには変わらないので、あまりメリットはありませんが。

スライスの一部を削除する

func cut(b []byte, first, last int) []byte {
	return append(b[:first], b[last:])
}

暗黙の配列からデータへの参照をはずす必要があるなら、次のコードになります。

func cut(b []interface{}, first, last int) []interface{} {
	copy(b[first:], b[last:])
	newlen := len(b) - last + first
	for i := newlen; i < len(b); i++ {
		b[i] = nil
	}
	return b[:newlen]
}