DI(Dependency Injection) — (@Golang)

DIはコードをinterfaceを介することでソースコード間の依存性を少なくして、例えばDBの接続種類の入れ替えとかテストの時にスタブに切り替えるとかをコードの修正を最低限で実行できる方法です。結果としてコードの保守性が向上するということになります。

https://free-engineer.life/golang-dependency-injection/

の記事がわかりやすかったのでこれを事例にしてみます。

実際にはVScodeで動かしてみて、パッケージが全てmainでは気持ちが悪いのでパッケージ名は分離しています。以下パッケージ分離版のコードです。

https://github.com/chateight/golang/tree/master/basic/di

<task_usecase(use case.go)のDI実現のキーコード部分>

// Saveメソッドの実装はTaskRepositoryで行っている
type TaskRepositoryInterface interface {
	Save(Task) (Task, error)
}
// structのフィールドにTaskRepositoryInterfaceを持たせる
// こうすることで、CreateTaskメソッドでDBへの保存処理を呼ぶことができる
type TaskUsecase struct {
	repo TaskRepositoryInterface
}

~~途中省略~~

func (usecase TaskUsecase) CreateTask(title string) (task.Task, error) {
	t := task.Task{Title: title}
	task, err := usecase.repo.Save(t)
	return task, err
}

つまりinterfaceを経由した抽象的な呼び出しにより依存先をinterfaceとすることで、CreateTask中ではinterfaceを呼び出し、結果として実装(ここではsave())と分離がされているので、例えば他の実装との入れ替えが簡単にできるということになります。

Golangの場合には明示的にinterfaceをimplementで明示しなくとも、interfaceで定義した関数のフットプリントと同じ関数が実装されていればimplementされたことになるのでDIの記述のようなケースでも、他の例えばJavaなどの比較すると綺麗にコードが分離できます。Golangはクラシックなオブジェクト指向では無いですが、現代的な抽象化手段を持つ言語です。

 

admin

クロージャー(Golang and Rust)

現代の言語ではクロージャー機能を多くの言語で持っていますが、同じ機能(単純な1の加算)をGolangとRustで実装の比較。Rustの説明で出てくる、クロージャーは環境をキャプチャーできる匿名関数という定義はわかりやすい。

<Golang>

package main

import "fmt"

func add(i int) func() int {
	n := i

	clo := func() int {
		n++
		return n
	}

	return clo		// equivalent to specify anonymous function here and maybe it's more popular
}

func main() {
	a := add(1)
	for i := 1; i < 10; i++ {
		fmt.Println(a())
	}
}

<Rust>

fn f1(i: i32) -> Box<dyn FnMut -> i32> {	// FnMut recieves &mut self
	let mut i = i;						// dyn keyword sepcifies a trait pointer in the heap area
	Box::new(move || {				// force to allocate values in the heap area
		i += 1;
		i
	})
 }
 
 fn main() {
	 let mut cup = f1(0);
	 for _ in 1..10 {
		 println!("{}", cup());
	 }
 }

コードの参考は、

https://scrapbox.io/takker/Rustで関数を返す関数を作

FnMutの解説は、

https://qiita.com/hiratasa/items/c1735dc4c7c78b0b55e9

Fnでは変更できないし、FnOnceでは一度しか呼べないのでFnMutの指定になります。

比較してみると、Rustの方が細かな指定が必要ですが、これはメモリ管理に関する指定を明示的に行わなければいけない言語だからでしょう。したがってこれぐらいの例では変わらないけれども、コードは長めになります。比較してGolangは細かな操作はしなくとも使うだけなら簡単ということになるでしょうか。

Box<dyn … >については、

Boxは変数をスタック領域ではなくヒープ領域への割り当て、dynについては以下の通り、

https://doc.rust-lang.org/stable/rust-by-example/trait/dyn.html

“Rust tries to be as explicit as possible whenever it allocates memory on the heap. So if your function returns a pointer-to-trait-on-heap in this way, you need to write the return type with the dynkeyword

 

admin

 

構造体

GolangやRustでは積極的に構造体を使い、また構造体を効率的に使うための言語仕様も用意されていますが、その背景は必要なデータセットは構造体にまとめることで、読みやすく従ってバグも入りづらいコードを書くことにあるだろうと思います。

同じ機能をGolangとRustで記述してみます。

<Golang>

type Rectangle struct {
  length  float64
  breadth float64
}

func (r Rectangle) area() float64 {
  return r.length * r.breadth
}

<Rust>

struct Rectangle {
    length: f64,
    breadt: f64,
  }
impl  Rectangle{
  fn area(&self) -> f64 {
    return &self.length * &self.breadt
  }
}

Rust呼び出し方、

  fn main() {
    let mut rec = Rectangle{length:0.0,breadt:0.0};

    rec.length = 20.0;
    rec.breadt = 30.0;

    print!("{}", rec.area())
}

