Goのフルスライス

あまりWebの記事中にはフルスライスという表現は出てこないようですが、スライスのスライス作成時は値参照ではなくてメモリは共有されるので、作成したスライスの振る舞いが意図したようにはなりません。

フルスライスとはスライスからスライス作成時にキャパシティと長さ(length)を同じにすることでappend()したときに必ず別領域に新たなスライス用のメモリを確保することで参照ポインタを分離して元のスライスとの干渉を避ける方法です。

y := x[:2:5]の5がキャパシタ指定です。

package main

import "fmt"

func main() {
	x := make([]int, 0, 5)
	x = append(x, 1, 2, 3, 4)
	fmt.Println("x:", x)
	y := x[:2:5]
	fmt.Println("y:", y)
	fmt.Println(len(x), len(y))
	fmt.Println(cap(x), cap(y))
	y = append(y, 30, 40, 50)
	x = append(x, 60)
	fmt.Println("x:", x)
	fmt.Println("y:", y)
	fmt.Println(len(x), len(y))
	fmt.Println(cap(x), cap(y))
}

実行結果は、メモリを共有しているのでスライス名は別でも相互干渉しています。

x: [1 2 3 4]
y: [1 2]
4 2
5 5
x: [1 2 30 40 60]
y: [1 2 30 40 60]
5 5
5 5

フルスライスにすると、

y := x[:2:2]

メモリが別領域に確保されるので干渉は発生しません。領域は1024バイト以下なら領域不足の都度二倍されるので本来は3バイトですが、6バイト確保されています。1024以上なら25%増加になるようですが、これは保証値ではなくて今後のバージョンアップで変更されるかもしれません。いずれにしてもスライスのサイズが大きくなるとメモリ領域確保とデータ転送が発生するので、サイズが大きければ性能に影響してきます。

x: [1 2 3 4]
y: [1 2]
4 2
5 2
x: [1 2 3 4 60]
y: [1 2 30 40 50]
5 5
5 6

スライスのキャパシティとサイズの遷移を図示すると以下の手書きのようになります。つまり

 

admin

recursive call

どの言語にも存在すると思いますがrecursive call(再帰呼び出し)、よく例として挙げられるのがnの階乗です。

recur()が再起呼び出しを使う方法でfact()は通常の繰り返し処理です。これだけ見たらなぜ再起処理必要なのというところでしょうが、例えばbinary tree(二分木)ではrecursive callが必須でこれ以外の方法ではうまく記述できないはずです。

package main

import (
	"fmt"
)

func main(){
	fmt.Println(recur(10))
	fmt.Println(fact(10))

}

// recursive call
func recur(n int) int{
	if n == 2{
		return 2
	}
	return n*recur(n -1)
}

// conventional method
func fact(n int) int{
	m := 1
	nf := n
	for i := 1; i <= n; i++{		
		m *= nf
		nf--
	}
	return m
}

以下はweb上に挙げられてるソースにノードの遷移がわかるようにPrint文を埋め込んだものです。よく見るとinsert()もprintNode()も再帰処理を使って同じような構造になっているのが理解できると思います。さらにdelete()を追加してもそうなります。

/*
https://selfnote.work/20210930/programming/golang-binary-tree-2/
*/

package main

import (
	"fmt"
)

type Tree struct {
	node *Node
}

type Node struct {
	value int
	left  *Node
	right *Node
}

func (t *Tree) insert(value int) *Tree {
	if t.node == nil {
		t.node = &Node{value: value}			// used only at the first node 
	} else {
		t.node.insert(value)
	}
	return t
}

func (n *Node) insert(value int) {
	if n.value >= value {
		if n.left == nil {
			n.left = &Node{value: value}
			fmt.Println("value_add_l", value)
		} else {
			fmt.Println("skip_l", value, n.left.value)
			n.left.insert(value)
		}
	} else {
		if n.right == nil {
			n.right = &Node{value: value}
			fmt.Println("value_add_r", value)
		} else {
			fmt.Println("skip_r", value, n.right.value)
			n.right.insert(value)
		}
	}
}

var i = 0

func printNode(n *Node) {
	if n == nil {
		i += 1
		fmt.Print("nil;")
		return
	}

	fmt.Println("value", n.value)
	printNode(n.left)
	fmt.Println("l",i, "node", n.value)
	printNode(n.right)
	fmt.Println("r",i, "node", n.value)
}

func main() {
	t := &Tree{}
	t.insert(2).
		insert(7).
		insert(5).
		insert(6).
		insert(1).
		insert(11).
		insert(9).
		insert(4).
		insert(20).
		insert(10)

	fmt.Println("-----------------------")
	printNode(t.node)
}

以下に実行結果から手書きでprintNode()の遷移を書いてみましたが、このように動作してます。

 

