Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Ventiディスクをオフサイトバックアップする方法[9fans]

この記事はQiitaで公開されていました

[9fans]Venti off-site backupsより、Ventiストレージのバックアップを行う方法をメモ。最後は個人的によく使う方法。

venti/mirrorarenasミラーリング

plan9portに含まれているmirrorarenas(8)コマンドで以下のように実行する。

% venti/mirrorarenas /dev/sdC0/arenas /dev/sdD0/arenas

これで、sdC0からsdC1にコピーが行われる。mirrorarenasはマニュアルに載っていない。

この方法は、異なるディスクにarenaパーティションが必要。

backup/readwriteでコピー

9legacyのbackup/readwriteでコピーする。このコマンドにはホスト名がハードコーディングされているので、ventihost変数を修正してから使うこと。

この方法は、別のventiが動作しているマシンが必要。

backup/readbackup/writeでコピー

これも9legacyに置いてあるコマンドを使う。backup/readarenaをローカルにバックアップして、一旦それを外部ストレージに移動させる。

リストアする場合は、backup/writeを使って、バックアップしていたファイルを書き戻す。

venti/copy -fでコピー

venti/copy(1)が使えるなら、コマンド叩くだけでコピーが取得できるので便利。遅いので、通常は-fオプションを付けて使うとよい。

この方法は、別のventiが動作しているマシンが必要。

golang.org/x/sync/singleflightで重複呼び出しを排除する

この記事はQiitaで公開されていました

ユーザ操作などで、同じAPIを同時にリクエストされたけれど、例えばGETメソッドの場合は結果もほとんど同じになるので、リクエストを1回にまとめてしまいたい場合は少なくないと思います。

または、期限付きの認証トークンが必要なAPIを並行して実行しているケースで、トークンの期限が切れた直後で同時に2つのリクエストが行われても、トークンの更新は1回だけに制限したい場合もあるかもしれません。

そういった、「複数の呼び出しが同時に発生しても、結果は同じなので同時に1つだけ行って結果を共有する」という処理に、x/sync/singleflightが使えます。

実装例

重複の排除を行いたい部分を、singleflight.GroupDo(name, fn)でラップします。以下の例では、1ミリ秒ごとにcallAPI("work")が実行されますが、callAPI("work")は3ミリ秒の時間がかかるので、続く2回の呼び出しが起こった時にはまだ前の処理が終わっていません。そうするとsingleflight.Groupは1つ目の呼び出しが終わるまで待って、1回目の結果を使って、2回目と3回目の呼び出しが行われたかのように振る舞います。しかし実際にAPIが呼ばれるのは1回目だけです。

package main

import (
    "log"
    "sync"
    "time"

    "golang.org/x/sync/singleflight"
)

var group singleflight.Group

func callAPI(name string) {
    v, err, shared := group.Do(name, func() (interface{}, error) {
        // 具体的に実行したい処理を書く
        <-time.After(3 * time.Millisecond)
        return time.Now(), nil
    })
    if err != nil {
        log.Fatal(err)
    }
    log.Println("結果:", v, ", 重複が発生したか:", shared)
}

func main() {
    log.SetFlags(0)

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            callAPI("work")
        }()
        <-time.After(time.Millisecond)
    }
    wg.Wait()
}

この実行結果は、おおむね以下のようになります。時刻が完全に一致している点から、結果が再利用されているのがわかると思います。

結果: 2017-06-12 23:53:11.936580392 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.936580392 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.936580392 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.940406256 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.940406256 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.940406256 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.94409058 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.94409058 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.94409058 +0900 JST , 重複が発生したか: true
結果: 2017-06-12 23:53:11.94766342 +0900 JST , 重複が発生したか: false

説明

singleflight.GroupDo(name, fn)メソッドは、nameの値が同じ呼び出しが実行中であれば、2回目以降の呼び出しを止めておいて、実行中だった最初のfnの結果をそのまま共有します。そのため、重複した呼び出しは全て同じ結果となります。(最初がたまたまエラーになったら全てエラーです)

結果が、nameの一致したDo(name, fn)に共有された後は、nameは未実行の状態に戻るので、次の呼び出しのfnは待機されずに実行します。また、nameが実行中であっても、異なるnameが使われた場合はそのままfnが実行されます。

上記の例ではDo(name, fn)だけ使いましたが、戻り値ではなくチャネル経由で結果を返すDoChan(name, fn)も用意されています。

