概要
素人としてこの1ヶ月間Golangでアプリケーション作ることになりましたので、所感や実際調べたことについて共有し、少しでも参考になれれば幸いです。
とりあえず現時点でのディレクトリ構成を貼ります
左がroot
ディレクトリで右がsrc
ディレクトリ配下です。
src
というディレクトリを切るかどうかで結構悩んでいましたが、とりあえず切ることにしました。
IDE、開発環境、実行環境
IDEは下記の理由でVSCodeにしました。
- VSCodeにはGolang開発チームが提供している(元々はMicrosoftがメンテしてたが、Golang開発チームに移管された)pluginがあるので、実質VSCodeがstandardらしい
- JetBrainsのGoLandというのがあるが、有料なので、今回はスキップ(30日間無料トライアルはある)
- Intellij Ultimate使う場合はaws toolkitもあり
Docker化
Golangアプリケーションは最終的にはLinuxのELF形式のバイナリ1個にまとまりますので、気軽にdocker化できます。
Golangアプリケーションを動かすためのdocker imageを探した結果、googleがメンテしているDistrolessイメージ群を発見し、良さそうでしたので、今回使うことにしました。参考サイト
そして、今回はDockerのmulti stage buildを使って一つのDockfileにてbuildとアプリケーションの実行まで書いてシンプルな構成にしました。
FROM golang:1.17.3-bullseye as builder ## -- buildするイメージ
COPY src/ /go/src/
WORKDIR /go/src
RUN GOOS=linux GOARCH=amd64 go build -o hoge main.go
FROM gcr.io/distroless/base:latest ## -- アプリケーション実行するイメージ & 最終的に出来上がるイメージ
COPY --from=builder /go/src/hoge /
ENTRYPOINT [ "/hoge" ]
パッケージ構成、アーキテクチャについて
私自身、長らくJavaでAPI開発とかやってきた者としては、SpringフレームワークをベースにしたMVCパターンに馴染みがありますが、GolangにはSpringみたいな強力なフレームワークが存在しませんので、プログラミングの原点に戻ってクリーンアーキテクチャを検討することにしました。
ディレクトリ構成画像からわかるように完全にクリーンアーキテクチャに出てくるレイヤーを元にディレクトリを切っています。クリーンアーキテクチャも今回初めてですし、うまく説明する自信がないですので、下記の記事を参考いただければと思います。
https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
ディレクトリ構成や、アーキテクチャについて色々先駆者達が試行錯誤しているような印象があって、これが正解だというのがないですので、チーム内で話し合って今の構成に決めました。(クリーンアーキテクチャのレイヤーで切ってたけど、後で直したよっていう経験談もあったが将来的に壁にぶつかったらまた考えることに)
WebフレームワークはGin
アーキテクチャの次にフレームワークの選定です。
githubのスター数だけではすべての指標ではないですが、Ginが一番スター数があるのは事実、そして活発的に開発されるし、ググったらたくさん参考できる日本語サイトも出てきますので、良いのではないかというところでGinにしました。
他にGinの特徴はこんなところかなと思います。
- 速い
- 軽量
JetBrainsさんがアンケートベースで出したレポートでも1位取ってますね こちら
各フレームワーク比較もご参考にどうぞ
コンパイル
GolangがサポートしているOS/CPUアーキテクチャや下記のコマンド確認できるので、環境ごとにコンパイルすることもできます。
go tool dist list
...(一部省略)
linux/386
linux/amd64
linux/arm
linux/arm64
...(一部省略)
今回本番環境はAWSのECS Fargateを想定していて、つい先日arm64もサポートするリリースがありましたので、linux/amd64とlinux/arm64のコンテナを用意することにしました。(実際どちらを使うかはまだ決めてないですが、arm64の方が省エネで料金節約になるらしいです。)
本番用と開発用のコンパイルオプションについて
-race
: thread-safeをチェックするみたいなもの(実際つけるとアプリケーションが遅くなるみたいな記事もあったので、チェックは開発環境だけにして、本番用にコンパイルする時はつけないようにしています)
-w -s
: このオプションをつけないとDebug用の情報も入るので、本番はつけてないです。
// 開発用
go build -race -o hoge main.go
// 本番用
go build -o hoge main.go -ldflags "-w -s"
クロスコンパイルときの注意点
クロスコンパイルでは-race
は使えないです。詳細はこちら
例:amd64のサーバーでarm64用のコンパイルをする時
Makefileにについて
Golangはコンパイラ言語としてMakefileの利用が主流になっています。
buildコマンドや環境構築コマンドをMakefileにまとめておくと結構便利です。
例えば本番用と開発用のコンパイルオプションについてで紹介した開発環境と本番環境のコンパイル差別化もMakefileに下記のようにまとめておくとmakeコマンド一つで実行できるようになります。
# disable symbol table and dwarf
GO_LDFLAGS_SYMBOL:=
ifeq ($(HOGE_ENV),prod)
GO_LDFLAGS_SYMBOL:=-w -s
endif
GO_LDFLAGS:=$(GO_LDFLAGS_SYMBOL)
# race detector
GO_BUILD_RACE:=-race
ifeq ($(HOGE_ENV),prod)
GO_BUILD_RACE:=
endif
# go build
GO_BUILD:=$(GO_BUILD_RACE) -ldflags "$(GO_LDFLAGS)"
.PHONY: build
build:
GOOS=linux GOARCH=amd64 go build -o hoge $(GO_BUILD) main.go
# 開発環境用build
HOGE_ENV=dev make build
# 本番環境用build
HOGE_ENV=prod make build
使用ライブラリ
viper
JSON, YAML, TOMLなどをサポートする設定ファイル用ライブラリ
参考できる資料はたくさんありますので、こちらでは割愛しますが、どんなふうに使っているかだけを簡単に紹介します。go:embed
と組み合わせて使ってます。
go:embedは静的ファイルをコンパイル済みELF形式バイナリファイルに含める機能を提供しています
├── config.go
└── yaml
├── config.prod.yml
└── config.dev.yml
database:
postgres:
name: hoge
host: localhost
port: 5432
user: user
pass: password
上記のようなディレクトリ構造で環境ごとの設定(DB設定など)を保持しているとするとconfig.goの中身はこうなります。
type Config struct {
Database struct {
Postgres struct {
Name string
Host string
Port string
User string
Pass string
}
}
}
var (
//go:embed yaml/*
staticYamlDir embed.FS
)
func LoadConfig() *Config {
setDefaultEnv()
hogeEnv := viper.GetString("env")
fileName := "yaml/config." + hogeEnv + ".yml"
viper.SetConfigName("config." + hogeEnv)
viper.SetConfigType("yaml")
viper.AddConfigPath("config/yaml")
b, err := staticYamlDir.ReadFile(fileName)
if err != nil {
panic("Failed to read config.")
}
if err := viper.ReadConfig(bytes.NewReader(b)); err != nil {
panic("Failed to load config.")
}
var c Config
if err := viper.Unmarshal(&c); err != nil {
panic("Failed to unmarshal config.")
}
return &c
}
func setDefaultEnv() {
viper.SetEnvPrefix("hoge")
viper.BindEnv("env")
viper.AutomaticEnv()
viper.SetDefault("env", "dev")
}
これでアプリケーション起動時にHOGE_ENV
という環境変数を設定することで読み込むconfigファイルがそれぞれ違うことになるので、環境別の設定をファイル分けて管理することができます。
sqlboiler
ORMライブラリです。使い方は割愛します。
有名なGORMとかありますが、今回sqlboilerを採用したのは下記の観点からです。
- schemaからコード生成がやりやすい
- 静的に型付けされており、実行時にリフレクションを使う必要がないため高速
- migration機能はないがflywayとかでカバーできる
- 生queryがかける
- relationもサポート
air
Golangはコンパイラ言語ですので、開発時に修正、ビルド、実行のサイクルを頻繁に行わないと行けないです。
airはそのサイクルを自動化してくれるいわゆるホットリロードライブラリとしてローカルでの開発効率を上げてくれます。
所感
Javaみたいにアノテーションつけておけばよしなにやってくれるみたな書き方ができないですが、とにかく素直にコードを書くところがGolangの特徴かなと思います。(JavaはそもそもSpringという強力な武器があるので、言語だけの比較にはならないですが)
良いところ:
- 新卒でもコード見ればある程度理解できるようなわかりやすさ
- 軽量、高速
- クラウドやコンテナ技術、マイクロサービスなどと親和性が高い
面倒なところ: Golangには例外という概念がなく、常にif err != nil
しないと行けないこと
Golangは何でもシンプルで行こうという設計思想から他の言語と差別化をし、たくさんの支持を得ていますので、使いにくいところもあるかもしれませんが、今後も仲良くなって行けそうな雰囲気でしたー。
最後に
新しい言語を勉強する時にネットにたくさんの情報が散らがっていて、実際そのとおりにやって見たらなんか古いやり方だったり、アンチパターンで作ったりするかもしれませので、一番先にオフィシャルドキュメントを一読することをおすすめします。特にGolangの場合はオフィシャルドキュメントが結構しっかりしていて、読みやすいので、ぜひご確認ください。
- オフィシャルドキュメント
- Effective go ※これとかは必ず読んで見てください。
Golangの目玉機能: goroutine, channelについては興味ありますが、まだ触れてないので、今後このあたりも攻略したいと思います。
おわり。