で、binary treeってどこで使われるの?ですがエンドのアプリで記述するよりもDBの内部処理などでは当たり前に使われているようです。insert(), delete(), search()ってDBでは普通に使われて、なおかつそれらを配列やスライスで表現させるとデータ追加の都度に再度作り直しが発生しますが、binary treeならば追加処理も簡単、さらにはアクセスも平均でn*log(n)で済むだろうから効率的であるとも言えます。

 

admin

Go言語(O’REILLYの初めてのGo言語)

以下の記事の書籍ですが、

https://isehara-3lv.sakura.ne.jp/blog/2023/01/07/go-lang/

とりあえず通読、理解の浅いところも無論ありますが。

O’Reilly本はハズレが少ないけど、この本も当たりでしょう。Goはかなりバージョンアップが頻繁ですが、1.18で追加されたgenericsもきちんとカバーさせてるし。「初めての」意味はもちろんコード書くのが初めてではなくて他の言語の経験が前提です。

副題にイディオマティックというタイトルがつけられていますが、これはGoFでは無いけれどもGoのミニデザインパターン的なことを言っているんだと思います。

結局Go言語の特徴(らしさ)は、並行処理はともかくも型定義、構造体とインターフェースに集約できるような気がします。

 

admin

 

Golang型指定でのチルダの意味

型指定時にチルダを使って~intとか~stringとか出てきますが、どういう意味かというと基底型を意味するのだと。

例えば以下のコードは、

https://github.com/mushahiroyuki/lgo/blob/main/example/ch15/ex1506b.go

を多少修正したものですが、独自の型をstringの拡張として定義するとstringに~なしではコンパイルエラーになりますが~を付けることでstringの派生型も含めることが出来るわけです。

package main

import (
	"fmt"
)

type BuiltInOrdered interface {
	~string | int | int8 | int16 | int32 | int64 | float32 | 
		float64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr

} 

func Min[T BuiltInOrdered](v1, v2 T) T { 
	if v1 < v2 {
		return v1
	}
	return v2
}

type Mystring string

func main() {
	var Astring Mystring = "a"
	var Bstring Mystring = "b"
	fmt.Println(Min(Astring, Bstring)) 
} 

 

admin

 

 

brewとGolang版数

Golangは2023/2に1.20になっていますが、brewで配布されるのは1.19.5が最新版になっています。

brewの場合には多少の遅延があるということだろうと思いますが、とりあえず実害はありませんが。

 

admin

 

Golangのtesting

Go言語にはテスト環境も整備されていますが、ここではベンチマークの例です。testingモジュールのベンチマーク機能を使うことで、わざわざソースコードに性能測定のためのコードの埋め込みが不要になります。ここでは素数計算のコードをターゲットとしてみました。

package adder

import (
	"fmt"
	"runtime"
	"time"
)

func oddCalc() {
	runtime.GOMAXPROCS(8)
	c := 2
	odd := []int{}
	tStart := time.Now()
	for i := 2; i <= 10000*1000; i++ {
		func() {
			flag := true // if odd number, stay "true"
			for j := 2; j*j <= c; j++ {
				if c%j == 0 {
					flag = false
					break
				}
			}
			if flag {
				odd = append(odd, c)
			}
			c++
		}()
	}
	tStop := time.Now()
	fmt.Println(len(odd))

	el := tStop.Sub(tStart)
	fmt.Println(el)
}

同じディレクトリにベンチマーク用の以下のソースコードファイルを用意しておきます。ターゲットと同じファイル中でも構わないと思いますが、ベンチマークを簡単に実行の趣旨からすると外れてます。

package adder

import (
	"testing"
)

func BenchmarkOddCalc(b *testing.B) {
        oddCalc()
	} 

実行結果は以下のとおりです。終了コードの判定もエラーコードも返していないのでpass~~が出力されています。

% go test -bench=. -v
goos: darwin
goarch: arm64
pkg: test
BenchmarkOddCal
664579
1.241648416s
BenchmarkOddCal-8              1        1241672042 ns/op
PASS
ok      test    1.485s

ソースコードに埋め込んだ時間測定とは微妙に値が異なりますが、精度から見れば誤差範囲です。

と言うわけでGoではわざわざ性能測定のためにターゲットのコード中にコードの埋め込みは要らないよという話でした。

 

admin

Goでのチャネルの空読み

Goでのチャネルは使い方を誤ればpanicになりますが、これはGoの潔いところと思えばいいですが、チャネルの使い方で読み出しだけれども変数には代入しないというのがあります。

具体的には値を使わないのだから、単にダミーのリードですが実際の使い方としては終了待ちとして使うことになるんだろうと思います。

非常にシンプルな空読みの確認用のコードですが、チャネルでバッファーを2個用意して、最初の値は空読みすることで、2個目の値が変数xに代入されていることがわかります。

package main

import  "fmt"

func main() {
	ch := make(chan int, 2)
	defer 	close(ch)
	go func() {
		ch <- 100
		ch <- 200
	}()
	<- ch		// dummy read
	x := <- ch
	fmt.Println(x)
}

<実行結果>
200

 

admin

Goのsliceの型はなんでも良い

