LoginSignup
5

More than 5 years have passed since last update.

IBM Cloud を利用した Go言語のアプリ開発入門(その2)

Last updated at Posted at 2018-01-16

Go言語のコードをスクラッチから書いて、IBM Cloud プラットフォームの Cloud Foundry アプリケーションとして公開するまでを試しました。なんとも、Go言語を書いた事が無い初心者なので、Go言語のプログラムの書き方を参考にして、一歩一歩進めていきます。

gopher.png

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でリポジトリを作成して、コードをアップロードします。

スクリーンショット 2018-01-14 10.44.26.png

作成したディレクトリに移動して

$ 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の直下に、以下のファイルを追加します。

config.json
{
  "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パッケージにテストを追加します。

loadConfig_test.go
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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5