plan9portのsecstoreを秘密のメモとして使う

この記事はQiitaで公開されていました

Plan 9 from User Spaceのsecstoreを使って秘密メモの扱いを便利にしました。

  • コマンドから秘密メモを扱いやすくなった
  • ある程度安全に秘密メモをGit管理できるようになった
  • 毎回パスワードを入れなくてもよくなった

動機

普段、業務を行なっていると、例えばアクセストークンや秘密鍵パスフレーズなど、パスワードに限らず色々な機密情報を扱うことがあります。これまではKeychain Access.appの秘密メモを使っていましたが、以下の点に不満がありました。

1つのファイルに複数メモが入っているので変更を追いづらい

Keychain Accessは、~/Library/Keychains/以下に、1つのKeychainあたり1つの*.keychain (Sierraからは.keychain-db)ファイルが作られているので、他のマシンに持って行ったりバックアップを取得したりするには都合が良いのですが、2つのバージョンどちらに必要なメモが入っていたのかを調べるのがとても大変です。

この場合、それぞれの*.keychainファイルをKeychain Accessの管理に入れて、内容を調べる必要があります。

GUIでの利用が前提になる

securityコマンドで、ある程度の操作は行えますが、「ちょっとメモを探す」ために使うのは難しそうです。(security dump-keychainを加工するしかなさそう)

ちょっと確認するために、Keychain Accessを開いて、メモを探して、パスワードを入力して、という手順を毎回行うのはめんどくさいですね。

secstoreを使う

secstoreはテキストファイルを暗号化して保存するためのサービスと、それを扱うコマンドです。Plan 9 from User Spaceの一部として配布されています。

Plan 9 from User Spaceのインストールは、自前の記事ですがPlan 9 from User Spaceを使うに書きました。launchdsecstoredを起動させるところまで設定しましょう。

そのあとで、secuserを使ってユーザを作成してください。

$ secuser -v $USER

成功すると、\$PLAN9/secstore/who/\$USERというファイルが作られます。

secstoreへ書く

secstore-pオプションを与えると、引数で渡したファイル名そのままの名前で、secstoredにエントリが追加されます。同じファイルが存在した場合は上書きされます。

$ echo hello >memo
$ secstore -p memo

このファイルは、secstoredによって暗号化されて、\$PLAN9/secstore/store/\$USER/以下に保存されます。

secstoreから読む

読む場合は2通りの方法があります。保存されているファイル名そのままカレントディレクトリに出力するには、-gオプションを使います。標準出力に内容を出力する場合は-Gを使います。

$ secstore -G memo
hello

$ rm -f memo
$ secstore -g memo
$ cat memo
hello

保存しているファイルリストを取得する

-gまたは-Gオプションに与えるファイル名が.の場合、保存されているファイルのリストを出力します。

$ secstore -G .
memo        38 Jun  1 09:29:52 JST 2017 j+J2ybyBzP5UzBbt9h/DkwPRDSk=

その他の操作

secstoredからエントリを削除する場合は-rオプションを与えます。また、保存しているメモのパスワードを変更したい場合は-cオプションです。これらの詳細はman secstoreしてください。

少し便利にする

secstoreパスワードを何度も入力したくない

secstoreは、-iオプションを与えると、パスワードを標準入力から読むようになります。これを使って、secstoreのパスワードをsecstoreという名前でKeychain Accessに保存しておき、securityコマンドで取り出しましょう。

#!/usr/bin/env bash

security find-generic-password -a $USER -l secstore -w |
$PLAN9/bin/secstore -i "$@"

バックアップをGitで行いたい

標準のインストール先は/usr/local/plan9なので、secstoreのディレクトリも普通はrootのファイルです。この部分だけを外に出すことができないので、以下のようなスクリプトを使って強引に対応しました。

#!/usr/bin/env bash

sudo bash -c "
cd $PLAN9/secstore
if [[ ! -d .git ]]
then
  git init
  git remote add origin git@repo.example.local:$USER/secstore.git
fi
if [[ -z \$(git status -s) ]]
then
  exit 0
fi
git add -A
git commit -m \"$(date +'%Y-%m-%d %H:%M:%S')\"
export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/id_rsa'
git push -u origin master
"

このリポジトリは、内容は暗号化されているとはいっても、プライベートに設定しておくのが無難でしょう。

