20211230のGoに関する記事は7件です。

Go!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go/gRPCコード生成の変更について

結構時間が経っているのですが、Go/gRPCでのサーバー開発において、2020年にコード生成の方法が変更されました。 最近gRPCを利用し始めた方は特に気にされる必要はありませんが、以前からGoでgRPCサーバーを構築していて 生成プラグインを入れ替えてないけどそろそろ更新しとくかーといった際に役立ちそうな情報として記事にしておきます。 以前の生成方法 GoでgRPCサーバーのコード生成をする際、 元はprotocプラグインとしてgolang/protobufを使用していました。 この際にはこのようなコマンドでコード生成を行なっていました。 $ protoc -I . \ --go_out=plugins=grpc:. --go_opt=paths=source_relative \ ./helloworld.proto このコードから生成される<Service>.pb.goに必要な全てのコードが入っていました。 protocプラグインの変更 2020年3月より、まず protocolbuffers/protobuf-goのv1.20.0のリリースとしてGoのシリアライズコード生成プラグイン(protoc-gen-go)が独立しました。 更に2020年10月にgrpc/grpc-goのcmd/protoc-gen-go v1.0.0でサーバースタブ生成プラグイン(protoc-gen-go-grpc)が独立しました。 そのため現在、.protファイルからgRPCサーバースタブを生成する際には下記のようなコマンドを打つことになります。 $ protoc -I . \ --go_out . \ --go_opt paths=source_relative \ --go-grpc_out . \ --go-grpc_opt paths=source_relative \ ./helloworld.proto 以前1ファイルにまとめられていたコードも<Service>.pb.goと<Service>_grpc.pb.goに分かれて生成されます。 下位互換性の無い変更 また、これに伴い以前のprotoc-gen-goで生成したスタブとの間に下位互換性を崩す変更が入っています。 gRPCスタブを生成するとサービス実装用の<Service>Serverインターフェイスが生成され、 またそのインターフェイスの前方互換性の為のUnimplemented<Service>Server構造体が生成されます。 type GreeterServer interface { SayHello(context.Context, *HelloRequest) (*HelloReply, error) } type UnimplementedGreeterServer struct { } func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") } これは<Service>Serverインターフェイスの実装構造体にUnimplemented<Service>Serverをembedすることで 実際にインターフェイスのメソッドを満たさなくても実装可能とする為の仕組みですが、全てのメソッドの実装後にembedを外してしまうと、サービスを追加した際などにエラーとなってしまいます。 こうした事例が多かったからなのか、 protoc-gen-go-grpcではmustEmbedUnimplemented<Service>Serverのようなプライベートメソッドがインターフェイスに追加されており、 これによりUnimplemented<Service>Serverのembedが強制されるようになりました。 type GreeterServer interface { SayHello(context.Context, *HelloRequest) (*HelloReply, error) mustEmbedUnimplementedGreeterServer() } func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {} 実際のコード このため、Unimplemented<Service>Serverを外すような実装をしている場合、protoc-gen-go-grpcでのコード生成に切り替えた途端にインターフェイス実装が崩れてしまうことになります。 この問題を回避する方法も用意されており、protoc-gen-go-grpcのReadmeに記載されています。 protoc-gen-go-grpcのオプションとしてrequire_unimplemented_servers=falseを指定することで mustEmbedUnimplemented<Service>Serverを生成しない下位互換性を維持したコード生成が可能になっています。 $ protoc -I . \ --go_out . \ --go_opt paths=source_relative \ --go-grpc_out . \ --go-grpc_opt paths=source_relative \ --go-grpc_opt require_unimplemented_servers=false \ ./helloworld.proto
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goのリリースアセットの配置を自動化したい