Goでは何でも型に定義できるかと思いますが(例外があるかどうかは定かではない)、例えば関数もそのまま型として使用できます。

例えば以下のように、

ここでは”func(float64) float64“を型名として使用して、二つの関数をスライスに格納して、それぞれの関数を無名関数の引数(あえて無名関数でなくても良いですが)としてfor rangeループで実行させています。

package main

import (
	"fmt"
	"math"
)


func main() {
	f1 := func(a float64) float64{
		return a*a
	}
	f2 := func(a float64) float64{
		return math.Pow(a, 3)
	}
	
	fs := []func(float64) float64{f1, f2}		// "[]func(float64) float64" is a type

	for _, fss := range fs{
		func(f func(float64) float64){
			fmt.Println(f(3))
		}(fss)
	}
}

他の言語からすると奇妙な記述方法ではありますが、Goではこのような記法も成立するのだと。

 

admin

 

Goのloopclosure問題、

問題というよりも、タイミングがそうだからそうなるよねという話です。

Goのgoroutineでの話で、以下のソースをVScodeで読み込ませるとopenclosureというwarningが出ます。実行結果も期待される値ではなくて、例えば全部40とかになるのですが、なぜか?

それは無名関数に明示的に引数としてvを与えてる訳ではないので、無名関数の中でvの使われる時点(v*2)のv値が使われるからです。具体的には並行処理を起動しても、すぐに各ルーチンが実行される訳でもなくて実行タイミングには遅延があるだろうから、実はvを使おうとしたタイミングではすでにfor range処理が先行したり、あるいは終了している可能性もあるから。

対応方法はv無名関数に引数として渡すのがスマートなやり方になるでしょう。

https://github.com/mushahiroyuki/lgo/blob/main/example/ch10/ex1005.go

package main

import "fmt"

func main() {
	a := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20} //liststart
	ch := make(chan int, len(a))
	for _, v := range a {
		go func() {
			ch <- v * 2
			fmt.Println(":", v)
		}()
	}
	for i := 0; i < len(a); i++ {
		fmt.Print(<-ch, " ")
	} //listend
	fmt.Println()
}

こうすれば、無名関数が不規則なタイミングでその時点のv値を使用することなく、意図した値を引き渡すことができます。別の対応方法としてはvをシャドーイング(v := vをfor rangeの直後に挿入)しても良さそうですが。いずれにしろ並行処理の実行順序は保証されないので、結果の並びは昇順にはなりません。

		go func(v int) {
			ch <- v * 2
			fmt.Println(":", v)
		}(v)

 

admin

 

gobotを使ってTelloを制御する

gobotはrobotics/IoT用のフレームワークですが、サポートモデルとしてTelloが対象に含まれています。

https://gobot.io/blog/2018/04/20/hello-tello-hacking-drones-with-go/

でコマンド制御と動画再生を行っているので、コードを実行してみました。

動画の再生にはmplayer(これは追加インストール必要でした)を使って、goから引き渡しています。

$ brew install mplayer

でインストールします。go getでもmplayerがインストールできますが、外部アプリなのでこちらではない。

<環境構築>

Goはインストール済みの前提で、gobotの他にdji/telloのインストールも必要。これはmod内でしかインストールできません。

% go get -d -u gobot.io/x/gobot/...
% go get gobot.io/x/gobot
% go get gobot.io/x/gobot/platforms/dji/tello

<Telloから画像受け取り再生するコード>

Telloの素のコマンドは意識する必要はなくて関数でwrapされています。明示的に並行処理は使っていなくて、drone.On()はイベント待ちのように見えます。

package main

import (
	"fmt"
	"os/exec"
	"time"

	"gobot.io/x/gobot"
	"gobot.io/x/gobot/platforms/dji/tello"
)

func main() {
	drone := tello.NewDriver("8890")

	work := func() {
		mplayer := exec.Command("mplayer", "-fps", "25", "-")
		mplayerIn, _ := mplayer.StdinPipe()
		if err := mplayer.Start(); err != nil {
			fmt.Println(err)
			return
		}

		drone.On(tello.ConnectedEvent, func(data interface{}) {
			fmt.Println("Connected")
			drone.StartVideo()
			drone.SetVideoEncoderRate(4)
			gobot.Every(100*time.Millisecond, func() {
				drone.StartVideo()
			})
		})

		drone.On(tello.VideoFrameEvent, func(data interface{}) {
			pkt := data.([]byte)
			if _, err := mplayerIn.Write(pkt); err != nil {
				fmt.Println(err)
			}
		})
	}

	robot := gobot.NewRobot("tello",
		[]gobot.Connection{},
		[]gobot.Device{drone},
		work,
	)

	robot.Start()
}

 

動画は外部のアプリを使っているせいか、遅延が大きいのと画素が飛んでいます。gobotの本質とはそれほど関係ないですが、

gobotはこれからも成長していくだろうから、Robotics/IoTアプリ開発はかなり楽になりそうです。

 

admin