Edited at

他言語から来た人がGoを使い始めてすぐハマったこととその答え


環境構築するまでに知りたかったこと


Golangの最新バージョンは?

1.12.5 (2019/05/06リリース)

https://golang.org/doc/devel/release.html


パッケージ管理ツールどれ使ったらいんだろう

OSSのコード眺めてるとまだgodepやglideも多いみたいだけど、最近(Go 1.12以降)は公式の vgodepを使ったほうがよさそう。vgoはdepからの移行、またdepはglideからの移行も簡単。

vgo, godep, glideなどが無い時代からの慣習でvendoring(2014年の時点ではそれがベスト・プラクティスだったはず)を好む人もいる。というか、だいたいの著名なプロジェクトはvendoringしている。


vendoringするかどうかにかかわらず、パッケージ管理ツールは入れよう

vendoringするとしてもdepかglideは導入して「何をvendorに入れるか」ということはコード化しておいたほうがいいと思うので、パッケージ管理ツールは必須と思っておいたほうがよさそう。というか、Go 1.12以降は何もしなければ vgo を使うことになる。


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

が設定されたものとみなされるようになった。

https://github.com/golang/go/issues/17262

基本的にはそのままでよくて、もし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/installgem install

glide get/install → だいたいGemfileいじって、bundle install

glide updatebundle 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しているプロジェクトの場合

例えば以下のリンク先のように、

https://github.com/coreos/kube-aws/blob/v0.9.2-rc.4/config/config.go#L16-L19



  • 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









  • Gopath4fork


    • src


      • github.com


        • mumoshu


          • kube-aws









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

個人的には、この方法で困ってないので、こちらを推奨しておきます。


コードを書き始めるまでに知りたかったこと


依存パッケージのインストール方法がわからない

まず、プロジェクトルート以下にvendor/ディレクトリが存在して、そこに依存先パッケージのソース一式が入ってるなら、改めてパッケージインストールする必要はない。go rungo test実行してみて、パーケージ不足のエラーがでないならOK。

パッケージ不足のエラーが出たり、vendorがなかったりする場合は以下の手順で確認する。


  • プロジェクトルートにGopkg.tomlというファイルがある場合は、depという公式パッケージ管理ツールを使っているので、dep ensureを実行

  • プロジェクトルートにglide.yamlというファイルがある場合は、Glideというパッケージ管理ツールを使っているので、glide installを実行

  • プロジェクトルートにGodepsというディレクトリがある場合はGodepというパッケージ管理ツールを使ってるので、godep getを実行


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へのポインタどちらも代入できる

https://play.golang.org/p/GpMgPexosL

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をコピーしているかというと、そうではない。

SliceTricks · golang/go Wiki


デプロイするまでに知りたかったこと


バイナリサイズ大きくない?



  • 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



あわせて読みたい: 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を直接利用できる。





  1. いわゆるTransitive Dependency