類似ツール

GitのリモートURL単位でuser.nameとuser.emailを強制する

この記事はQiitaで公開されていました

困ったこと

GitHubは、コミットログのアイコン表示などで、Authorのメールアドレスを使います。なので、例えば普段とは違うマシンからコミットした場合、Authorのメールアドレスが普段と異なる場合は、アイコンが表示されません。

私自身は、業務ではGitHub Enterpriseを使っていて、GitHub Enterpriseには会社のアカウントでログインしています。しかし当然ですがGitHubも使ってまして、こちらのアカウントはGitHub Enterpriseとは異なります。

Cloneするリポジトリの数は圧倒的にGitHubの方が多いので、~/.gitconfigGitHub側のアカウントを書いていますが、これではGitHub EnterpriseのリポジトリGitHub側のアカウントが使われてしまいます。そのままコミットしてもエラーにはならないので、自分で管理しておかなければいけません。

例えば~/.gitconfigにはこのように書いておいて、

[user]
  name = lufia
  email = lufia@example.org

GitHub Enterpriseのリポジトリには個別に設定する。

$ git config user.name user1
$ git config user.email user1@example.com

今まではこれで運用していましたが、面倒だし、忘れた時に気付かなくて困ります。

対策

~/.gitconfigのデフォルトを削除する

コミット時に警告が表示されるので気がづく、という方法です。リポジトリごとにuser.nameやuser.emailの設定を強制するという記事があります。

ですが、間違えはないけど面倒だと思って、この方法は使いませんでした。また、勝手に~/.gitconfigを書き換えるソフトウェアとか存在しますし、その場合に気付けません。

pre-commitフックで弾く

後輩が似たようなことをやっていたので参考に、pre-commitで弾くようにしました。pre-commit~/.gitconfigに、init.templatedirを設定しておくと、その内容をリポジトリの初期化時にコピーしてくれるので、これを設定します。

[init]
  templatedir = ~/.git_template

フックスクリプトはこのような感じ。originのURLを取得して、期待と異なるユーザ情報であればエラーになります。

#!/bin/sh
# ~/.git_template/hooks/pre-commit
origin=$(git config --get remote.origin.url)

function must()
{
    local v=$(git config --get $1)
    if ! [[ $v =~ $2 ]]
    then
        echo >&2 "$1=$v: must match $2" 
        exit 1
    fi
}

case $origin in
*.example.com[:/]*)
    must user.name user1
    must user.email user1@example.com
    ;;
*)
    must user.name lufia
    must user.email lufia@example.org
    ;;
esac
exit 0

この方法では、途中で~/.git_template/hooks/pre-commitを変更しても、すでに存在するリポジトリのフックは変更されないところです。気になるなら、gitのhookを全リポジトリで共有するのように設定してみるといいかもしれません。

GoでFUSEを使ってGitHubのIssuesをマウントする

この記事はQiitaで公開されていました

この記事は、Goアドベントカレンダー(その2)の19日目です。

何を言っているのか分からないかもしれませんので、動画を用意しました。

何をするものなのか

これは、GitHubのIssueやその他サービスにおける同等なものを、ファイルツリーとしてひとつのディレクトリにマウントするものです。

現在の職場では、色々な事情があり、

など、いくつかのプロジェクト管理ツールを使って業務を行なっています。その中には、自社で用意したものもあれば、お客様によって用意されたものもあって、理由はわかるけれど自分のタスクが分散されてしんどいなーと思っていました。

一方で、今年のアドベントカレンダーPlan 9と絡めた話にしようと心に決めていたので、せっかくだし9PのLinux版とも言えるFUSE(Filesystem in Userspace)を使って、色々なプロジェクト管理ツールのうち自分が担当者となっているチケットを読むファイルサーバを作ろうと思って実装しました。

現時点では、GitHub, GitHub Enterprise, GitLabに対応しています。

FUSEとは

FUSE(9P)というのは、ディスクに限らず、プロセスもネットワークも全てをファイルとして表現することができるプロトコルです。Linuxではprocfsやsshfs、最近はs3fsとかgoofysが有名ですし、本家のPlan 9ではTCPやHTTP、ウィンドウとかエディタのようなものもファイルとして表現されています。ファイルにするメリットは、普通のread, writeだけでなんでもできる点かなと思います。シェルやAwk等のツールでもある程度のことができますし、本格的なプログラミングが必要になったとしても、裏で実行されているAPIを気にする必要なく単純にファイルの読み書きで済むのは便利なんじゃないかなと思います。