これを自動化したい GitHubのReleaseにクロスコンパイルで作成したアーカイブを配置(Assets) こんなかんじ Prometheus 方法 GitHub ActionsのパイプラインでOS・CPUアーキテクチャごとのアーカイブを作成し、リリースに配置します。 GitHub Actionsにアーカイブを作成するコマンドを書いてもいいのですが、自分はMakefileで書いてみました。 (GitLabを使うことが多いのでそちらに流用できるように) ./Makefile ./.github/workflow/release.yml Makefile ビルドやアーカイブファイルの作成用の処理 GO ?= go GOOS ?= $(shell $(GO) env GOOS) GOARCH ?= $(shell $(GO) env GOARCH) MODULE_NAME ?= $(shell cat go.mod | head -n1 | cut -f 2 -d ' ') all: build # パッケージのダウンロード .PHONY: get get: go mod download # ビルド (変数にOSやCPUアーキテクチャをいれてクロスコンパイル) # ビルド後のファイルの配置場所 : `./.build/<OS>-<CPUアーキテクチャ>/* .PHONY: build build: get mkdir -p .build/$(GOOS)-$(GOARCH)/ GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build if [ $(GOOS) = "windows" ] ; then \ mv ./$(MODULE_NAME).exe ./.build/$(GOOS)-$(GOARCH)/ ; \ else \ mv ./$(MODULE_NAME) ./.build/$(GOOS)-$(GOARCH)/ ; \ fi ; \ # アーカイブの作成 (必要なファイルを含んだアーカイブファイルをOS・CPUアーキテクチャを指定して作成) # アーカイブファイルの配置場所 : `./packages/<Gitのタグ>/* TAG ?= $(shell git tag | tail -n1) .PHONY: package package: mkdir -p ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) cp -r $(MODULE_NAME).service sh.yml commands Makefile LICENSE README.md \ ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) if [ $(GOOS) = "windows" ] ; then \ cp ./.build/$(GOOS)-$(GOARCH)/$(MODULE_NAME).exe ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \ else \ cp ./.build/$(GOOS)-$(GOARCH)/$(MODULE_NAME) ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \ fi cd ./packages/$(TAG) ; \ if [ $(GOOS) = "windows" ] ; then \ zip -r $(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH).zip ./$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \ else \ tar cvf $(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH).tar.gz ./$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \ fi ; \ rm -r ./$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) # 削除用 .PHONY: clean clean: -rm -r ./.build ./packages ./.build.env makeでビルド、make packageでアーカイブファイルの作成 GOOS=linux GOARCH=amd64などを指定してコンパイルするOS・CPUアーキテクチャの指定ができます。 packageの処理はアプリケーションごとに必要なファイルが変わってくると思うので各々書き換えてください。 この部分の1行目が必要なファイルを指定している箇所で、2行目に指定しているディレクトリにコピーされます。 cp -r $(MODULE_NAME).service sh.yml commands Makefile LICENSE README.md \ ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) Goが対応しているすべてのOS・CPUアーキテクチャでアーカイブファイルをそれぞれ作成する処理 # Goが対応しているOS・CPUアーキテクチャのリストからそれぞれビルドしてアーカイブファイルを作成 # アーカイブファイルの配置場所 : `./packages/<Gitのタグ>/* .PHONY: package-all-with-build package-all-with-build: get $(GO) tool dist list | grep 'aix\|darwin\|freebsd\|illumos\|linux\|netbsd\|openbsd\|windows' | while read line ; \ do \ printf GOOS= > ./.build.env ; \ echo $$line | cut -f 1 -d "/" >> ./.build.env ; \ printf GOARCH= >> ./.build.env ; \ echo $$line | cut -f 2 -d "/" >> ./.build.env ; \ . ./.build.env ; \ make build GOOS=$$GOOS GOARCH=$$GOARCH ; \ make package GOOS=$$GOOS GOARCH=$$GOARCH ; \ done rm ./.build.env go tool dist listコマンドでリストを取得し、シェルのwhlieループでそれぞれビルドとアーカイブファイルの作成をしています。 grepすることで、想定していないOS(例の場合はandroid, ios等)を除外し、特定のOSだけビルドするようにしています。 こんなかんじのを1行ずつ読んでループさせてます。 $ go tool dist list | grep 'aix\|darwin\|freebsd\|illumos\|linux\|netbsd\|openbsd\|windows' aix/ppc64 darwin/amd64 darwin/arm64 freebsd/386 freebsd/amd64 freebsd/arm freebsd/arm64 illumos/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mips linux/mips64 linux/mips64le linux/mipsle linux/ppc64 linux/ppc64le linux/riscv64 linux/s390x netbsd/386 netbsd/amd64 netbsd/arm netbsd/arm64 openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 openbsd/mips64 windows/386 windows/amd64 windows/arm windows/arm64 GitHub Actions workflow Makefileが書けたら次はGitHub Actionsのワークフローを書く。 name: "<任意の名前>" # `v*`に一致するタグがPushされたらパイプラインを実行 on: push: tags: - "v*" jobs: # Goのセットアップ setup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 # ビルド・アーカイブファイルの作成とリリースアセットのアップロード release: needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 # Makefileで書いたアーカイブファイルの作成処理を実行 - name: Build and package run: make package-all-with-build TAG=${GITHUB_REF##*/} - uses: actions/upload-artifact@master with: name: packages path: ./packages # 作成した`*.tar.gz`と`*.zip`をアップロード - run: | set -x assets=() for asset in ./packages/${GITHUB_REF##*/}/*.tar.gz ./packages/${GITHUB_REF##*/}/*.zip ; do assets+=("-a" "$asset") done tag_name="${GITHUB_REF##*/}" hub release create "${assets[@]}" -m "$tag_name" "$tag_name" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} これでGitHubでタグを作るとパイプラインが実行されてリリースページにアセットを配置できます。 あとがき シェルスクリプトを紹介してばかりになってしまった・・・難しい。 パッケージを作成するときの必要なファイルをコピーする部分だけ各々書き換えて貰えれば使えると思うので参考にしてもらえれば嬉しいです。 参考 【シェルスクリプト】ファイルの中身を一行ずつ読み込む方法 Makefile ことはじめ How to upload multiple assets? #28
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Go] slack-go/slackによるメッセージ一括削除

