環境構築するまでに知りたかったこと
Golangの最新バージョンは?
1.16.6 (2020/07/12リリース)
Goのインストール方法は?
macOS の場合は brew で OK
$ brew install go
この場合、goのsdkは /usr/local/Cellar/go/<バージョン>/libexec
にインストールされます。 IntelliJ IDEA や GoLand にような IDE に「GOROOT」というものの指定を求められたら、このパスを使います。
brew で入らないバージョンの go が必要だったり、いくつかのバージョンを併用したい場合は
$ go get golang.org/dl/go1.14.4
$ go1.14.4 download
# 1.xx.0 の場合
$ go get golang.org/dl/go1.15
$ go1.15 download
# 上記でgo getしたgo1.15等が見つからない場合、 `$GOPATH/bin` 以下を参照してください
# $GOPATH は go env | grep GOPATH で特定できます
これ以降、 go1.14.4
を実行すると名前のとおり go 1.14.4 が利用できます。1.14.4
のところは必要に応じて置き換えてください。
なお、この場合のGOROOTは go env GOROOT
で確認できます。
$ go1.14.4 env GOROOT
/Users/mumoshu/sdk/go1.14.4
参照: https://golang.org/doc/install#extra_versions
パッケージ管理ツールどれ使ったらいんだろう
2019年現在はほとんど go
コマンドにも組み込まれている go mod
(Go Modules) を使っているので、それを使いましょう。
2018年以降メンテされていないような古いプロジェクトはまだ glide
や dep を使っていることがあるので、そういうプロジェクトをビルドする必要がある場合は、後述の手順を参考に dep
や glide
を使ってビルドしてください。go modはvgoはdepからの移行、またdepはglideからの移行が簡単にでまます。
go mod, godep, glideなどが無い時代からの慣習でvendoring(2014年の時点ではそれがベスト・プラクティスだったはず)を好む人もいる。今でも多くの著名なプロジェクトでvendoringしている。
vendoringするかどうかにかかわらず、パッケージ管理ツールは入れよう
vendoringするとしてもdepかglideは導入して「何をvendorに入れるか」ということはコード化しておいたほうがいいと思うので、パッケージ管理ツールは必須と思っておいたほうがよさそう。というか、Go 1.12以降は何もしなければ Go Modules を使うことになると思う。
どのツールでも vendoring は可能なので、それによって古いパッケージ管理ツールをあえて選択する必要もない。例えば Go Modulesなら go mod vendor
で vendor を生成して、 go build -mod=vendor
のように vendor を使ったビルドを実行することができる。
Go Modules / vgo
Go 1.11から(Versioned) Go Modules (a.k.a その実装である vgo
)が利用できるようになっている。 vgo
が有効化された go
を使うと、 go build
のタイミングで勝手に必要なモジュールバージョンをダウンロードしてくれる。 dep
等だと vendor
に該当モジュールをダウンロードする前提だったけど、 vgo
の場合はデフォルトでは vendor
を利用しない。$GOPATH/go/mod
以下にバージョニングされた状態でダウンロードされて、vgo
を利用する全プロジェクトはそこを参照するようになる。ので、vendor
を必ずしもgit commitしなくてもよい。
Go 1.11なら GO111MODULES=on go build
のように go
コマンドを実行すると利用できる。Go 1.12以降はデフォルトで GO111MODULES=auto
の扱いとなっている。auto
の場合は 後述する GOPATH
以下のディレクトリの場合は vgo
が無効化され、 GOPATH
の外なら vgo
が有効化された状態になる。
GOPATHどこにしたらいいの?
GOPATHは、git cloneしたパッケージや、go getしたパッケージ、自分がコードを変更する可能性のある・なしにかかわらず、vendor以外のパッケージすべてをインストールする先となるディレクトリ。
Go 1.8以上
特に指定されていない場合は、
export GOPATH=$HOME/go
が設定されたものとみなされるようになった。
基本的にはそのままでよくて、もしGOPATHを複数持ちたいのであれば、export GOPATH=変更後のGOPATH
で切り替えればOK。
Go 1.5以上
GOPATH設定してないと、go getすらできないけど…。
今まではプロジェクトルート以下に依存パッケージをgo getしたいからって、苦し紛れにプロジェクトルートやその配下をGOPATHにしたりしてた。
でも、どうやらvendorが来たGolang 1.5以上は、
- GOPATHはプロジェクトルート以外(ホーム以下とかでもOK)の適当なところに設定しておく(go getしたものはそこにインストールされる)
- ついでに$GOPATH/binをPATHに追加しておく(これでgo installで生成されたバイナリへのパスが通せる)
- プロジェクト毎に必要なパッケージはglide get(この時点ではGOPATHを変更する必要はなし)
- scaffoldingをするようなツールがGOPATH以下に生成したソースを置いてくれることがあるので、それで都合が悪い場合は
GOPATH=$(pwd)
→これはやめたほうがいいと思います- 例えば
bash -c "GOPATH=$(pwd); cobra init github.com/mumoshu/hoge"
- …でこの時はいいんだけど、GOPATHが変わらない前提でワークフローが組まれているツールがあると思う
- 例えばcobra。
cobra init foo/bar
すると$GOPATH/src/github.com/foo/bar
以下にソースが生成されて、以降のcobra hoge
コマンドはそのディレクトリにcd
した状態で実行される想定になっている。
- 例えば
ということでよさそう。
Rubyから来た人なら、この状態でgemコマンドと対応付けて覚えられそう。
go get/install
→ gem install
glide get/install
→ だいたいGemfileいじって、bundle install
glide update
→ bundle update
GOPATH以下ってどういう構成になってるはず?
自分がいま書いてるコードもGOPATH以下に置く、っていうのが他言語から来た人がハマるポイントだと思う。
$GOPATH
├─bin/
├─pkg/
│ └─darwin_amd64/
│ ├─github.com/
│ │ └─GitHubユーザ名
│ │ ├─`*.a`ファイル[^1]
│ │ └─GitHubレポジトリ名/`*.a`ファイル[^2]
│ └─pkg.in/
│ ├─パッケージ名/
│ └─`*.a`ファイル
└─src/
├─gopkg.in/
│ └─パッケージ名/
│ └─LICENSEとか`*.go`とかREADMEとか
└─github.com/
├─GitHubユーザ名
│ └─GitHubレポジトリ名/
│ └─LICENSEとか`*.go`とかREADMEとか
└─<あなたのGitHubユーザ名>
└─GitHubレポジトリ名/
├─glide.yaml
├─main.go
├─その他、あなたが開発中のソフトウェアのコード
└─vendor/依存先パッケージのコード(glideでとってきたやつ)
git cloneしてきたGoパッケージはどこに置くの?
(最近少ない)自プロジェクトのサブパッケージを相対パスでimportしているプロジェクトの場合
GitHub上でforkした上で、REPO=repo; git clone git@github.com:$YOUR_GITHUB_USER/$REPO.git $GOPATH/src/github.com/$YOUR_GITHUB_USER/$REPO
forkする理由は、パッケージが被らないようにするため。既にgo get
でそのGoパッケージをGOPATH以下にインストールしてあった場合、forkしてないとgo get
したものも、git clone
したものも両方github.com/user/repo
インストールすることになっちゃうので、後でやったほうが失敗してしまう。
代案として、GOPATHは複数設定できるので、ふたつ目のGOPATH以下にgit cloneする・・みたいなのも考えたけど、その場合はcobra
みたいにGOPATH以下にソースをscaffoldしてくれるツール(おそらくGOPATHに2つのパスが書かれていたら、最初の一つめ以下にscaffoldすると思う)と相性悪そうなのでやってない。
同じ結論が【翻訳】プロダクション環境でのベストプラクティス - Qiitaに書いてあったけど、こういう理由でそうしてるのかは不明。
(最近多い)自プロジェクトのサブパッケージを絶対パスをimportしているプロジェクトの場合
例えば以下のリンク先のように、
-
github.com/coreos/kube-aws
というプロジェクトのサブパッケージが、 - 同プロジェクトの他のサブパッケージを"github.com/coreos/kube-aws/subpackage"のように絶対パスで
参照している場合、前述の方法でこれをgithub.com/mumoshu/kube-aws
のようなfork先のパッケージ名にgit cloneすると、コンパイルできない。というか、fork元のパッケージがGOPATHにあったら、そちらをimportしていまう。
この場合は、二通り方法がある。一つは、「fork元とfork先をそれぞれ別のGOPATHへgit cloneする」。もう一つは「fork先はfork元の名前でgit cloneする」。
fork元とfork先をそれぞれ別のGOPATHへgit cloneする
もう何を言っているのかわからないかもしれないけど、以下のようなディレクトリ構造にするということ。
ディレクトリ名はわかりやすさのためにあえてダサくしたので、もっと良い名前があればそれを使ってください!または教えてください。
- Gopath4origin
- src
- github.com
- coreos
- kube-aws
- coreos
- github.com
- src
- Gopath4fork
- src
- github.com
- mumoshu
- kube-aws
- mumoshu
- github.com
- src
fork先を開発するときは、以下のようにfork先用に作ったGopathを$GOPATHに設定して、その下にある該当パッケージに移動して開発を進める。
export GOPATH=$HOME/Gopath4fork
cd $GOPATH/src/github.com/mumoshu/kube-aws
go build .
fork先はfork元の名前でgit cloneする
fork元がgithub.com/coreos/kube-aws
で、fork先がgithub.com/mumoshu/kube-aws
だったら、以下のようにfork元もfork先もgithub.com/coreos/kube-aws
へgit cloneしてそこで開発をする、ということ。
mkdir $GOPATH/src/github.com/coreos
git clone git@github.com:mumoshu/kube-aws.git $GOPATH/src/github.com/coreos/kube-aws
cd $GOPATH/src/github.com/coreos/kube-aws
自分が普段開発するときは、remoteにはfork元とfork先の両方が常にある状態にしてます。
$ git remote
camilb
mumoshu
origin
上記の例だとoriginがfork元で、camilbが他の人のfork、mumoshuが自分のforkという状態にしてあります。例えば、他の人のforkは以下のように追加できますね。
$ git remote add camilb git@github.com:camilb/kube-aws.git
最初にcloneしたのがfork元レポジトリだった場合、originがfork元なので自分のforkを追加したくなりますよね。その場合は以下のようにできます。
# まだforkしてなかったら..
$ hub fork
$ git remote add mumoshu git@github.com:mumoshu/kube-aws.git
個人的には、この方法で困ってないので、こちらを推奨しておきます。
コードを書き始めるまでに知りたかったこと
- 新規プロジェクトの作り方
- 依存パッケージのインストール方法
- 定番ライブラリ
- interface, ポインタ型の特徴
- sliceはimmutableでない、appendは破壊的
新規プロジェクトの作りかたがわからない
2019年現在では Go Modulesを使うのがデファクトだと思うので、それ前提であれば以下のようにプロジェクトを作成する。
$ PROJECT_NAME=myapp
$ mkdir -p path/to/${PROJECT_NAME}
$ cd $_
#
# Go Modulesプロジェクトとして初期化する
# これをやらない = go.modが無い状態だと、Go Modulesプロジェクトとして認識されない
#
$ go mod init github.com/${GITHUB_USER}/${PROJECT_NAME}
go: creating new go.mod: module github.com/${GITHUB_USER}/${PROJECT_NAME}
#
# 使う予定のパッケージをプロジェクトに追加する
#
$ go get github.com/hashicorp/hcl/v2@v2.2.0
go: finding github.com v2.2.0
go: finding github.com/hashicorp v2.2.0
go: finding github.com/hashicorp/hcl/v2 v2.2.0
go: finding github.com/hashicorp/hcl v2.2.0
go: downloading github.com/hashicorp/hcl/v2 v2.2.0
go: extracting github.com/hashicorp/hcl/v2 v2.2.0
go: downloading github.com/zclconf/go-cty v1.1.1
go: downloading github.com/agext/levenshtein v1.2.1
go: extracting github.com/agext/levenshtein v1.2.1
go: extracting github.com/zclconf/go-cty v1.1.1
go: finding github.com/zclconf/go-cty v1.1.1
$ cat go.mod
module github.com/${GITHUB_USER}/${PROJECT_NAME}
go 1.13
require github.com/hashicorp/hcl/v2 v2.2.0 // indirect
あとは Using Makefile(s) for Go で紹介されているような Makefile を置いておくと便利です。
依存パッケージのインストール方法がわからない
ほぼGo Modules前提であると思われるGo 1.12以降の場合、go get github.com/foobar
で依存パッケージを(go.modというファイルへ)追加するということと、go.mod
に記載されている依存パッケージのうち足りないものは go build
、go run
時に自動的にインストールされる、ということを覚えておけば問題ないと思います。
なにかうまく行かない、という場合は以下の手順を参考に対応方法を決めてください。
まず、プロジェクトルート以下にvendor/
ディレクトリが存在して、そこに依存先パッケージのソース一式が入ってるなら、改めてパッケージインストールする必要はない。go run -mod=vendor
やgo test -mod=vendor
実行してみて、パーケージ不足のエラーがでないならOK。
パッケージ不足のエラーが出たり、vendorがなかったりする場合は以下の手順で確認する。
- プロジェクトルートに
go.mod
というファイルがある場合はGo Moudlesという公式パッケージ管理ツールを使っているので、go run
やgo build
実行時に不足パッケージが自動的にダウンロードされるはず。あえてvendor
を生成する必要がある場合はgo mod vendor
を実行する。 - プロジェクトルートに
Gopkg.toml
というファイルがある場合は、depという以前の公式パッケージ管理ツールを使っているので、dep ensure
を実行 - プロジェクトルートに
glide.yaml
というファイルがある場合は、Glide
というパッケージ管理ツールを使っているので、glide install
を実行 - プロジェクトルートに
Godeps
というディレクトリがある場合はGodep
というパッケージ管理ツールを使ってるので、godep get
を実行
依存ライブラリの一部だけforkしたものに置き換えたい場合はどうしたら?
go.mod
中で replace
directive を使うと、 =>
の左辺で指定したmoduleを右辺で指定したものに置き換えることができます。
go.mod
中のimport
をfork先に変更してしまうと、ソースコード中の import
元もfork先に変更しないといけなくなってしまうし、fork先のソースコードのgo.mod
中でのパッケージ名も変更しないといけないので、面倒ばかりなのでやらないほうがいいと思います。 replace
しましょう。
例えば、 github.com/google/go-github/v37
を github.com/mumoshu/go-github
に fork して、色々変更を加えたソースのコミットに v37.0.100
というタグをつけたとします。そのタグに replace
するためには、以下のような replace
を go.mod
に追記します。
replace github.com/google/go-github/v37 => github.com/mumoshu/go-github/v37 v37.0.100
わざわざ v37.0.100
のようなタグを打たなくてもコミットID等を指定できないんでしょうか? 2021/08/01 現在 Go 1.16.x で試した限りでは、できないようです。
わざわざ fork しないで、ローカルディレクトリに向けることはできないんでしょうか?できます。
例えば、対象プロジェクトと同じレベルのディレクトリに go-github を git-clone して変更を加えている場合、以下のような replace
でそのローカルディレクトリを参照させることもできます。 ../go-github
のところで現在のプロジェクトとの相対パスでいじった go-github
を含むローカルディレクトリを指定しています。
replace github.com/google/go-github/v37 => github.com/mumoshu/go-github/v37 ../go-github
ただし、 docker build
中で go build
するときに、 replace
から参照したモジュールも build context に用意しておかないといけない点には注意してください。build context に導入できるのはカレントディレクトリ以下のファイルになるので、 replace
で参照しているディレクトリもカレントディレクトリ以下において置く必要があります。
例えば、上記の replace
が go.mod
にある状態で以下のような Dockerfile を docker build
すると落ちます。
COPY . .
RUN go build
../go-github
ではなくカレントディレクト以下の、例えば ./go-github
に replace
先をおいておいて、 build context に含まれるようにしておけば問題ありません。
replace github.com/google/go-github/v37 => github.com/mumoshu/go-github/v37 ./go-github
glide install成功したのに、依存先の依存先1のパッケージが見つからない
2016/03時点では、自分で書いたコードから直接参照してる依存関係だけ解決するらしい。それが、glide.yaml
に直接書かれているパッケージだけが解決される、直接書かれていないものは解決されない、というのと同義なのかな・・・?
今まで経験したパッケージ管理ツールはtransitive dependencyを勝手に解決してくれるものばかりだったので、目からうろこでした。勝手に解決しないから、k8sみたいな大きなプロジェクトの一部だけを再利用しても、依存パッケージが爆発しなくて済む、ということなのかな?それは嬉しいですね。
cliアプリ開発用のライブラリどれ使ったらいいんだろう
cli: https://github.com/codegangsta/cli
cobra: https://github.com/spf13/cobra
の2強っぽいけど、k8sとかCoreOS関係のOSSがcobra採用してるみたいなのでcobraにしました。
あと、cobraと同じ作者のviperと組み合わせると、コマンドラインフラグとYAMLファイルなどで設定をカスタマイズ可能なCLIが楽につくれます。
viper: https://github.com/spf13/viper
Godepでパッケージ管理してるプロジェクトってどうやってビルドしたらいいの?
go 1.5以降
godepはvendor/
に対応している。プロジェクトのソースにvendor/
ディレクトリが含まれているなら、godepにGOPATH以下を変更させなくてもビルドできるはず。(vendor/
にすべての依存パッケージがgodep save
で保存されているはずなので)
vendor/
が含まれるプロジェクトがgo build
でビルドできるように、godepで依存パッケージを管理しているプロジェクトはgodep go build
でビルドできるはず。
例えば、export GOPATH=$HOME/Gopath
になっている場合、以下のようにする。
$ git clone git@github.com:kubernetes/contrib.git ~/Gopath/src/k8s.io/contrib
$ cd ~/Gopath/src/k8s.io/contrib/cluster-autoscaler/
$ make build
rm -f cluster-autoscaler
go get github.com/tools/godep
GOOS=linux godep go build ./...
GOOS=linux godep go build -o cluster-autoscaler
go 1.5まで
godepはglideと違ってvendorを使わないので、プロジェクト毎にGOPATHを分けるほうが安全そう。ということで、GOPATHを作ってプロジェクトをそこにcloneし、次にgodep restore
によってGOPATH以下に依存パッケジージをダウンロードさせればビルドできる。
Glideを利用する前提のGOPATHと混ざらないように注意(まざっても実害はないと思うけど・・・不要なsrcがGopathに増えてしまって、きれいに掃除できなくなると思う)。
ホーム以下にそのプロジェクト専用のGOPATHを作るには、例えば以下のようにする。
$ export GOPATH=$HOME/Gopath-適当なID
$ mkdir -p $GOPATH/src/github.com/$ORG_OR_USER
$ git clone git@github.com:$ORG_OR_USER/$REPO.git $GOPATH/src/github.com/$ORG_OR_USER/$REPO
$ cd $GOPATH/src/github.com/$ORG_OR_USER/$REPO
$ godep restore
$ make hoge
プロジェクトルート以下にGOPATHをつくればよいのでは?と思うかもしれないが、godep的にNG。
$ mkdir -p path/to/project
$ git clone git@github.com:$ORG_OR_USER/$REPO.git path/to/project
$ cd path/to/project
$ export GOPATH=$(pwd)/go
$ godep restore
$ make bulid
...
godep: [WARNING]: godep should only be used inside a valid go package directory and
godep: [WARNING]: may not function correctly. You are probably outside of your $GOPATH.
godep: [WARNING]: Current Directory: /path/to/project
godep: [WARNING]: $GOPATH: /path/to/project/go
cluster_autoscaler.go:45:2: cannot find package "github.com/golang/glog" in any of:
「インタフェースのポインタ」は定義してはならない
または、不要。
例えば、あるインタフェースInterfaceを実装するStructがあるとして、
s Struct
i Interface
のとき
s = i
は問題なく代入可能だが、
s *Struct
i *Interface
のとき
s = i
はコンパイルエラー。
インタフェースはポインタのようなもの(より正確には、インタフェースは型のポインタ+structへのポインタのようなもの)だから、ポインタのポインタへstructのポインタを代入するような意味合いになってしまう。
go - Why can't I assign a *Struct to an *Interface? - Stack Overflow
インタフェースはnilになれる&Struct、Structへのポインタどちらも代入できる
package main
import "fmt"
type Struct struct {}
type Interface interface {}
func main() {
s1 := Struct{}
s2 := &Struct{}
var i1 Interface
i1 = s1
var i2 Interface
i2 = s2
fmt.Printf("%v, %v\n", i1, i2)
//=> {}, &{}
i1, i2 = nil, nil
fmt.Printf("%v, %v\n", i1, i2)
//=> <nil>, <nil>
}
sliceはimmutableではない/appendは破壊的操作
appendはsliceを必ずしもコピーしない。言い換えると、appendするとappend先のsliceを変更してしまうこともある。具体的には、append先のsliceのサイズが足りなくなった場合に自動的にコピーされる(と思っているのですが、間違っていたら教えてください)。
あと、「sliceをコピー」と書いたが、この場合実際コピーされるのはslice(参照先配列のポインタ+長さ)の裏にある固定長配列といったほうが正しい気がする。
詳しくは以下
appendを使うときにお作法として
a := append(a, "something")
のように再代入しているからといって、appendが必ずaをコピーしているかというと、そうではない。
デプロイするまでに知りたかったこと
バイナリサイズ大きくない?
-
go build -ldflags "-s -w"
を使う - upx/goupxを使ってもいいけど、起動時間が伸びる
あわせて読みたい: Shrink your Go binaries with this one weird trick
Dockerイメージのサイズ大きくない?
- 基本的にscratchイメージにGoバイナリをADDするだけにする
- scratchでだめなケースはalpineで賄えないか考える
- CGO使うならビルド時に
apk add *dev
で必要devパッケージを入れて静的リンク。それをscratchイメージにADD - TLSでCA証明書が必要なら、scratchではなくalpineをベースにして、apk add ca-certificates
- CGO使うならビルド時に
あわせて読みたい: Dockerで最小のGoのイメージを作成する(cgo編) - Qiita
複数goバージョンを共存させたい
公式の「Installing extra Go versions」に書いてあるとおりだけど、go get
で以前のバージョンのgoをインストールできる。例えば、1.12がある状態で1.11.11をインストールしたい場合は以下のようにする。
$ go get golang.org/dl/go1.11.11
$ go1.11.11
go1.11.11: not downloaded. Run 'go1.11.11 download' to install to /Users/myuser/sdk/go1.11.11
$ go1.11.11 download
Downloaded 0.0% ( 16384 / 120494223 bytes) ...
Downloaded 32.6% ( 39239680 / 120494223 bytes) ...
Downloaded 76.3% ( 91979776 / 120494223 bytes) ...
Downloaded 100.0% (120494223 / 120494223 bytes)
Unpacking /Users/myuser/sdk/go1.11.11/go1.11.11.darwin-amd64.tar.gz ...
Success. You may now run 'go1.11.11'
これで /Users/myuser/sdk/go1.11.11
以下に 1.11.11 がインストールされ、go1.11.11 <command>
でシステムのgoとは別バージョンのgoを直接利用できる。
-
いわゆるTransitive Dependency ↩