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

 

interfaceの基本(Go言語)

interfaceはGo言語における唯一の抽象型だそうで、抽象型というのは実装の自由度がある訳だから使いこなせば便利でしょう。

ここでは使いこなしではなくて、基本機能の説明です。構造体のデータの操作を行う二つのメソッドを定義しています。

package main

import (
	"fmt"
)

type person struct {
	FirstName	string
	FamilyName	string
	Age			int8
}

type inf interface{
	print()
	exchange()
}

func (p *person) print(){
	fmt.Println(p.FirstName, p.FamilyName, p.Age)
}

func (p *person) exchange(){
	fn := p.FirstName
	p.FirstName = p.FamilyName
	p.FamilyName = fn
	p.print()
}

func main(){
	var i1 inf = &person{
		FirstName: "Mary",
		FamilyName: "Bloody",
		Age: 36,
	}

	i1.print()

	i1.exchange()
}

実行結果は、

Mary Bloody 36
Bloody Mary 36

Goの場合にはJavaやC#のように明示的なimplementは不要です、つまり緩い関係になってます。interfaceで定義したメソッドが全て実装されていればimplementと等価です。

じゃ『interfaceは何が良いの?』というとそれは抽象型だからというところに行き着くでしょう、何故なら実装をどう記述しようがinterfaceに規定したメソッドと同じ形(名称、引数と戻り値)であれば入れ替えできるわけで、実は明示的では無いですが継承や多態性が実現できることになります。それゆえGoにおける唯一の抽象型というのは使い方に多様性があることになります。

例えば、上の例で異なる型を扱う同名のメソッドを定義すれば、それは正しく多態性になる訳だから。

このように型によって緩やかな結合を取ることがGoの一つの大きな特徴であるように思います

P.S. (2023/1/25)

interfaceの型名は~~er(~~するもの)とするのが通例のようですから、命名規則からは外れています。

 

admin

 

値を変更するならポインター渡し(Go言語)

https://isehara-3lv.sakura.ne.jp/blog/2023/01/17/goは値渡し言語、/

で、Goは値渡しなので、渡し先で値を変更するならポインター渡しと言っていますがその例です。

以下のソースで関数printPersonにポインターを渡した時はjohnの値が変更されていますが、値渡しにすると元の値は変更されません。

package main

import (
	"fmt"
)

type Person struct{
	LastName string
	FirstName string
	Age int
}

func (p Person) printPerson(p1 *Person) Person{
	p1.Age = 72
	fmt.Println("John : ", p1.FirstName, p1.LastName, p1.Age)
	return *p1
}

func main() {
	var john Person
	john.FirstName = "John"
	john.LastName = "Wein"
	john .Age = 70
	john.printPerson(&john)
	fmt.Println("returned age : ", john.Age)
}

以下はポインタ渡しと値渡しでの実行結果です、確かに値渡しではオリジナルの情報は更新されません。

<ポインター渡し>
John :  John Wein 72
returned age :  72

<値渡し>
John : John Wein 72
returned age : 70

 

admin

 

 

Goは値渡し言語、

タイトルの通りですがGo言語は値渡し、例えば関数に引き渡す引数は値が引数のアドレスとは違う場所の関数内で使用する変数のアドレスにコピーされて使われます。

これでは都合悪い時もあるだろうから、参照渡しのためにポインターが存在知ると考えれば良いかもしれない。ポインターはC/C++の記法そのままで、

package main

import "fmt"

func main() {
	val := 20
	pointer := &val
	*pointer = 23
	fmt.Println("address : ", pointer, "value : ", val)
}


address :  0x14000126008 value :  23

のように使います、しかしGoでポインターを普段使いしたら、変数がmutableになることでコードの見通しが悪くなるから使うのは限定的だろうと思う。Goでは通常の変数はすべてimmutable扱いだから、逆に言えばポインター宣言は明示的なmutable宣言であるということです。

例にあるようなケースでわざわざポインターを使う必要性はないし、こんな使い方はしちゃいけない例です。数少ない例外はインターフェースを受け取るだけだとは『初めてのGo言語』の記載。

 

admin

 

 

 

Goの並行処理と並列処理、

ダラダラと継続中ですが、とりあえずの現状のまとめです。

<条件>

M1 MacBook air

1000万までの素数計算してスライスに格納、昇順は考慮してない

 

<言えること>

・goはシングルスレッドでもマルチコアを使うように(並列処理するように)動く、これは便利だ

・channelのオーバーヘッドはmutexよりも大きい予想通りですが、ただしmutexのように個別の変数に対する考慮は不要だからエラーの入りにくいのがchannel

・コア数を強制的にしてしてやるとこのコードの場合には4コアぐらいが最適、もちろん状況により変わりますが

それぞれのソースコードはこちら、

https://github.com/chateight/go/tree/master/concpara

 

 

admin