Golangでは

(r Rectangle)

を使って構造体と結びつけ、Rustでは

impl Rectangle

でインターフェースの如くimplementでメソッドを定義していますが、上記二種の結果は全く同じ出力をするし、記法にも大差ありません。c++ではこのような記法はないので、現代の言語の特徴と言ってもいいのではないかと思います。

 

admin

Goのアップデート

すでに2月に1.20になっていますが今さら。homebrewでインストールしているので、

% brew upgrade go

で1.19 -> 1.20へ、

==> Running `brew cleanup go`…

で1.19は関連ファイルが削除されます。

$GOROOTを確認すると、

% go env GOROOT
/opt/homebrew/Cellar/go/1.20.3/libexec

1.20にupgradeされました。$GOROOTはこのように自動的に更新されるので、直接手を入れる必要はないようです。以上はcuiモードで有効で、VScodeだと、以下のGo xxをクリックすれば、インストール済みの版数の選択あるいは新規ダウンロードができます。

 

admin

ラズパイ用チャットアプリ再構成

チャット機能は以下の記事の通り独立モジュールとして開発していましたが、

https://isehara-3lv.sakura.ne.jp/blog/2023/04/28/ラズパイにチャット機能を追加/

mifareカードアプリと分ける必要もなくなったので統合しました。ソースコードのリンクは以下になります。

https://github.com/chateight/myfare-copy

修正箇所は、router.goの以下の2行と、myfareのブラウザ画面からのリンクボタン追加とmain.goからの呼び出し(以下のコード切り出し)の追加。