普段からGoogle CalendarとSlackを連携して使っていて、新規予定作成時にSlack通知がくるように設定しています。ところが、この通知が溜まってくると全体のメッセージ数を圧迫してきます。 そこでSlackAPIを用いて、指定したチャンネル・タイトルの投稿を一括削除することにしました。折角なので、楽にPythonではなく、勉強がてらGoを用いて実行してみることに。Slack APIの仕様変更もあり、あまり情報がなかったので、まとめてみました。 0.環境 Go: 1.16.3 slack-go/slack: 0.10.1 1.APIトークンの取得 以前使われていたトークンは「レガシートークン」と呼ばれているようです。新しいトークンはアプリを作成した上で、必要な権限を設定する必要があります。1 今回は、このように設定しました。 2.slack-go/slackのインポート タイトルにもあるようにslack-go/slackを使用しました。GoでのslackAPI操作を効率的に行うことができます。 今回は、下記パッケージをインポートしました。そして、案の定パッケージインポートでつまづきました。 import ( "flag" "fmt" "strings" "time" "github.com/slack-go/slack" ) 下記、サイトにて問題点を解決できました。問題点はモジュールモードとGOPATHモードでの扱い方を区別せずに取り組んでしまってことのようでした。 Golangのパッケージ完全に理解した - くろのて 2.クライアントの生成 slack-go/slackでは、Client構造体をトークンを用いて生成し、そこから操作を始めます。 今回は実行引数でトークンを渡すようにしました。 // Get access token from args flag.Parse() tkn := flag.Arg(0) api := slack.New(tkn) 3.ChannelIDの取得 まずは、削除したい投稿があるChannelIDをチャンネル名称から見つけ出します。引数として独自の構造体を渡しますが、この渡し方がスマートな方法なのかどうかは分かりません。 func check(e error) { if e != nil { panic(e) } } func get_calendar_channel(api *slack.Client) string { var result string calender_name := "calendar" // Target name of channel prm := slack.GetConversationsParameters{} channels, _, err := api.GetConversations(&prm) check(err) // Find channel for _, c := range channels { if strings.Contains(c.Name, calender_name) { result = c.ID } } return result } 4.メッセージの特定と削除 最後に、該当のChannelIDに対して該当するメッセージを特定し、DeleteAPIを呼びます。APIの呼び出し上限を超えてしまわないように1秒間のSleepを入れています。 chn_id := get_calendar_channel(api) // Get target channel id // Get message list prm := slack.GetConversationHistoryParameters{ChannelID: chn_id, Limit: 1000} res, err := api.GetConversationHistory(&prm) check(err) delete_title := "hogehoge" // Target title name to delete var cnt int for _, m := range res.Messages { for _, att := range m.Attachments { if att.Title == delete_title { _, _, err := api.DeleteMessage(chn_id, m.Timestamp) check(err) time.Sleep(1 * time.Second) // To escape the api limit cnt++ } } } fmt.Println(cnt, "messages were deleted") 処理プロセスとしては以上です。 躓いたポイント slack-go/slackのドキュメントには、各メソッドの説明がほぼない ver0.10なので仕方ない点ではありますが、今回用いた各メソッドの説明は最低限のみでした それぞれの引数の意味を知るためには、公式SlackAPIドキュメントから、slack-go/slackが呼ぶ先のAPI仕様を確認する必要がりました GetConversationHistoryParametersメソッドの上限は1000件 1000より大きい数値を実行すると、デフォルトの100件に戻されます 以上となります。Goは勝手にシンプルなコードにならざるを得ない点が気持ち良いですね。全体コードはこちらに公開しています。 参考: https://risaki-masa.com/how-to-get-api-token-in-slack/ ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go(Gorm)+MySQLなDocker環境でdial tcp connection refusedが発生した際の対処法

