GOPATH を build.Default.GOPATH で適切に扱う

project-1

golang を使う人にとって、環境変数に GOPATH を設定しているのは当たり前ですが、go1.8 からは GOPATH が空の場合のデフォルト値として $HOME/go を使用する下記のプロポーザルが採用されました。

GOPATH の動作検証 – go1.8

新規にインスタンスを立てて、go1.7.5 と go1.8rc3 をインスタンスにダウンロードしています。
この環境で go env GOPATHgo env | grep GOPATH を実行して GOPATH の状態を見てみます。

### GOPATH に何も設定されていないのを確認
$ echo $GOPATH

### go1.7.5 の GOPATH
$ GOROOT=$HOME/go1.7.5 $HOME/go1.7.5/bin/go env | grep GOPATH
GOPATH=""

### go1.8rc3 の GOPATH
$ GOROOT=$HOME/go1.8rc3 $HOME/go1.8rc3/bin/go env | grep GOPATH
GOPATH="/home/kaneshin/go"

go1.8rc3 のとき、 GOPATH に $HOME/go が設定されているのが確認できました。

デフォルト GOPATH のメリット

デフォルト GOAPTH によって初心者が「$GOPATH の設定し忘れで動作できない」といった問題でハマるといったことが無くなります。

### go1.7.5: GOPATH が設定されていないため、 go get 不可能
$ GOROOT=$HOME/go1.7.5 $HOME/go1.7.5/bin/go get -v github.com/golang/dep
package github.com/golang/dep: cannot download, $GOPATH not set. For more details see: go help gopath

### go1.8rc3: GOPATH が設定されていないが $HOME/go が作成されて go get 可能
$ GOROOT=$HOME/go1.8rc3 $HOME/go1.8rc3/bin/go get -v github.com/golang/dep
github.com/golang/dep (download)
created GOPATH=/home/kaneshin/go; see 'go help gopath'
github.com/golang/dep/vendor/github.com/Masterminds/semver
github.com/golang/dep/vendor/github.com/Masterminds/vcs
github.com/golang/dep/vendor/github.com/pkg/errors
github.com/golang/dep/vendor/github.com/armon/go-radix
github.com/golang/dep/vendor/github.com/sdboyer/gps
github.com/golang/dep

ただし、メリットだけではなく、既存のコードに対してデメリットが発生することもあります。

デフォルト GOPATH のデメリット

GOPATH が空だった場合、あたかも GOPATH=$HOME/go のように環境変数に設定されているようになります。しかし、環境変数そのものに設定されているのではなく GOPATH が空だった場合、 go/build パッケージの build.Default.GOPATH に下記の defaultGOPATH 関数の値が設定されているだけです。

var Default Context = defaultContext()

// ...

func defaultContext() Context {
    var c Context

    c.GOARCH = envOr("GOARCH", runtime.GOARCH)
    c.GOOS = envOr("GOOS", runtime.GOOS)
    c.GOROOT = pathpkg.Clean(runtime.GOROOT())
    c.GOPATH = envOr("GOPATH", defaultGOPATH())

// ...

func defaultGOPATH() string {
    env := "HOME"
    if runtime.GOOS == "windows" {
        env = "USERPROFILE"
    } else if runtime.GOOS == "plan9" {
        env = "home"
    }
    if home := os.Getenv(env); home != "" {
        def := filepath.Join(home, "go")
        if def == runtime.GOROOT() {
            // Don't set the default GOPATH to GOROOT,
            // as that will trigger warnings from the go tool.
            return ""
        }
        return def
    }
    return ""
}

これを知らないと「環境変数として GOPATH に設定されている」と勘違いされやすいです。

(go でバイナリ作成されたものが勝手に GOPATH を環境変数として設定していたら気持ち悪いので、当たり前といえば当たり前ですね)

os.Getenv("GOPATH") が動かなくなる可能性

さて、これによって何が起こるかと言うと、 os.Getenv("GOPATH") が実装されているソースコードに対して問題が発生する可能性があります。

試しにGitHub上で os.Getenv(“GOPATH”) を検索してみてください。かなりヒットすると思いますが、これらが正常に動かなくなる可能性があります。


screen-shot-2017-01-27-at-12-53-04-pm


go1.8 から初心者のためにデフォルトの GOPATH を設定してくれるようになりましたが、そのまま GOPATH を理解せずにサードパーティ製のライブラリを使ったら動かなくなってハマる…… みたいなことが起きそうだなと思っています。

os.Getenv("GOPATH") への対処方法

コード内で os.Getenv("GOPATH") を使っている場合、os.Getenv("GOPATH") の代わりに build.Default.GOPATH を使うようにします。

package main

import (
    "fmt"
    "go/build"
    "os"
)

func main() {
     fmt.Println(os.Getenv("GOPATH"))  // => (何も出力されない)
     fmt.Println(build.Default.GOPATH) // => /home/kaneshin/go
}

build.Default – build.Context

build.Default は go/build パッケージで定義されているデフォルトのコンテキストを保持しています。

var Default Context = defaultContext()

このコンテキストはビルド時の情報を色々と保持しています。

type Context struct {
    GOARCH      string // target architecture
    GOOS        string // target operating system
    GOROOT      string // Go root
    GOPATH      string // Go path
    CgoEnabled  bool   // whether cgo can be used

// ...

GOPATH に限らず、ビルド時の環境変数として必要なものはここから取得するようにしておいた方がと良いです。

GOPATH の仕様

本記事とはあまり関係ないですが GOPATH は PATH と同じように、セパレータを使えば複数のパスを設定することができます。

$ echo $GOPATH
/home/kaneshin/local:/home/kaneshin/go:/usr/local/go

GOPATH には複数のパスでなく、ひとつだけでも十分に事足りるのですが、第三者が複数の GOPATH を設定しているのを想定しておくのも必要です。そのために filepath.SplitList を使用します。

package main

import (
    "fmt"
    "go/build"
    "path/filepath"
)

func main() {
    for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
        fmt.Println(gopath)
    }
}

これを先ほどの GOPATH で実行した結果は下記のようになります。

/home/kaneshin/local
/home/kaneshin/go
/usr/local/go

GOPATH は PATH のように広範囲に使うことはあまりユースケースとしてないので、一つのパスを設定する方をオススメします。私の場合は、App Engine を使うとき、稀に GOPATH を巧く扱うことがある程度です。

おわりに

テストコードでは os.Getenv("GOPATH") を使うのは問題ない場面が多いですが、実行するコードではなるべく使わないようにして、どの環境でも動くようにしておくのがベストです。

※ 本記事は go1.7.5 と go1.8rc3 を元に検証を行っています。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

10 Tips when moving from Objective-C to Swift

SwiftでiPhone標準写真アプリのアニメーションを再現してみる