値を変更するならポインター渡し(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

Goの並行処理でchannelを使う

https://isehara-3lv.sakura.ne.jp/blog/2023/01/12/goの並行処理/

ではWaitGroup使って終了を判定しましたが、Go独自の機能にスレッド間での通信機能channelを使う方法もあります。どちらが良いというのではなくて、目的によって使い分けするんだと思いますが。

以下はリンクの処理と同じく一千万までの素数を求める処理です。40行目付近から下のループ処理でchannelを使って素数をスライスに格納しています。channelが存在しないことはタイマーの一秒待ちで判定しています。

package main

import (
    "sync"
	"time"
	"fmt"
)

//
// to use channel instead of "WaitGroup"
//
// in this case, slower than WaitGroup. Channel may be useful when it takes long processing time and less inforamtion size
//
func main() {
	maxNumber := 1000*10000
	var mu sync.Mutex
	ch := make(chan int)
	defer close(ch)
	c := 2
	oddCh := []int{}
	tStart := time.Now()
	tStop := time.Now()
	for i := 2; i <= maxNumber; i++ {
		go func() {
			flag := true		// if odd number, stay "true"
			mu.Lock()
			defer mu.Unlock()
			for j :=2; j*j <= c ; j++ {
				if c%j == 0{
					flag = false
					break
				}
			}
			if flag == true{
				ch <- c
			}			
			c++
		}()
	}
	for i:= 0; i < maxNumber; i++{				// set odd numbers to the slice
		setBreak := false
		select {
		case p := <- ch:
			oddCh = append(oddCh, p)
			tStop = time.Now()
		case <- time.After(time.Second):		// to check last "ch" data
			fmt.Println("Time Out!")
			setBreak = true
		}
		if setBreak == true{
			break
		}
	}
	
	fmt.Println("len : ", len(oddCh))

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

実はWaitGroupを使った場合に比較して、処理時間は5倍ぐらいになっています。おそらく渡しているのが素数というint形式データを渡しているだけでオーバーヘッドが大きい。したがってchannelを使うのに細かなデータをやり取りするには向かないんだろうと思います。

いずれにしろこのような簡単な処理では、goroutineも使わずにgoの実行環境でマルチコアを使う方が一番高速なのだから、余計なことをやらないのがいちばんのようです。

 

admin

 

 

Goの並行処理

Goの並行処理は、関数の前にGoを入れるだけで処理対象になります。リソースの排他処理が必要なときには他の言語と同様なmutexを使う、あるいはチャネルを使っても良さそうです。

<素数を求めるコード>

package main

import (
    "sync"
	"time"
	"fmt"
)

func main() {
	var wg sync.WaitGroup
	var mu sync.Mutex
	c := 2
	odd := []int{}
	tStart := time.Now()
	for i := 2; i <= 10000*1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			flag := true		// if odd number, stay "true"
			mu.Lock()
			defer mu.Unlock()
			for j :=2; j*j <= c ; j++ {
				if c%j == 0{
					flag = false
					break
				}
			}
			if flag == true{
				odd = append(odd, c)
			}			
			c++
		}()
	}
	wg.Wait()
	tStop := time.Now()
	fmt.Println(len(odd))

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

mutexを宣言する場所はこのケースではc++の直前でも良さそうなのですが、この位置じゃないとちゃんと結果が出ません。(TBD)

ちなみにC++との実行速度の比較、

千万までの素数計算をM1 Macでさせると、

C++ : およそ1.5秒、Go : およそ6秒と四倍程度遅い。スクリプト言語とは比べようもなく速いのですが。

しかしGoで並行処理をやめて(関数の先頭にgoを付けない)シングルスレッドにすると1.3秒程度で処理完了するから、並行処理にするとオーバーヘッドの分遅くなるだけです。実はGoはシングルスレッド(ネーティブで)でもマルチコアで並列処理を実行するようですね。

—————————————————

さらに以下のコードをM1 MacとRaspberry PI B+(700MHz single core)で実行速度を比較(実行速度向上の点からはほぼ無意味な並行処理)すると、

おおよそラズパイは1/150の実行速度、

 

 

admin

 

 

 

Goの標準のwebサーバー機能(@RaspberryPI)

Goの標準のライブラリにnet/httpというのがあって、これを使うとwebサーバーが簡単に立ち上げできます。もちろん複雑なことをやるならば他の言語と同じようにフレームワーク(実は単にnet/httpのラッパーらしい)が必要となるのですが。

ともかくも、以下のコードだけでhttp://raspberrypi.local:4000で実行ファイルディレクトリのpubディレクトリにあるindex.htmlの静的ページを返します。別にラズパイ以外でも同じなのですが、実際に使うのはラズパイだろうからラズパイでやっています。

package main

import (
    "net/http"
)

func main() {
    fs := http.FileServer(http.Dir("pub"))
    http.Handle("/", fs)
    http.ListenAndServe(":4000", nil)
}

ブラウザからアクセスするとこんな感じです、スタイルは未指定。

今時の言語ではwebサーバーは特別に分離しないで言語と一体化が自然な流れになってきています。

 

admin

 

Golangでクロスビルド

Go言語の特徴の一つだと思いますが、クロス環境のバイナリを環境変数の指定で作成できること。

例えば
Raspberry PIのような決して早くはないハード用のバイナリをMacで作成するのは現実的だろうと思う。

<hello_go.go>

package main
import "fmt"
func main(){
	fmt.Println("hello Go", 2*3)
}

のシンプルなソースを環境変数指定でビルドします。Raspberry PIの環境変数は、

$ go env
GO111MODULE=""
GOARCH="arm"
GOBIN=""
GOCACHE="/home/pi/.cache/go-build"
GOENV="/home/pi/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="arm"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/pi/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/pi/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_arm"
GOVCS=""
GOVERSION="go1.19.4"
GCCGO="gccgo"
GOARM="6"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/pi/go_prj/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -marm -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1086455489=/tmp/go-build -gno-record-gcc-switches"

必要なのは、上のリストでGOOS=linux GOARCH=arm GOARM=6の部分、

ビルドを以下のコマンドで実施して、

$ GOOS=linux GOARCH=arm GOARM=6 go build hello_go.go

scpでRaspberry PIに転送でうまく実行できました、最初どこかのページでGOARM=7になっていたので、それで実行すると、

$ ./hello_go

Illegal instruction

と言われたので、ラズパイ環境からそのまま使用するのが間違いないでしょう。当然ラズパイでもバージョンで異なるはずですが、ここで使ったのはかなり初期の素のラズパイB+です。

ただし、ネーティブ環境よりはビルド時間遅くなっています。これはある意味当然かもしれませんが、それでもラズパイでビルドするよりはずっとマシだと思います。

 

admin

 

 

Go Lang

今時の言語のGoをしばらく触ってみます。とりあえずのゴールはラズパイでWebサーバー構築してM5Stackとやりとりすること。node.jsと同等の機能が実現できるはずで、コンパイラーであるだけ高速でしょう。

とりあえずM1 Macと初代に限りなく近いRaspberry PIにインストールしてみました。

— M1 Mac環境 —

<install>

% brew install go

するだけ、

% go version

go version go1.19.4 darwin/arm64

が今の最新版数のようです。

VScodeはGoマークの拡張機能を二つインストールしただけでソースコード編集ができます、追加で必要ならばインストールを要求されます。

 

<directory>

ユーザディレクトリ直下にgoという名前で作成されます。

 

<初めてのソース>

定番ですが、

package main
import "fmt"
func main(){
	fmt.Println("hello Go")
}

WordPressのアドオン古くてGo言語選択できないので近そうなC++にしています。

import “fmt”は他言語のimport “sys”のようなものでしょうか。

 

<module>

そこそこの規模になるとモジュール化が必要ですが、先ほどのソースディレクトリにモジュール用のディレクトリ作成してモジュールを作成します。

% mkdir mod
% cd mod
% go mod init mod 
% cp ../hello_go.go ./
% go mod tidy
% go build
% ./mod
hello Go

全体のディレクトリ構成は以下のようになります@VScode

通常の% go runでは実行ファイルはテンポラリに作成され実行後に削除されるようですが、実行ファイルを残すためには% go buildを使います。

このようにgoコマンドのパラメータ指定で作業をコントロールできるようになっています。

 

— RaspberryPI 環境 —

https://zenn.dev/ysmtegsr/articles/20d6e0c7159be2

を参考にインストール、

$ wget https://go.dev/dl/go1.19.4.linux-armv6l.tar.gz

$ sudo tar -C /usr/local -xzf go1.19.4.linux-armv6l.tar.gz

/usr/local/go/bin/に移動して、

$ ./go version
go version go1.19.4 linux/arm

まだパスが通っていない、

パスを通す、

$ echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
$ echo 'export PATH=$HOME/go/bin:$PATH' >> ~/.bashrc

$ source ~/.bashrc

Macと同じような簡単なソースファイル(main.go)をgo_prj配下に作る、

package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

$ go mod init go_prj

$ cd go_prj

$ tree ../go_prj
../go_prj
├── go.mod
├── main
└── main.go

$ go run

コンパイル実行だとRaspberryPIでは数秒待つから遅い

$ go build -o main

$ ./main

で実行ファイル呼び出すと高速(当然ですが)、この起動時間待ちの雰囲気はnode.jsの起動でも同じだからRaspberry PiではGoをbuildして使うのが普通になると思います。

 

Go言語の第一印象

随所に現代的(ビルド環境やソースコードの検証など、まだイントロしか読んでいないからこの程度)なところを見ることができます。既存の言語はバージョンアップでも過去との互換を考えないといけないわけですが、新たな言語はその時点で必要な機能を順位づけして言語仕様や開発環境を決めることが出来るわけですから。

 

admin