docker上にgoの環境作れたと思ったけど作れていなかった話
以前dockerを使ってgoの環境構築やdockerを使ってgoの環境構築(DBと接続)を書きました。これでgoを使ってサーバを作れるぞと意気込み作業を始めたのですが詰まりました。多分初心者の人しかつまずかないところだと思います。
- main関数の処理をモジュールに分けてインポートしようと思ったら出来なかった。
- インポートできるようになったら今度はホットリロードが上手く行かなかった。
この2つを解決するのにコンテナの権限周りをいじったりいろいろ検討外れなことをしてしまったので書き残そうと思います。
モジュールに分けたらインポートが出来なかった
単純にモジュールに分けた
htmlファイルのパース処理、ハンドラー関数、DBの処理などmain.goの中に書くには少し多すぎるのでモジュールごとに分けることにしました。モジュールに分ける方法は以前説明したパッケージにする方法がありますが、これは機能ごとに切り出し再利用できるようにするイメージがあります。ここではこのプロジェクト以外では使わなそうな処理の切り分けをするだけなのでパッケージにするのは違うと思いました。また処理内容も頻繁に書き換える可能性があるのでその都度go install
でパッケージ化するのも面倒です。
調べると相対パスで指定することが出来そうなのでそれでやってみることにしました。
/go/src/app # tree
.
├── handler
│ └── handler.go
├── main.go
└── public
├── login.html
├── register.html
package handler
// 略
package main
import (
"html/template"
"log"
"net/http"
"./handler"
)
// 略
このようにhandler関数だけを切り出してmain.goで読み込んであげると以下のようにエラーが出ます。
$ docker-compose exec app sh
/go/src/app # go run main.go
build command-line-arguments: cannot find module for path _/go/src/app/handler
パスの指定を変えたりいろいろ試しましたが上手く行かず原因はわかりませんでした。相対パスを指定する方法は曖昧さ回避のために公式でも非推奨なので別の方法を探しました。
go_modulesを使う
go_modulesなるものがあるみたいなのでそれで指定してあげようと思いました。
go_modulesはJavaScriptでいうnpmのようなパッケージ管理ツールのようです。使ってないモジュールや使用しているけどダウンロードしていないモジュールを調べていい感じにしてくれるgo mod tidy
や、依存しているモジュールのファイルツリーのハッシュ値が記録され、ビルド時に確認してくれる機能などがあります。
go_modulesについて調べてみたのですがネットの記事での一般的な使い方と今回のやり方で、私の中でリンクしない部分があるので今から紹介するやり方でなぜできるのかを説明することは出来ません(もちろん今から紹介するやり方もネットで見つけたものですが…)。あくまでこうしたら出来たというtips的なものとして読んでいただければと思います。
プロジェクトのルートディレクトリにgo.mod
を作成します。例えば以下のようなディレクトリ配置の場合、コンテナ内にマウントするappディレクトリがプロジェクトディレクトリになるのでそこのルートに作成します。
$ tree
.
├── app
│ ├── Dockerfile
│ ├── main.go
│ └── public
│ ├── login.html
│ ├── register.html
│ ├── template
│ │ ├── footer.html
│ │ └── header.html
│ └── user.html
├── docker-compose.yml
└── mysql
├── Dockerfile
├── data
├── init
│ └── 1_create.sql
└── my.cnf
作成はappディレクトリで$ go mod init
を実行します。そうすると以下のようなファイルが作成されます。
module app
go 1.15
これによりappディレクトリがモジュールとして追加されたので、以下のようにモジュールを作成します。
/go/src/app # tree
.
├── Dockerfile
├── go.mod
├── main
├── main.go
# 以下が作成するモジュール
├── mod
│ └── handler
│ └── handler.go
package handler
import (
"html/template"
"log"
"net/http"
)
// HelloHandler モジュールの読み込み確認用
func HelloHandler() string {
return "hello Handler"
}
これをmain.goから読み込んであげます。
package main
import (
"html/template"
"log"
"net/http"
"app/mod/handler" //go.modで指定したapp以下のパス
)
// 略
これで自作のモジュールも簡単に読み込むことが出来ます。
しかしこれは問題があり、ホットリロードを実現するrealize
が使えなくなってしまいます。どうやらちゃんと設定してあげないとgo.modと喧嘩してしまうみたいなので次にその設定を紹介します。
ホットリロードが出来なくなった
realizeで起きるエラー
先ほどまででgo run main.go
ならばちゃんと動きます。
/go/src/app # go run main.go
2020/10/20 04:14:36 hello Handler
2020/10/20 04:14:36 Server listening on http://localhost:8080/register
しかしrealize start
だと止まってしまいます。
/go/src/app # realize start
[04:13:13][APP] : Watching 2 file/s 5 folder/s
[04:13:13][APP] : Install started
[04:13:13][APP] : Install
exec: not started
調べるとどうやら.realize.yaml
を書き換えることで対応出来そうなので書き換えます。
realize.yamlの書き換え
install:
status: true
method: go build -o main
run:
status: true
method: ./main
を書き加えて以下のようにします。
settings:
legacy:
force: false
interval: 0s
schema:
- name: app
path: .
commands: {}
commands:
install:
status: true
method: go build -o main
run:
status: true
method: ./main
watcher:
extensions:
- go
paths:
- /
ignored_paths:
- .git
- .realize
- vendor
これで実行すると以下のようにちゃんと実行出来ます。
/go/src/app # realize start
[04:22:39][APP] : Watching 2 file/s 5 folder/s
[04:22:39][APP] : Install started
[04:22:39][APP] : Install completed in 0.366 s
[04:22:39][APP] : Running..
[04:22:39][APP] : 2020/10/20 04:22:39 hello Handler
[04:22:39][APP] : 2020/10/20 04:22:39 Server listening on http://localhost:8080/register
この方法だとプロジェクトディレクトリに実行ファイルが出来てしまいますが、私は気にしないのでこれで良しとします。
最後に
調べているとテストなどやることを増やしていくとそれに合わせて設定や便利ツールのインストールも増えていきそうな感じがしました。それらについても必要になったときにやってみてまた記事にするかもしれません。
あと今使っているエディタのvscodeだとインポート出来ないモジュールに赤波線を引いたり、リンターがエラーを出してしまいます(もちろんコンテナ内では問題なく実行出来ます)。おそらくコンテナではなくローカルのGoの設定を元にしているからエラーを履いてしまうのだと思います。そこらへんに詳しい方いたらぜひ教えていただきたいです。
読んでいただきありがとうございました。
Discussion