エラー内容 docker-compose up時に以下のエラーが発生しました。 [error] failed to initialize database, got error dial tcp 172.19.0.2:3306: connect: connection refused 原因 docker-composeでcontainer_nameを指定していなかったため 対処法 Before docker-compose.yml version: "3.9" services: backend: build: . ports: - 8000:8000 volumes: - .:/app depends_on: - db db: image: mysql:5.7.22 restart: always environment: MYSQL_DATABASE: ambassador MYSQL_USER: root MYSQL_PASSWORD: root MYSQL_ROOT_PASSWORD: root volumes: - .dbdata:/var/lib/mysql ports: - 33066:3306 main.go var DB *gorm.DB func Connect() { var err error DB, err = gorm.Open(mysql.Open("root:root@tcp(db:3306)/ambassador"), &gorm.Config{}) if err != nil { panic("Could not connect with database!") } } After docker-compose.yml container_name: godockerDBを追加 version: "3.9" services: backend: build: . ports: - 8000:8000 volumes: - .:/app depends_on: - db db: image: mysql:5.7.22 container_name: godockerDB # 追加 restart: always environment: MYSQL_DATABASE: ambassador MYSQL_USER: root MYSQL_PASSWORD: root MYSQL_ROOT_PASSWORD: root volumes: - .dbdata:/var/lib/mysql ports: - 33066:3306 main.go godockerDBを指定 var DB *gorm.DB func Connect() { var err error # container_nameを指定 DB, err = gorm.Open(mysql.Open("root:root@tcp(godockerDB)/ambassador"), &gorm.Config{}) if err != nil { panic("Could not connect with database!") } } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goroutineでバックグランドタスク実行する例

