【Go(golang)×Docker】自作パッケージの関数を呼び出す方法

## はじめに

この記事では、**docker×go環境で自作パッケージとして切り出した関数を実行する方法**を書いています。

別パッケージの関数がうまく読み込めず苦戦した（go modulesを理解できていなかった。。）ので、備忘録的にまとめました。

#### 前提
- Dockerがインストールされていること。

#### 実行環境
- macOS Catalina: 10.15.4
- docker version: 19.03.13

### 目次

- dockerによる環境構築
- main.goファイルを作成
- シンプルなプログラムを実行
- サブパッケージに関数を切り出す
- 切り出した関数をmain.goで読み込んでプログラム実行

### 最終的なディレクトリ構造とゴール

```
.
|--Dockerfile
|--docker-compose.yml
|--go.mod
|--main.go
|--pokemon
|  |--houen.go
|  |--kantou.go
|  |--secret.go
```

上記、pokemonパッケージ下の各ファイルの関数をmain.goでインポートして実行するところまで。

## dockerによる環境構築

まずは、dockerを使ってGoのプログラム実行環境を構築していきます。

<br>

① ルートディレクトリと各種dockerファイルを作成します。

```:ターミナル
2022projects % mkdir go-sample && cd $_
go-sample % touch {Dockerfile,docker-compose.yml}
```

<br>

② Dockerfileを書いていきます。

```:Dockerfile
# Dockerイメージ指定 Doc: https://hub.docker.com/_/golang
FROM golang:1.18.1-alpine
# 作業ディレクトリ作成
RUN mkdir /go/src/app
# コンテナログイン時のディレクトリ指定
WORKDIR /go/src/app
# ホストのファイルをコンテナの作業ディレクトリに移行
ADD . /go/src/app
```

<br>

③docker-compose.ymlを書いていきます。

```:docker-compose.yml
# composeファイルのバージョン指定 doc: https://docs.docker.jp/compose/compose-file.html
version: "3"
services:
    app: # service名
        build: . # ビルド時に参照するDockerファイル指定
        tty: true # コンテナの正常終了を防ぐ(≒ 永続化)
        volumes:
            # マウントするディレクトリ指定（ホストディレクトリ : コンテナディレクトリ）
            - .:/go/src/app
```

<br>

④コンテナを作成します。
```:ターミナル
# コンテナ作成
go-sample % docker-compose build
#　作成されたコンテナの確認
go-sample % docker ps
# 作成されたイメージの確認
go-sample % docker images

# docker psの例
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
23a42594db7c7e        go-demo_app         "bash"              2 days ago          Up About an hour                        go-demo_app_1

# docker imagesの例
REPOSITORY            TAG                 IMAGE ID            CREATED              SIZE
go-sample_app         latest              c733dc31a8b3a        About a minute ago   329MB
golang                1.18.1-alpine       cd6f429de0e957        2 days ago           329MB
```

<br>

## Goファイルを作成してプログラムを実行してみる

<br>
次は、go言語でシンプルな文字列を返すプログラムを書いてみます。

main.goファイルを作成。

```:ターミナル
go-sample % touch main.go
```

<br>

main.goファイルにて、文字列を返すプログラムを書いてみる。

```:main.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hey Siri!")
}
```

:::note info
メモ
- Goでは、パッケージという単位でコードを管理する。
- 異なるパッケージの関数を利用する場合は、importが必要。
- 外部パッケージの関数を利用する際は、`パッケージ名.関数名`として明示的に指定。
- ↑でいう```fmt```など、Goの標準ライブラリに関してはパッケージ名をimportするだけで、簡単にパッケージ内の関数を利用できる。
:::

※ Goの標準パッケージ一覧
https://xn--go-hh0g6u.com/pkg/

<br>

**main.goファイルを実行してみる。**

```:ターミナル
go-sample % docker-compose up -d
go-sample % docker-compose exec app go run main.go

############### 出力結果 ###############
Hey Siri!
```

出力に成功しました！

:::note info
メモ
`go run`:　コンパイルとプログラムの実行を行ってくれる。
`go build`: コンパイルを行う。

:::

<br>

### 自作パッケージの作成

