docker上にgoの環境作れたと思ったけど作れていなかった話

5 min read読了の目安(約4500字

以前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
handler.go
package handler
// 略
main.go
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を実行します。そうすると以下のようなファイルが作成されます。

app/go.mod
module app

go 1.15

これによりappディレクトリがモジュールとして追加されたので、以下のようにモジュールを作成します。

/go/src/app # tree
.
├── Dockerfile
├── go.mod
├── main
├── main.go
# 以下が作成するモジュール
├── mod
│   └── handler
│       └── handler.go
handler.go
package handler

import (
	"html/template"
	"log"
	"net/http"
)

// HelloHandler モジュールの読み込み確認用
func HelloHandler() string {
	return "hello Handler"
}

これをmain.goから読み込んであげます。

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

を書き加えて以下のようにします。

.realize.yaml
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の設定を元にしているからエラーを履いてしまうのだと思います。そこらへんに詳しい方いたらぜひ教えていただきたいです。

読んでいただきありがとうございました。