r.Static(“/static”, “./go_chat/view/static”)
r.LoadHTMLGlob(“./go_chat/view/*.html”)
Static/LoadHTMLGlob()のリンクディレクトリ呼び出しは、Run()を起動したmain.goが基準になるのでそこからの相対ディレクトリに変更、関連処理のmain.goからの呼び出しを移動しただけです。
<main.go部分>
func main() {
	// to call card reader function()
	go uidSerial.SerialMain()

	// chat data base create and make table to store chat messages
	sql_db.DbCreate()
	// start chat service
	go chat.Run()
	
	// start myfare card service
	wevServer()
}

 

admin

GolangでHTML escape処理

HTML escapeはほとんどのケースで入口、つまりHTMLでFORM入力された文字列のチェック、でのJavaScriptの仕事です。もしGolangがフロントエンドの役割を果たす時にはhtmlパッケージにある、

func EscapeString(s string) string

関数が使えます。もしunescapeするならば、

func UnescapeString(s string) string

になります。docのリンクは、

https://pkg.go.dev/html

 

admin

 

ラズパイにチャット機能を追加

ラズベリーパイでMyfareカードのアプリを動かすのと同時にWi-Fi内でのチャット機能も追加しました。

https://www.cetus-media.info/article/2021/line-chat/

雛形は上のリンクですが、いくつかカスタマイズしています。

一番大きな部分は、チャット内容をSQLite3に保存して、新たな参加者が入ったらそれまでの履歴を表示するようにしたことでしょうか。

普通に提供されているHTML renderingはテキストしか対応出来ないので、タグが使えるようにginのWriter.WriteStringでHTMLページを組み立ててroom.htmlリクエストに対応しています。

<router.goの該当部分抜き出し>

	r.GET("/room/:name", func(c *gin.Context) {
		chattername := c.Query("name")
		pmsg := chatbuild(chattername)
		c.Writer.WriteString(pmsg)		// to genrate initial chat page instead of the static html

全部のソースは、

https://github.com/chateight/golang/tree/master/go_chat

になります。ginとmelody使っているので、制御そのものはシンプルに出来ています。

画面はこんな感じです、ブラウザはsafariはJavascriptのサポートレベルがイマイチ(チャット画面に移動時、投稿の最新にスクロールしない)なのでchromeがおすすめです。

 

admin

Goのビルド時間を条件同じにして計測してみた

以下の記事で、Intel MacとM1 Macでビルド時間が違うことに気づいたので、条件を同じにして比較。

https://isehara-3lv.sakura.ne.jp/blog/2023/04/23/dockerコンテナからイメージを作成する/

<時間計測の条件>

一度ビルドを完了した状態で、実行ファイルを削除して再度ビルドに要する時間を計測(実際行っていることはリンケージですね)

スクリプトファイルで、

% go build main.go

の前後のタイムスタンプで計測

M1 Mac native build time -> 2 seconds

 % ./build.sh 
2023年 4月24日 月曜日 13時12分41秒 JST
2023年 4月24日 月曜日 13時12分43秒 JST


M1 Mac Docker container build time -> 11 seconds

# ./build.sh
Mon Apr 24 05:00:53 BST 2023
Mon Apr 24 05:01:04 BST 2023


Intel mac Docker container build time -> 61 seconds

# ./build.sh
Mon Apr 24 04:18:53 BST 2023
Mon Apr 24 04:19:54 BST 2023


Raspberry Pi model B+ native build time -> 118 seconds

$ ./build.sh
Mon 24 Apr 13:05:53 JST 2023
Mon 24 Apr 13:07:51 JST 2023

 

Intel/M1 Macは他のタスクではほぼ同じパーフォーマンスなので、Intel CPUではarm CPUのエミュレーションが明らかに速度が遅いようで、M1 MacでのDockerだけがクロスビルドの許容内、規模にもよりますが。

 

admin

DockerでラズパイのGolang build環境を作る

https://isehara-3lv.sakura.ne.jp/blog/2023/04/19/golangアプリは単純にクロスビルドしても動かない(db/

の対応としてとりあえずラズパイ自身でbuildさせましたが、ラズパイModel B+でbuildに必要な時間は、実行ファイルのタイムスタンプからおよそ二時間。これでは実用的ではないので代替え方法を考えるけれども、一番楽そうなのはDockerを使うことでしょう。

自分の環境を汚染させないとか、イメージの配布とかではなく、ラズパイの環境を作るための使用(DockerはM1 Macにインストール、ターゲットのコンテナはraspbian32 OS)です。

多少以前の記事になりますが、以下を参考に実行。raspbianイメージとかは最新版、と言っても2020年度が最新ですが。

https://www.koatech.info/blog/raspbian-on-docker/

・イメージ取得と作成

% wget http://ftp.jaist.ac.jp/pub/raspberrypi/raspios_lite_armhf/root.tar.xz

% docker image import ./root.tar.xz raspbian-stretch-lite:2020

・以下のコマンドでwarningが出る、とりあえずは無視して大丈夫な様子(QEMUは元々Intel CPU用だからか、Apple siliconでも今のところ何とかなってるけど)

% docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested

exec /register: exec format error

・コンテナ作成と起動(名前指定)

% docker run -it --name rasp32 raspbian-stretch-lite:2020 bash

・作成しているコンテナを起動するなら

% docker exec -it rasp32 bash

・Golang install

# wget https://golang.org/dl/go1.20.1.linux-armv6l.tar.gz

# sudo tar -C /usr/local -xzf go1.20.1.linux-armv6l.tar.gz

# /usr/local/go/bin/go version (パスが通っていない状態)

・Dockerのコンテナからファイルをクライアントにコピー(例)

% docker cp 21f2c6ce6eb8:/home/pi/hello .

・ディレクトリ指定すればその中全部をコピー(クライアントからコンテナへ)

% docker cp myfare 21f2c6ce6eb8:/home/pi/

コンテナ上のMacの該当ディレクトリを丸ごとコピー(gomodなど含めて)してきてbuild、できた実行ファイルをscpでラズパイに転送して起動するとちゃんと実行できました。実行ファイルの動作確認、最初は定番のハローワールドで確認しています。

エミュレーション(QEMU)なので、速度はMacでbuildするのに比較すると遥かに遅い、それでも二分ぐらいで終わっているからやはりラズパイで実行するよりはほぼ百倍速いから実用的です。

実行ファイルのサイズが微妙に違うのはライブラリの版数違いか、main1のほうがコンテナでbuildしたもの。

コンテナ上のraspbian、

ラズパイのbuild環境としてはおそらくデフォルトだと思う。カスタマイズしたコンテナを作る時にDcokerfileを記述しておけば、再現性確保できます。

 

admin

Golangアプリは単純にクロスビルドしても動かない(DBドライバがcgo使ってる)

Macで開発したアプリをラズパイで動かそうとしましたが、そのままでは動かない。なぜならgormもdbドライバーもcgoを使っている、つまりターゲットのgccを用意してそれを指定しないといけないから。

とりあえず動かすだけなら、すごく時間はかかりますがラズパイでビルド、2時間ぐらい放置してたらビルド完了してました。

実行ファイルを起動すると、Macよりは多少レスポンスは遅いのですがちゃんと動作しています。

<layout.html>

これだけはws://mbair.local:8080/wsをラズパイに変更が必要です。

 window.onload = function () {
  socket = new WebSocket("ws://mbair.local:8080/ws");
  socket.onopen = function () {
    append_message("system", "Socket Connected");
  };
  socket.onmessage = function (event) {
    append_message("server", event.data);
  };

  const send = function (){
    socket.send("")
  }
  setInterval(send, 500);

};

クロス環境をどうするかですが、Dockerがおすすめのようなので、それでやってみます。

メモリ使用状況は、こんな感じです、クライアント一台だけで、

すぐにもう一台増やすと、およそ200KBぐらい増えていますが、この程度では普通には十分です。

 

admin