macOSは、標準ではFUSEを使えませんが、FUSE for macOSを導入すればLinuxと同様に使えることを確認しています(macOS 10.12.1)。また、WindowsにはDokanというものがあるみたいですが、こっちは使えるかどうか分かりません。

ソースコードlufia/taskfsで公開しています。

インストール方法

Linuxでの準備

CentOSまたはRHELの場合、fuseパッケージが必要です。

$ sudo yum install fuse

DebianUbuntu等も、同じようなパッケージが提供されていると思います。

macOSでの準備

macOSではFUSE for macOSからインストーラをダウンロードして実行するのが簡単です。私は試していませんが、Homebrewでも提供されているようです。

コマンドのインストール

go getでインストールできます。

$ go get github.com/lufia/taskfs

使い方

taskfsを実行すると、mtptにファイルツリーを構築します。mtptが省略された場合は/mnt/taskfsを使います。

$ taskfs [-d] [mtpt]

このコマンドは、アンマウントされるまでプロンプトへ戻りません。必要なら&を付けてバックグラウンドで実行させてください。

ファイル操作

最初は、mtpt以下にctlというファイルだけ存在しています。このファイルに、以下の書式で文字列を書くことによって、書き込んだURLのドメイン名でディレクトリが作られます。

GitHubの場合

add github {github_token} {github_api_url}

GitLabの場合

add gitlab {gitlab_token} {gitlab_api_url}

このうち、{github_api_url}は、github.comの場合のみ省略可能です。GitHub EnterpriseやGitLabの場合は省略できません。

$ cd /mnt/taskfs
$ echo add github $github_token >ctl
$ echo add github $ghe_token $ghe_url >ctl
$ echo add gitlab $lab_token $lab_url >ctl
$ ls
ctl     ghe.example.com     github.com  lab.example.com

ドメインディレクトリの中にはctlというファイルと、repository@organization#numberのルールで複数のディレクトリがあります。ディレクトリがひとつのIssueに対応していて、例えばtaskfs@lufia#1/messageを読むと、#1のIssueに書かれたコメントが読めます。 また、ドメインディレクトリにあるctlファイルは、refreshという文字列を書くとIssueを再取得します。

$ cd github.com
$ ls
ctl     taskfs@lufia#1
$ cat taskfs@lufia#1/message
メッセージ内容
$ echo refresh >ctl
$ ls
ctl     taskfs@lufia#1     taskfs@lufia#2

アンマウントする

Linuxの場合は、fusermount -uコマンドを使います。

$ fusermount -u /mnt/taskfs

macOSは普通のumountコマンドでアンマウントできます。

$ umount /mnt/taskfs

実装について

今回の実装ではhanwen/go-fuseと、そのサブパッケージnodefsを使ったので、これを基準に、FUSEの実装はどんな感じなのかを簡単ですが紹介します。

ファイルツリーのマウント

ファイルツリーをユーザに公開するため、最初のディレクトリをマウントする必要があります。これはnodefs.MountRoot(mtpt, root, opts)で行います。root引数は、nodefs.Nodeインターフェイスを実装している必要があります。nodefs.NodeFUSEで必要になるメソッドが30個ほど定義されている、とても大きなインターフェイスですが、nodefs.NewDefaultNode()でデフォルト実装を用意してくれているので、必要なメソッドだけ自分で実装すればいいようになっています。

import (
    "github.com/hanwen/go-fuse/fuse"
    "github.com/hanwen/go-fuse/fuse/nodefs"
)

// ルートディレクトリをあらわす型
type Root struct {
    nodefs.Node
}

// 必要なメソッドだけ自分で実装する
func (root *Root) GetAttr(out *fuse.Attr, file nodefs.File, ctx *fuse.Context) fuse.Status {
    // GetAttrは必要な属性やパーミッションでoutを更新しなければならない
    // ルートはディレクトリなので、fuse.S_IFDIRフラグと0755をセットする
    out.Mode = fuse.S_IFDIR | 0755
    out.Atime = uint64(time.Now().Unix())
    out.Mtime = uint64(time.Now().Unix())

    // 正常な場合はfuse.OKを返す
    return fuse.OK
}