main.goファイル単体でのプログラム実行ができたので、次は**自作パッケージを作成→main.goで読み込ませて実行**できるようにします。

```:ターミナル
go-sample % mkdir pokemon
go-sample % touch {pokemon/kantou.go,pokemon/houen.go,pokemon/secret.go}
```

<br>

作成した各goファイルにパッケージ名と関数を書いていく。

```:pokemon/kantou.go
package pokemon

func KantouBigThreePokemon() string {
	return "カントー地方の御三家: フシギダネ！・ヒトカゲ！・ゼニガメ！"
}
```

```:pokemon/houen.go
package pokemon

func HouenBigThreePokemon() string {
	return "ホウエン地方の御三家: キモリ！・アチャモ！・ミズゴロウ！"
}
```

```:pokemon/secret.go
package pokemon

func SecretBigThreePokemon() string {
	return "？？？地方の御三家: ニャオハ！？・ホゲータ！？・クワッス！？？？？？"
}
```

<br>

Golangは静的型付け言語のため、**戻り値の型を指定する必要がある（今回の例だと、文字列なのでstringを指定**)。これを怠ると、下記のようにコンパイル時にエラーが返ってきてしまうので注意。

```:エラー例
go-sample % docker-compose exec app go run main.go
pokemon/houen.go:4:9: too many return values
	have (string)
	want ()
```

<br>

自作パッケージファイルが作成できたら、ファイルをimportするためにmain.goを修正する。
```
package main

import (
	"fmt"
	"./pokemon"
)

func main() {
  fmt.Println(pokemon.KantouBigThreePokemon())
  fmt.Println(pokemon.HouenBigThreePokemon())
  fmt.Println(pokemon.SecretBigThreePokemon())
}
```

<br>

importの記述ができたので、早速main.goファイルを読み込んで見るが...
```:ターミナル
go-sample % docker-compose exec app go run main.go
main.go:5:2: package go-demo/pokemon is not in GOROOT (/usr/local/go/src/go-demo/pokemon)
```

上述のようなエラーでうまくプログラムを実行できない。

**結論、go modulesを使って自作パッケージを読み込ませる必要がありました。**

:::note info
メモ
go modulesとは、**Goパッケージの集合体であるモジュールを管理**してくれるシステム。

このgo modulesが提供する**go.modファイルに、「モジュールのインポートパスとバージョン情報」を記述**する。

:::

先ほど失敗した自作パッケージの読み込みは、このモジュールのインポートパスを使うことで解消できます。


## go.modファイルを作成して自作パッケージの関数を実行する

<br>

ということで、自作パッケージを読み込むためにgo.modファイルを作成します。

```:ターミナル
# go mod init モジュール名
go-sample % docker-compose exec app go mod init go-sample
```

<br>

すると、下記のようなファイルが出来上がります。
```:go.mod
module go-sample

go 1.18
```

<br>

このモジュール名を使って、main.goファイルの自作モジュールをimportする記述を修正

```:main.go
package main

import (
	"fmt"
    // ↓　ここをモジュール名/パッケージ名に変更　| 相対パスは使えないので注意が必要。　↓
	"go-sample/pokemon"
)

func main() {
  fmt.Println(pokemon.KantouBigThreePokemon())
　　　　fmt.Println(pokemon.HouenBigThreePokemon())
　　　　fmt.Println(pokemon.SecretBigThreePokemon())
}
```

<br>

改めて、Goプログラムを実行する。
```:ターミナル
go-sample % docker-compose exec app go run main.go
############### 出力結果 ###############
カントー地方の御三家: フシギダネ！・ヒトカゲ！・ゼニガメ！
ホウエン地方の御三家: キモリ！・アチャモ！・ミズゴロウ！
？？？地方の御三家: ニャオハ！？・ホゲータ！？・クワッス！？？？？
```
無事、成功しました！　

### 参考記事

[Go のモジュール管理【バージョン 1.17 改訂版】](https://zenn.dev/spiegel/articles/20210223-go-module-aware-mode)

[【Go言語】GOPATHの外に作ったプロジェクトでサブパッケージをインポートする](https://zenn.dev/turara/articles/golang-import-subpackages)

一応、githubにもソースコードを公開しているので良かったらご確認ください。

https://github.com/kazuma630/go-demo

完