背景 以下のことをgoroutineで実現したいです。つまり、メインスレッドでFibonacci計算ではなく、goroutineでバックグランド計算したいです。 例1 func main() { endChannel := make(chan int) go backgroundTask(endChannel) <- endChannel // main goroutineを終了させるまで、endChannelの補填を待ち } func backgroundTask(endChannel chan int) { fmt.Println("background running...") fmt.Println("result: ",calculate(45)) endChannel <- 1 } func calculate(n int) int{ if n < 2 { return n } return calculate(n - 1) + calculate(n - 2) } 例2 func main() { endChannel := make(chan int) go backgroundTask(endChannel) for _ = range endChannel { } } func backgroundTask(endChannel chan int) { fmt.Println("background running...") fmt.Println("result: ",calculate(45)) endChannel <- 1 close(endChannel) } func calculate(n int) int{ if n < 2 { return n } return calculate(n - 1) + calculate(n - 2) } 例3 func main() { endChannel := make(chan int) go backgroundTask(endChannel) for { _,ok := <- endChannel if !ok { break } } } func backgroundTask(endChannel chan int) { fmt.Println("background running...") fmt.Println("result: ",calculate(45)) endChannel <- 1 close(endChannel) } func calculate(n int) int{ if n < 2 { return n } return calculate(n - 1) + calculate(n - 2) } 例4 func main() { endChannel := make(chan int) go backgroundTask(endChannel) for { select { case <- endChannel: return } } } func backgroundTask(endChannel chan int) { fmt.Println("background running...") fmt.Println("result: ",calculate(45)) endChannel <- 1 //close(endChannel) // case<-endChannelの処理はfor分を終了してあるから、closeしなくても良いです。 } func calculate(n int) int{ if n < 2 { return n } return calculate(n - 1) + calculate(n - 2) } 例5 func main() { wg := sync.WaitGroup{} wg.Add(1) go backgroundTask(&wg) wg.Wait() } func backgroundTask(wg *sync.WaitGroup) { fmt.Println("background running...") fmt.Println("result: ",calculate(45)) wg.Done() } func calculate(n int) int{ if n < 2 { return n } return calculate(n - 1) + calculate(n - 2) } まとめ バックグランドで動いているgoroutineをメインgoroutineから待つ必要があるため、専用なendChannelを用意するか、sync.WaitGroupでコントロールするかという二つの案があります。 さらに、endChannelをメインgoroutineから待つ方法はfor文、for range文、<- endChannel、for select分、という四つの案があります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パッケージ名を指定してgo buildしよう

はじめに フューチャー Advent Calendar 2021の7日目です。 本記事では go build main.go と go build {パッケージ名} としてビルドしたときの微妙な違いについて説明します。Goのバージョンは go1.18beta1 で確認しています。 Go 1.18から runtime/debug パッケージの ReadBuildInfo() を使って、ビルドしたときのVCSのハッシュを取得できるようになりますが、このビルドしたときの微妙な違いが影響するようになります。1 https://tip.golang.org/doc/go1.18#go-command go build の引数による違い go build でGoのソースをビルドして、実行バイナリが生成されることはよく知られています。 Usageにあるように go build [-o output] [build flags] [packages] パッケージ名 (packages) を指定してビルドしますが、Goの1つあるいは複数のソースファイルを go build の引数に渡すこともできます。go build a.go b.go c.go といったものです。 簡単のために以下のようなディレクトリ、ファイル構成を考えてみます。 ディレクトリ構成 . ├── go.mod └── main.go go.mod module sample go 1.18 main.go package main import ( "fmt" ) func main() { fmt.Println("Hello, 世界") } go build ${パッケージ名} としてビルドしたとき go build ${パッケージ名} としてビルドします。本例ではパッケージ名は sample です。 go version -m sample で確認すると以下のようになります。path の値が sample のパッケージ名になっており、また mod の値として sample のパッケージ名が含まれていることが分かります。VCS(ここではGit)のハッシュが含まれていることも分かります。便利ですね。 $ go build sample $ go version -m sample sample: go1.18beta1 path sample mod sample (devel) build -compiler=gc build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=amd64 build GOOS=linux build GOAMD64=v1 build vcs=git build vcs.revision=sample0123456789012345678901234567sample build vcs.time=2021-12-27T04:24:55Z build vcs.modified=true go build main.go としてビルドしたとき go build main.go としてGoのソースファイルを指定してビルドするとコマンドライン引数(command-line-arguments)としてビルドされます。 go version -m main で確認すると以下のようになります。path が command-line-arguments となっていることが分かります。mod の値はありません。VCSのハッシュも含まれていません。 $ go build main.go $ go version -m main main: go1.18beta1 path command-line-arguments build -compiler=gc build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=amd64 build GOOS=linux build GOAMD64=v1 より詳細を知りたい方へ Goの標準ライブラリも多くはGoで書かれています。上記の go build コマンドへの引数による違いは cmd/go/internal/work/build.go や cmd/go/internal/load/pkg.go のソースを読んでみると参考になります。cmd/go/internal/load/pkg.go の中で .go のファイルが引数として渡されたときは引数がコマンドライン引数として渡されたものとしてフラグ(pkg.Internal.CmdlineFiles)がセットされます。 cmd/go/internal/load/pkg.go func GoFilesPackage(ctx context.Context, opts PackageOpts, gofiles []string) *Package { // ... bp, err := ctxt.ImportDir(dir, 0) pkg := new(Package) pkg.Internal.Local = true pkg.Internal.CmdlineFiles = true pkg.load(ctx, opts, "command-line-arguments", &stk, nil, bp, err) if !cfg.ModulesEnabled { pkg.Internal.LocalPrefix = dirToImportPath(dir) } pkg.ImportPath = "command-line-arguments" pkg.Target = "" pkg.Match = gofiles // ... } まとめ go build の引数にファイル名を渡すか、パッケージ名を渡すかで、ビルドの方法が異なります。 ファイル名を渡してビルドした場合は、VCSのハッシュがビルドに含まれません。 本挙動は2021/12/30現在Go 1.18のドラフトリリースノートからは確認できませんでした。もしかしたら今後追記されるかも知れません。 ビルドするときはパッケージ名を指定してビルドしましょう。 go version -m でも確認できます。https://pkg.go.dev/cmd/go#hdr-Print_Go_version ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む