Go言語のコードをスクラッチから書いて、IBM Cloud プラットフォームの Cloud Foundry アプリケーションとして公開するまでを試しました。なんとも、Go言語を書いた事が無い初心者なので、Go言語のプログラムの書き方を参考にして、一歩一歩進めていきます。
Go言語のコードの書き方
Go言語のプロジェクトのディレクトリ構造の概要
- すべてのGoコードを1つのワークスペースに保持
- ワークスペースは、Gitなどでバージョン管理されるリポジトリが含まれる
- 各リポジトリには1つ以上のパッケージが含まれています。
- 各パッケージは、1つまたは複数のGoソースファイルで構成されています。
- パッケージのディレクトリへのパスによって、インポートパスが決まります。
Go言語では、ワークスペースの全てをバージョン管理リポジトリで管理しない点が注意です。
ワークスペース
ワークスペースには、3つのディレクトリがあります。 go tool は、srcをビルドし、結果のバイナリをpkgおよびbinディレクトリにインストールします。
- src 依存するパッケージのソースコード、開発対象のソースコード
- pkg パッケージのオブジェクト形式
- bin 実行形式のコマンド
一般的なワークスペースには、多くのパッケージとコマンドを含む多くのソースリポジトリが含まれています。 ほとんどのGoプログラマは、Goソースコードと依存関係をすべて単一のワークスペースに保持します。
GOPATH環境変数
環境変数 GOPATHは、ワークスペースの場所を指定します。 デフォルトでは、ホームディレクトリ内のgoというディレクトリになります。
指定した場所で作業するために、GOPATHを設定する必要があります。
コマンドgo env GOPATHは有効な現在のGOPATHを表示します。 環境変数が設定されていない場合、デフォルトの場所が表示されます。
$ go env GOPATH
/home/vagrant/go
便宜上、ワークスペースのbinサブディレクトリをPATHに追加します。 ~/.bash_profile
の末尾に設定する例です。
# for golang workspace
export GOPATH="$HOME/share/go_workspaces/webapp01"
export PATH="$GOPATH/bin:$PATH"
インポートパス
インポートパスは、パッケージを一意に識別する文字列です。 このパスは、ワークスペースまたはリモートリポジトリに対応します。
標準ライブラリのパッケージには、 "fmt"や "net/http"などの短いインポートパスが与えられます。独自のパッケージでは、標準ライブラリまたは他の外部ライブラリへの将来の追加と衝突する可能性の低い基本パスを選択する必要があります。
コードをソースリポジトリのどこかに置いておくと、そのソースリポジトリのルートをベースパスとして使用する必要があります。たとえば、GitHubアカウントをgithub.com/userに持っていれば、そのパスがベースパスになります。
ビルドする前に、コードをリモートリポジトリに公開する必要はありません。あなたのコードをいつか公開するかのようにコードを整理するのは良い習慣です。実際には、標準ライブラリとより大きいGoエコシステムに固有の任意のパス名を選択できます。
github.com/userを基本パスとして使用します。ワークスペース内にソースコードを保存するディレクトリを作成します。
例えば、自分のGitHubのユーザーは takara9
でURLは https://github.com/takara9 なので、以下の様になります。
mkdir -p $GOPATH/src/github.com/takara9
Go言語の最初のコード
最もシンプルなウェブサーバのコードで、go_webserverというディレクトリを作成して、main.go を書きます。
$ cd $GOPATH
$ tree
.
└── src
└── github.com
└── takara9
└── go_webserver
└── main.go
以下は、main.goのコードです。 このコードは8080ポートでHTTPリクエストをリッスンして、メッセージを返します。
package main
import (
"fmt"
"net/http"
)
func handler(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Hello World, %s!", request.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
ビルド
プロジェクトのルートディレクトである $GOPATHへ移動して、go install
を実行します。
$ cd $GOPATH
$ go install github.com/takara9/go_webserver
そうすると、以下の様にbinが出来て、実行形式のファイルがデプロイされます。
$ tree
.
├── bin
│ └── go_webserver
└── src
└── github.com
└── takara9
└── go_webserver
├── main.go
└── README.md
テスト
コマンドを実行して、別のターミナルから、curlを使って HTTPリクエストを送信します。
$ $GOPATH/bin/go_webserver
テキストの文字列が帰って来れば、テスト成功です。
$ curl http://localhost:8080/
Hello World, !
GitHub リポジトリへの登録
ここで書いたコードをリポジトリに登録してして再利用できる様にします。 GitHubでリポジトリを作成して、コードをアップロードします。
作成したディレクトリに移動して
$ pwd
/home/vagrant/share/go_workspaces/webapp01/src/github.com/takara9/go_webserver
以下を順次実行して、GitHubに登録します。
$ echo "# go_webserver" >> README.md
$ git init
$ git add README.md main.go
$ git commit -m "first commit"
$ git remote add origin https://github.com/takara9/go_webserver.git
$ git push -u origin master
ここで登録したコードは、次のURLで参照できます。 https://github.com/takara9/go_webserver
始めてのライブラリ
異なるリポジトリの独立したコードとして書いて、先ほどのgo_webserverから利用できる様にします。
mkdir $GOPATH/src/github.com/takara9/go_util
このコードは、起動するとコンフィグを読み取り、値を外部変数にセットするものです。
package go_util
import (
"os"
"log"
"encoding/json"
)
var Config Configuration
var Logger *log.Logger
type Configuration struct {
IpAddress string
TcpPort string
ReadTimeout int64
WriteTimeout int64
Static string
}
func OpenLog(logFileName string) int {
file, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open log file", err)
}
Logger = log.New(file, "INFO ", log.Ldate|log.Ltime|log.Lshortfile)
return(0)
}
func LoadConfig(configFileName string) int {
file, err := os.Open(configFileName)
if err != nil {
log.Fatalln("Cannot open config file", err)
}
decoder := json.NewDecoder(file)
Config = Configuration{}
err = decoder.Decode(&Config)
if err != nil {
log.Fatalln("Cannot get configuration from file", err)
}
return(0)
}
ポインタを外部参照させる場合は、大文字で記述する。このため config
では外部参照できないので、Config
とする。
同様に、Logger, func OpenLog, func LoadConfig も大文字で始まる様に命名している。
メインのパッケージから上記のモジュールを読める様に変更する、それから、IBM Cloud CloudFoundry アプリとして動作する様に、ポート番号は環境変数を優先する様にする。
package main
import (
"os"
"fmt"
"net/http"
"github.com/takara9/go_util"
)
func handler(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Hello World, %s!\n", request.URL.Path[1:])
go_util.Logger.Printf("Hello World, %s!\n", request.URL.Path[1:])
}
func main() {
go_util.OpenLog("logfile.txt")
go_util.LoadConfig("config.json")
port := ":" + os.Getenv("PORT")
if port == ":" {
port = go_util.Config.TcpPort
}
http.HandleFunc("/", handler)
http.ListenAndServe(port, nil)
}
ビルドしてインストールする
$ go install github.com/takara9/go_webserver
これが完了すると、pkgのディレクトリが追加され、go_utilというパッケージが利用されていることが解ります。
$ tree
.
├── bin
│ └── go_webserver
├── pkg
│ └── linux_amd64
│ └── github.com
│ └── takara9
│ └── go_util.a
└── src
└── github.com
└── takara9
├── go_util
│ └── loadConfig.go
└── go_webserver
├── main.go
└── README.md
10 directories, 5 files
コンフィグファイルをロードする様にユーティリティモジュールが作られているので、実行すると以下の様にエラーになります。
$ ./bin/go_webserver
2018/01/14 04:12:43 Cannot open config file open config.json: no such file or directory
そこで、$GOPATHの直下に、以下のファイルを追加します。
{
"IpAddress" : "0.0.0.0",
"TcpPort" : "8080",
"ReadTimeout" : 10,
"WriteTimeout" : 600,
"Static" : "public"
}
再度、バックグランドで実行して、curlコマンドでアクセスすることで、結果を得られます。
$ ./bin/go_webserver &
$ curl http://localhost:8080/
パッケージ名について
- パッケージ内のすべてのファイルは同じ名前が必要
- パッケージ名はインポートパスの最後の要素であること。 crypto/rot13ならパッケージ名はrot13
- 実行可能コマンドは常にpackage mainが必要
- インポートパスは一意でなければなりません。
詳細参照先: https://golang.org/doc/effective_go.html#names
パッケージのテスト
Go言語には、go testコマンドとテストパッケージで構成される軽量テストフレームワークがあります。
_test.goで終わるファイルを作成し、TestXXXという名前の関数にfunc(t * testing.T)という名前の関数が含まれているテストを作成します。 テストフレームワークは、このような各機能を実行します。 関数がt.Errorやt.Failなどの関数を呼び出すと、テストは失敗したとみなされます。
次のGoコードを含む$GOPATH/src/github.com/takara9/go_util/loadConfig_test.goファイルを作成して、go_utilパッケージにテストを追加します。
package go_util
import "testing"
func TestCase001(t *testing.T) {
ret := 0
logFn := "_test_logfile.txt"
ret = OpenLog(logFn)
if (ret != 0) {
t.Errorf("openLog(%s)",logFn)
}
Logger.Print("Open log")
cnfFn := "_test_config.json"
Logger.Print("Load _test_config.json")
ret = LoadConfig(cnfFn)
if (ret != 0 ) {
t.Errorf("loadConfig(%s)",cnfFn)
}
if (Config.IpAddress != "0.0.0.0") {
t.Errorf("Config.IpAddress(%s)",Config.IpAddress)
}
if (Config.TcpPort != "8080") {
t.Errorf("Config.TcpPort(%s)",Config.TcpPort)
}
Logger.Print("END OF TEST")
}
テストの実行例として、2つのディレクトリを変えての実行結果です。
$ go test github.com/takara9/go_util
ok github.com/takara9/go_util 0.003s
パッケージのディレクトリに移動して実行したケースです。
$ cd src/github.com/takara9/go_util$
$ go test
PASS
ok github.com/takara9/go_util 0.001s
リモートパッケージ
go get github.com/takara9/go_webserver
を実行すると Gitから取得して、ビルドまで完了させる。
$ export GOPATH=/home/vagrant/share/go_workspaces/webapp02
$ mkdir $GOPATH
$ cd $GOPATH
$ go get github.com/takara9/go_webserver
$ tree
.
├── bin
│ └── go_webserver
├── pkg
│ └── linux_amd64
│ └── github.com
│ └── takara9
│ └── go_util.a
└── src
└── github.com
└── takara9
├── go_util
│ ├── config.json
│ ├── loadConfig.go
│ ├── loadConfig_test.go
│ └── README.md
└── go_webserver
├── main.go
└── README.md
IBM Cloud CloudFoundry PaaS へのデプロイ
godepパッケージ・マネージャー
IBM Cloud の CloudFoundry アプリケーションとして Go言語を実行したい場合は、パッケージマネージャーを利用します。
CloudFoundry のビルドパックが対応してしているパッケージ・マネージャーは、Godep, Glide そして dep だそうです。
いずれ dep に置き換わるかもしれませんが、もっとも利用されていると思われる godep を利用したいと思います。
goenv を利用して Go言語を利用している場合、godepが正常に動作しない様です。 次の様なエラーで止まってしまいます。
~/go/webapp01/src/github.com/takara9/go_webserver$ godep save
godep: Package (fmt) not found
godep save -d -v github.com/takara9/go_webserver log 2>&1
とすると、サーチパスを表示して来れますが、
原因がはっきしませんが、goenvを利用せずに、golangのインストール手順 Getting Start, The Go Programing Languageを利用することで、godepは正常に動作します。
$GOPATHのディレクトリで、次のオプションをつけて、godepを実行します。
godep save github.com/takara9/go_webserver
これによって、Godeps と vendor のディレクトリが作成されます。
~/go/webapp01$ tree -L 2
.
├── bin
│ ├── godep
│ └── go_webserver
├── config.json
├── Godeps
│ ├── Godeps.json
│ └── Readme
├── pkg
│ └── linux_amd64
├── src
│ └── github.com
└── vendor
└── github.com
CloudFoundry のマニフェストを作成
IBM Cloud CloudFoudryアプリとしてデプロイするために、マニフェストを作成します。詳しい作成方法は、Go Buildpack を参照してください。
以下のファイルで、ポイントは、command のフィールドに、go_webserverをセットします。 アップロードした後に、ビルドするので、binを付ける必要はありません。
applications:
- name: go-webserver
path: .
memory: 128M
instances: 1
domain: mybluemix.net
host: go-webserver
disk_quota: 1024M
command: go_webserver
buildpack: https://github.com/cloudfoundry/go-buildpack.git
env:
GOVERSION: go1.9.2
GOPACKAGENAME: github.com/takara9/go_webserver
IBM Cloud CloudFoundryアプリとしてデプロイ
cf push
する前に、別のディレクトリを作って、以下のファイルをコピーします。 bin,pkg,srcが存在していると、ビルドパックなのか原因ははっきりしないですが、デプロイに失敗してしまうので、この様にします。
vagrant@vagrant-ubuntu-trusty-64:~/go/webapp02$ tree
.
├── config.json
├── Godeps
│ ├── Godeps.json
│ └── Readme
├── manifest.yml
└── vendor
└── github.com
└── takara9
├── go_util
│ ├── loadConfig.go
│ ├── README.md
│ ├── _test_config.json
│ └── _test_logfile.txt
└── go_webserver
├── main.go
└── README.md
これで bx cf push
することで、IBM Cloud から公開することができます。
vagrant@vagrant-ubuntu-trusty-64:~/go/webapp02$ bx cf push
Invoking 'cf push'...
Using manifest file /home/vagrant/go/webapp02/manifest.yml
Updating app go-webserver in org takara@jp.ibm.com / space dev as takara@jp.ibm.com...
OK
Using route go-webserver.mybluemix.net
Uploading go-webserver...
Uploading app files from: /home/vagrant/go/webapp02
Uploading 4.8K, 16 files
Done uploading
OK
Stopping app go-webserver in org takara@jp.ibm.com / space dev as takara@jp.ibm.com...
OK
Starting app go-webserver in org takara@jp.ibm.com / space dev as takara@jp.ibm.com...
Creating container
Successfully created container
Downloading app package...
Downloaded app package (3.9K)
Downloading build artifacts cache...
Downloaded build artifacts cache (220B)
Staging...
-----> Download go 1.9.1
-----> Running go build supply
-----> Go Buildpack version 1.8.15
-----> Checking Godeps/Godeps.json file
-----> Installing godep v79
Download [https://buildpacks.cloudfoundry.org/dependencies/godep/godep-v79-linux-x64-9e37ce0f.tgz]
-----> Installing glide v0.13.1
Download [https://buildpacks.cloudfoundry.org/dependencies/glide/glide-v0.13.1-linux-x64-4959fbf0.tgz]
-----> Installing dep v0.3.2
Download [https://buildpacks.cloudfoundry.org/dependencies/dep/dep-v0.3.2-linux-x64-8910d5c1.tgz]
$GOVERSION = go1.9.2
**WARNING** Using $GOVERSION override.
If this isn't what you want please run:
cf unset-env <app> GOVERSION
-----> Installing go 1.9.2
Download [https://buildpacks.cloudfoundry.org/dependencies/go/go1.9.2.linux-amd64-f60fe671.tar.gz]
-----> Running go build finalize
-----> Running: go install -tags cloudfoundry -buildmode pie vendor/github.com/takara9/go_webserver
Exit status 0
Staging complete
Uploading droplet, build artifacts cache...
Uploading build artifacts cache...
Uploading droplet...
Uploaded build artifacts cache (217B)
Uploaded droplet (2.3M)
Uploading complete
Stopping instance a1480c1c-a629-4aa0-802b-0ff59e132c06
Destroying container
Successfully destroyed container
1 of 1 instances running
App started
OK
App go-webserver was started using this command `go_webserver`
Showing health and status for app go-webserver in org takara@jp.ibm.com / space dev as takara@jp.ibm.com...
OK
requested state: started
instances: 1/1
usage: 128M x 1 instances
urls: go-webserver.mybluemix.net
last uploaded: Tue Jan 16 06:44:34 UTC 2018
stack: cflinuxfs2
buildpack: https://github.com/cloudfoundry/go-buildpack.git
state since cpu memory disk details
# 0 running 2018-01-16 06:45:36 AM 0.0% 4.8M of 128M 8.9M of 1G
これでデプロイ完了です。 次はcurl
でテストして、確認します。
Last login: Tue Jan 16 13:56:34 on ttys002
imac:~ maho$ curl https://go-webserver.mybluemix.net/
Hello World, !
imac:~ maho$ curl https://go-webserver.mybluemix.net/xyz
Hello World, xyz!
まとめ
goenvでgodepが正しく動作しないという事実に気づくまで、時間を使ってしまいましたが、なんとか、デプロイして公開するところまで、完了して良かったです。
参考資料
[1] How to Write Go Code https://golang.org/doc/code.html
[2] 今さらだけど、Go言語に入門するための情報源 https://qiita.com/MahoTakara/items/10fede35c03db1e3b849