struct{}とstruct{}{}(Golang)

struct{}は型(type)を表現しているし、struct{}{}はstruct{}のインスタンス、従って変数の宣言時には型表現としてのstruct{}を使い、変数への代入時には値(インスタンス)としてのstruct{}{}を使わないといけません。

var empt struct{} 
empv := struct{}{}

考えてみればそうですが、混乱しやすいかもしれない

 

admin

doneパターン(Golang)

Goでの特徴的な機能の一つであるgoルーチンですが、その終了判定あるいはチャネルの選択時の一つのパターンがdoneパターンと言われるもの。

コードのベースは以下からですが、

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

実はチャネルサイズが10なので、以下のgoルーチンのループでは11以降はチャネルへの書き込みができないので待ちになります。実質はmainルーチンが終了するとgoルーチンも終了されるので処理上の問題はないのですが、

ここでgoルーチン側で終了待ちをするためのdoneパターンを導入してみます。

close(done)がgoルーチンへの終了シグナルになりますが、goルーチン側ではselect/caseを使っています。チャネルの処理が継続できる条件が複数あるときに、継続可能となった処理をselect/caseで選択して実行するものです。select/case文の特徴的なところは、複数の条件が成立しているときにはランダムで継続可能処理を選ぶことでswitch文のように上から順番のような優先順位はつけないということです。

func main() {
	ch := make(chan int)
	var result []int

	done := make(chan struct{})

	go func() {  // 処理してもらう数値をchに入れる
		for i := 0; i < 100; i++ {
			select {
			case <- done:
				fmt.Print(". ")
				return
			case ch <- i:
			}
		}
	}()
	
	result = processChannel(ch)
	close(done)
	
	time.Sleep(1000)
	fmt.Printf("result: %d\n", result)
}

このサンプルでは確実にdone処理を実行するためにmainルーチンで1000nsの待ち時間を入れています。

実行すると、例えば以下のようになります。

ゴルーチン 起動完了
process: 0 0 0
process: 9 81 0
process: 5 25 2
process: 6 36 2
process: 4 16 0
process: 7 49 1
process: 1 1 2
process: 8 64 2
process: 2 4 1
process: 3 9 1
. result: [0 81 16 9 4 49 64 25 1 36]

 

admin

 

Stringer interface(Golang)

今更ですが、fmtパッケージにはStringerインターフェースが定義されているので、StringerインターフェースのメソッドString()を独自に実装すると、実装したメソッドが有効となるのでその書式でPrintされます。

以下はGlangのAPIからのコードですが、String()を実装すればその書式になるし、未実装ならば構造体のままでプリントされています。

package main

import (
	"fmt"
)

// Animal has a Name and an Age to represent an animal.
type Animal struct {
	Name string
	Age  uint
}

// String makes Animal satisfy the Stringer interface.
func (a Animal) String() string {
	return fmt.Sprintf("%v (%d)", a.Name, a.Age)
}

func main() {
	a := Animal{
		Name: "Gopher",
		Age:  2,
	}
	fmt.Println(a)
}
<String() string実装>
Gopher (2)

<デフォルト>
{Gopher 2}

多少事情は異なりますがtime.Timeでは、time.Timeが独自のString()メソッドを実装しているから意図した通りにはならず独自のStringメソッドが補完してくれるようです。

https://tutuz-tech.hatenablog.com/entry/2019/11/28/091036

 

admin

 

関数に可変長引数をスライスで渡すとき(Golang)

Goで可変引数を渡すときにの記述方法についてです。

可変長引数は関数で内部的にはスライスに変換されるのでスライスで渡すこともできますが、関数側は”v1(names ...string)“のように受け取りますが、呼び出し側は”v1(names...)“のように…が関数とは逆の位置に来ますよということです。

/*
Variadic functions in Golang
*/ package main import ( "fmt" ) func v1(names ...string) { // pack operator used before type in argument fmt.Println(names) } func main() { names := []string{"Albert", "Issac"} v1("John", "Jane", "Dexter", "Bruce") // [John Jane Dexter Bruce] // Here is unpack operator in action v1(names...) // [Albert Issac] name1 := []string{"John", "F.", "Kennedy"} v1(name1...) }

決まり事ではありますが、論理的に考えれば受け取り側は型を指定していて、呼び出し側は変数名(スライス名)を指定しているから合理的とは言えます。

 

admin

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