func main() {
 
    // 中略

    root := &Root{
        // デフォルト実装を埋め込む
        Node: nodefs.NewDefaultNode(),
    }
    s, _, err := nodefs.MountRoot("/mnt/taskfs", root, &opts)
    s.Serve()
}

これだけでは、ルートはマウントできるけど空のディレクトリでしかありません。

ファイルツリーの構築

ルート以下のファイルは、nodefs.InodeNewChild(name, isDir, fsi)を使って構築していきます。

NewChildfsi引数はnodefs.Nodeインターフェイスを実装する型でなければいけません。

type File struct {
    nodefs.Node
}

func (root *Root) CreateChildren() {
    p := root.Inode()

    // ルート以下にfile1というファイルを作成する
    // ファイルの内容はhelloという文字列になっている
    file1 := &File{
        // ファイルの場合はnodefs.NewDefaultNodeより、
        // nodefs.NewDataFileの方が便利
        Node: nodefs.NewDataFile([]byte("hello")),
    }
    p.NewChild("file1", false, file1)

    // ルート以下にdirというディレクトリを作成する
    // ディレクトリの中にはfile2というファイルがある
    dir := &File{
        Node: nodefs.NewDefaultNode(),
    }
    p.NewChild("dir", true, dir)
    p1 := dir.Inode()

    file2 := &File{
        Node: nodefs.NewDataFile([]byte("hello")),
    }
    p1.NewChild("file2", false, file2)
}

これで、以下のようなファイルツリーになりました。

mtpt/
├── dir/
│   └── file2
└── file1

通常のファイル操作(catls等)でfile1へのアクセスを行うと、FUSEによってfile1が実装したnodefs.Nodeのメソッドが実行されるようになります。同様にdirへのアクセスはdirのメソッドが実行されます。あとは、必要に応じてnodefs.Nodeのメソッドを実装すれば良いです。

代表的なメソッド

nodefs.Nodeに定義されているメソッドで、よく使うと思われるものを抜き出しました。

メソッド名 どういう時に呼ばれるか
GetAttr ファイルの情報を取得する時に呼ばれる(ls等)
Lookup ディレクトリ内で特定のファイルを探す時に呼ばれる
OpenDir ディレクトリ内のファイル取得時に呼ばれる(ls等)
Open ファイルを開く時に呼ばれる(cat等)
Read/Write ファイルを読み書きする場合に呼ばれる
Truncate ファイルサイズを切り詰める時に呼ばれる(>等)
Mkdir ディレクトリ作成時に呼ばれる(mkdir等)
Create ファイルを新規作成する時に呼ばれる(touch等)
Unlink ファイルを削除する時に呼ばれる(rm等)

基本的には、nodefs.NewDefaultNode()nodefs.NewDataFile()のどちらも、ファイルの基本的な読み書き等といった、よくある動作は行ってくれるため、足りない動作だけ実装するのが良いと思います。

まとめ

ファイルとして表現する方法を考えるという手間がかかるので、Webやコマンドラインツールと比べるとお手軽ではありませんが、程度でいえば少しめんどくさい程度です。ファイルというUIがマッチする場面は比較的多いと思いますので、アイデアがあるならぜひやってみてください。

taskfs自身については、今回は間に合わなかったのですが、Plan 9(9P)でも動かすように対応したいですね。あと、業務ではBacklogを広く使っているので、これは近いうちに対応します。または、新しいIssueの登録と編集もファイルシステム経由でできたら便利かもしれないなと思っているので、これも対応するかもしれません。

その他の情報

FUSEで実装したけどうまく動作しない

動作は間違っていないのにうまくファイルとして扱えない場合、大半はGetAttrで必要な値をセットしていないか、間違っていることが多いです。このあたりを見直してみましょう。

macOSno FUSE devices foundエラー

macOSで実行した時、

no FUSE devices found

というエラーになる場合は、おそらくosxfuse.kextの拡張がロードされていません。以下のコマンドで、FUSE for macOSカーネル拡張をロードしてから試してみてください。

$ sudo kextload /Library/Filesystems/osxfuse.fs/Contents/Extensions/10.12/osxfuse.kext

Unix-timeと日時の相互変換

この記事はQiitaで公開されていました

Unixコマンドで日時の変換をする方法が、OSによって全然違うのでまとめました。

日時からUnix-timeへの変換

ローカル時刻からUnix-time

2015年12月1日(JST)をUnix-timeに変換するコマンドです。

macOS:
$ date -v15y -v12m -v1d -v0H -v0M -v0S +%s
1448895600

Linux:
$ date --date='2015-12-01 00:00:00' +%s
1448895600

Plan9:
% seconds '1 dec 2015 00:00:00'
1448895600

macOSだけ、時刻部分を省略すると現在の時刻がそのまま使われます。LinuxPlan 9は、時刻部分が0:00:00として扱われます。

UTC表記の時刻からUnix-time

2015年12月1日(UTC)をUnix-timeに変換するコマンドです。

macOS:
$ date -u -v15y -v12m -v1d -v0H -v0M -v0S +%s
1448928000

Linux:
$ date -u --date='2015-12-01 00:00:00' +%s
1448928000

Plan9:
% seconds '1 dec 2015 00:00:00 UTC'
1448928000

Unix-timeから日時への変換

Unix-timeからローカル時刻

macOS:
$ date -r 1448895600
Tue Dec  1 00:00:00 JST 2015

Linux:
$ date -d @1448895600
Tue Dec  1 00:00:00 JST 2015

Plan9:
% date 1448895600
Tue Dec  1 00:00:00 JST 2015

Unix-timeからUTC表記の時刻

macOS:
$ date -u -r 1448895600
Mon Nov 30 15:00:00 UTC 2015

Linux:
$ date -u -d @1448895600
Mon Nov 30 15:00:00 UTC 2015

Plan9:
% date -u 1448895600
Mon Nov 30 15:00:00 GMT 2015

Sierraでgo1.7.3のコンパイル

この記事はQiitaで公開されていました

macOS SierraでソースからGoをコンパイルできなくて調べた内容です。macOSなら公式からバイナリパッケージが提供されているので、自分でコンパイルしなくてもそれを使えばいいだけなのですが、他の環境でも使える内容だったので書いてます。

めんどくさい人用のまとめ

  • バイナリパッケージを使いましょう

前提

Go 1.5から、Go自身でコンパイラを記述するようになっているので、ソースからビルドするときに前バージョンのGoコンパイラが必要です。(デフォルトでは~/go1.4) macOS Sierraでは、Go 1.6.3または1.7以上でなければうまく動作しません。 なので~/go1.4を使ってソースからGoをビルドすると、~/go1.4コンパイラ自体がクラッシュします。 具体的には、go_bootstrapのコンパイル時に、

failed MSpanList_Insert 0xXXXX 0xXXXX 0x0
fatal error: MSpanList_Insert

のようなエラーで停止します。なので最新のソースからGoコンパイラをビルドするために、~/go1.4を1.6.3以上のバージョンで置き換える必要があります。

Go 1.7の準備

手元の環境(macOS Sierra)では動かないことがわかったので、まずはLinux環境を用意します。ここではDockerを使っていますが、他のOSで作業をする場合も違いはありません。

$ docker run -ti --rm -v $(pwd):/mnt/term golang:1.7.1

コンテナが起動したら、手順で必要になるツール類をインストールします。(以下、bash$プロンプトはコンテナ内で実行するコマンドを表します)

bash$ apt-get update
bash$ apt-get install bzip2

あとはソースをビルドするだけなのですが、ターゲットとなる環境のGOOSGOARCHを忘れないようにしましょう。macOSで使うコンパイラが必要なので、ここではdarwinamd64です。

bash$ git clone https://github.com/golang/go.git
bash$ cd go/src
bash$ GOOS=darwin GOARCH=amd64 GOROOT_BOOTSTRAP=/usr/local/go ./bootstrap.bash

ビルドが終わったら、2つ上のディレクトリにアーカイブが作られているのでそれをmacOS~/go1.7に展開します。

bash$ mv ../../go-darwin-amd64-bootstrap.tbz /mnt/term/
$ bunzip2 go-darwin-amd64-bootstrap.tbz
$ tar xf go-darwin-amd64-bootstrap.tar
$ mv go-darwin-amd64-bootstrap ~/go1.7

ここまで終わったらコンテナは不要です。macOSに戻って、以前の~/go1.4の代わりに~/go1.7を使ってコンパイラをビルドしましょう。

$ GOROOT_BOOTSTRAP=~/go1.7 ./all.bash

参考