【Docker】Golangのwebアプリケーションをdocker化する(3)
今回はマルチステージビルドをします。
とその前に、livereloadができるツール,Airを導入しようと思います。
こちら導入後、マルチステージビルド対応の際に開発環境だけに適用するようにしたいと思います。
Airは、Goで実装したアプリケーションをホットリロードしてくれるツールです。
以下のようにDockerfileを修正します
FROM golang:1.18-alpine WORKDIR /go/src COPY . . RUN go install github.com/air-verse/air@latest ←追加!!! RUN go mod download RUN go build -o main ./main.go // CMD ["./main"] CMD ["air", "-c", ".air.toml"] ←追加修正
それでは、docker compose up !
早速エラー発生。
package slices is not in GOROOT (/usr/local/go/src/slices)
golang:1.18-alipneにはスライスがまだ実装されていないようです。
なのでバージョン上げろってことですね。
- FROM golang:1.18-alpine + FROM golang:tip-alpine3.22
再度、docker compose up!!!

起動されました〜〜!
しかし、mainファイルをいじってもホットリロードできていない様子。。。
更新してもビルドが走りません。。なんでだろう。。
これは速攻で解決したのですが、
APIコンテナでvolumeを設定していませんでした(ホスト側でmain.goを修正しても、コンテナ内にその修正が反映されてない)
docker-compose.ymlを以下のように修正します
services:
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
container_name: web-server-gin
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
volumes: ←追加!!!
- ./:/go/src
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
working_dir: /go/src
stdin_open: true
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
healthcheck:
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-u",
"mysql",
"-pmypassword",
]
timeout: 20s
retries: 10
volumes:
my-db:
ローカルで編集したgoファイルをコンテナ内に反映できていなかったようですね!
再度、docker compose up !!!
無事、起動。

goファイルを編集してみる
-c.IndentedJSON(http.StatusOK, gin.H{"message": "Hello World!!!"})
+c.IndentedJSON(http.StatusOK, gin.H{"message": "Hello World!!!!!!!!!!!!!!!!!!!!!!!!!"})

リロードしてますね〜〜!

ホットリロード完了しました!
さて、本題のマルチステージビルドです。
今回主に参考にした記事は以下です。よくまとまっていてわかりやすいです。
今回、開発環境用と本番環境用でマルチステージビルドをします。
イメージとしては 以下のような感じです。
dev ステージ → 工場でビルド(原材料・工具・作業場あり) prod ステージ → 完成品(箱詰めされた商品)だけ持ってくる倉庫 工場の作業員や工具は、商品には入らない。 → だからイメージが軽くなる
なので大まかな修正としては以下のようになります
Dockerfileにdevステージとprodステージをそれぞれ記載
docker-compose.ymlを開発用と本番用に分ける
では修正していきましょう。
まずDockerfileから
FROM golang:tip-alpine3.22 AS dev
WORKDIR /go/src
COPY . .
RUN go install github.com/air-verse/air@latest
RUN go mod download
RUN go build -o main ./main.go
CMD ["air", "-c", ".air.toml"]
FROM alpine:3.22.1 AS prod
WORKDIR /app
COPY --from=dev /go/src/main .
COPY . .
CMD [ "/app/main" ]
Dockerfile内で FROMを新たに書くと、完全に別のイメージが作られます
FROM を新たに書いているので
dev ステージで使ってたもの(Go, Air, go.mod など)は prod ステージに引き継がれません。これがマルチステージビルドの肝です。
引き継ぎたいものはCOPY --from=dev {引き継ぎたいもののパス} でその新たなステージにコピーします。
FROM golang:alpine AS dev # → GoやAirをインストール、build、mainバイナリ作成 FROM alpine AS prod # → 完全に別の新しいイメージ # devステージから欲しいファイルだけ手動でCOPYする COPY --from=dev /go/src/main .
次にdocker-compose.ymlです。 現場のdocker-compose.ymlは以下の通りです。
services:
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
container_name: web-server-gin
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
volumes:
- ./:/go/src
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
working_dir: /go/src
stdin_open: true
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
healthcheck:
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-u",
"mysql",
"-pmypassword",
]
timeout: 20s
retries: 10
volumes:
my-db:
先ほども書いた通り、本番環境用と開発環境用にdocker-compose.ymlを分けます
ではまず既存のdocker-compose.ymlを本番環境用として修正します ※本番環境用のdocker-compose.ymlのファイル名はそのまま「docker-compose.yml」としています。 修正点は以下の通りです。
services:
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
target: prod ←追記!!!
container_name: web-server-gin
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
volumes:
- ./:/go/src
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
working_dir: /go/src
stdin_open: true
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
healthcheck:
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-u",
"mysql",
"-pmypassword",
]
timeout: 20s
retries: 10
volumes:
my-db:
修正点は
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
target: prod ←追記!!!
targetのラインを追加するだけです
こちらはDockerfile内のどのステージをビルドしますか?の設定になります。
本番環境用のdocker-compose.ymlになるのでprodとしています。
先ほどはったDockerfileにて
FROM alpine:3.22.1 AS prod
と記載ありましたね。これを指定しています。
続いて開発環境用のdocker-compose.ymlです。 こちらはファイル名を「docker-compose.dev.yml」として作成します
こちらもtargetを既存のdocker-compose.ymlにtargetを追記するだけです。
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
target: dev ←追記!!!
はい、修正は以上です。
では実際に開発用と本番用を起動してみましょう。
開発環境の場合
2. イメージをビルド
docker compose -f docker-compose.dev.yml build

ビルド成功です。
ログを見ると、確かにdevステージ内のラインのみ実行できていますね。
3. コンテナを起動
docker compose up -d
Airによる ソースのライブリロード が有効になります- http://localhost:8080 にアクセスして動作を確認

開発環境が起動できました

ここでdocker-compose.dev.ymlで生成したイメージの容量を見てみましょう。 667MBです。
この容量を覚えておきてください。
Goのツールなどビルドに必要なものが諸々はいっているので容量が大きいですね。(本番では容量が小さくなります)
それと、開発環境なのでAirが有効になっているはずです。goのソース修正と同時にホットリロードが走るはずです。(本番では不要なため効かないようになります)
さて、次に本番用を起動してみます 一度 docker compose down -v docker rmi でコンテナとイメージを消しましょう。
本番環境の場合
2. イメージをビルド
docker compose build
3. コンテナを起動
docker compose up -d
起動完了しました。
今度は「docker compse build」で起動しているので、デフォルトでdocker-compose.ymlがビルドに使われています
docker-compose.ymlではtargetをprodとしていたので、本番用のビルドが走りますね。
Dockerfile内の
FROM golang:tip-alpine3.22 AS dev
WORKDIR /go/src
COPY . .
RUN go install github.com/air-verse/air@latest
RUN go mod download
RUN go build -o main ./main.go
CMD ["air", "-c", ".air.toml"]
FROM alpine:3.22.1 AS prod
WORKDIR /app
COPY --from=dev /go/src/main .
COPY . .
CMD [ "/app/main" ]
こちらのprodステージのイメージが生成されます。
target prodとしているので Dokcerfile内のprodステージだけ実行されるのかと思いきや、 devもしっかり実行しています。
prodではdevステージで作ったビルド済ファイルを持ってきて、実行するだけです。
実際に生成したイメージを見てみると

容量が20.3MB!! かなり縮小できています。
これがマルチステージビルドの美味しいところです。
あと起動してみると本番ではAirはないので、ホットリロードは起こりません。
あと開発用と本番用で必要のものを分けられる点も美味しいところです。
以上、拙い文章で、ここまで読んでくださりありがとうございました。
【Docker】Golangのwebアプリケーションをdocker化する(2)
というわけで次はDBコンテナを起動しようと思います。 以下、遭遇したエラーとその対処などつらつらと書いていきます
no matching manifest for linux/arm64/v8 in the manifest list entriesエラー
コンテナ起動時に出たエラー。
docker compose up db --build
当方M3Macで開発しているため、過去にもよく見たエラーだ。
k_tanaka@tanakakenyanoMacBook-Air web-service-gin % docker compose up db [+] Running 0/1 ⠼ db Pulling 2.4s no matching manifest for linux/arm64/v8 in the manifest list entries
解決方法はdocker-compose.ymlに以下を追記すればOK
db:
image: mysql:5.7
platform: linux/x86_64 ←これを追記
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-init.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
docker-entrypoint-initdb.d の初期化でデータが作られない。
docker-compose.ymlでdbのサービスを以下のように記載し
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d ←これ!!!
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
./sql/init:/docker-entrypoint-initdb.dをかき
./sql/init/init.sqlは以下の通り記載している。
create database testdb; user testdb DROP TABLE IF EXISTS album; CREATE TABLE album ( id INT AUTO_INCREMENT NOT NULL, title VARCHAR(128) NOT NULL, artist VARCHAR(255) NOT NULL, price DECIMAL(5,2) NOT NULL, PRIMARY KEY (id) ); INSERT INTO album (title, artist, price) VALUES ('Blue Train', 'John Coltrane', 56.99), ('Giant Steps', 'John Coltrane', 63.99), ('Jeru', 'Gerry Mulligan', 17.99), ('Sarah Vaughan', 'Sarah Vaughan', 34.98);
という格好で書いているのに、docker compose up してDBコンテナの中身に行っても、
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | +--------------------+ 1 row in set (0.02 sec)
testdb作られてない・・・
なんでや(泣)(泣)(泣)(泣)(泣)(泣)
と色々調べていると
MYSQL_DATABASEという環境変数を設定すればDB初期化時にCREATE DATABASEを勝手にしてくれるとのこと。。
ymlに環境変数を追記
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE} ←追加!!!!!!
これでdocker compose upすると無事DB作られました!!
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | testdb | +--------------------+ 2 rows in set (0.03 sec)
DB起動前にAPIがアクセスしてしまいAPIが落ちる
docker-compose.ymlは以下のような状態
depends_onを設定したのでちゃんとDB→APIの順番で起動するので大丈夫!。ではなかった。
どうやらdepends_onは起動順序を保証するだけで、起動が終わるまで待ちはしないみたいです。
なので起動順序はDB→APIの順で立ち上がっていますが、DBが起動し終わる前にAPIがアクセスしてしまい、APIコンテナが落ちてしまっていたようです。
services:
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
container_name: web-server-gin ##任意のコンテナ名を指定する
ports:
- "8080:8080"
depends_on:
- db
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
working_dir: /go/src
stdin_open: true
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
volumes:
my-db:
なので以下のように修正。
services:
go:
build:
dockerfile: ./docker/go/Dockerfile
context: .
container_name: web-server-gin
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy ←追記!!
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
working_dir: /go/src
stdin_open: true
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
healthcheck: ←追記!!
test:
[
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-u",
"mysql",
"-pmypassword",
]
timeout: 20s
retries: 10
volumes:
my-db:
ちなみに以下の記事参考にしました。
DBは作られたが肝心の初期化スクリプトが実行されていない
次々と不具合がでますね(汗)(汗)(汗)
APIコンテナとDBコンテナに疎通できたものの、肝心の init.sqlが実行できていませんでした。
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | testdb | +--------------------+ 2 rows in set (0.02 sec) mysql> use testdb; Database changed mysql> show tables;
Volumeが残っているのが原因かと思い以下の記事試しましたが解決せず。。
結果
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
って書いてるにも関わらず、 init.sqlを
においてました😀
お茶目してしまいました。。
というわけで、APIコンテナとDBコンテナの起動、連携がうまくできました!!
次回、マルチステージビルドを用いて開発用(development)と本番用(production)で処理をわける処理書いていきます!
それでは!!
【Docker】Golangのwebアプリケーションをdocker化する (1)
とりあえずdocker runで起動できるようにしてみる
Docker化するサンプルアプリはこちら
package main import ( "database/sql" "fmt" "log" "net/http" "os" "github.com/gin-gonic/gin" "github.com/go-sql-driver/mysql" ) var db *sql.DB // album represents data about a record album. type Album struct { ID int64 `json:"id"` Title string `json:"title"` Artist string `json:"artist"` Price float64 `json:"price"` } func home(c *gin.Context) { c.IndentedJSON(http.StatusOK, gin.H{"message": "Hello World!"}) } func getAlbums(c *gin.Context) { var albums []Album rows, err := db.Query("SELECT * FROM album") if err != nil { return } defer rows.Close() for rows.Next() { var album Album if err := rows.Scan(&album.ID, &album.Title, &album.Artist, &album.Price); err != nil { c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err}) return } albums = append(albums, album) if err := rows.Err(); err != nil { c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err}) return } } c.IndentedJSON(http.StatusOK, albums) } func postAlbums(c *gin.Context) { var newAlbum Album err := c.BindJSON(&newAlbum) if err != nil { c.IndentedJSON(http.StatusUnprocessableEntity, gin.H{"message": err}) return } result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", newAlbum.Title, newAlbum.Artist, newAlbum.Price) if err != nil { c.IndentedJSON(http.StatusUnprocessableEntity, gin.H{"message": err}) return } id, err := result.LastInsertId() if err != nil { c.IndentedJSON(http.StatusUnprocessableEntity, gin.H{"message": err}) return } var createdAlbum Album createdAlbum.ID = id createdAlbum.Artist = newAlbum.Artist createdAlbum.Price = newAlbum.Price createdAlbum.Title = newAlbum.Title c.IndentedJSON(http.StatusCreated, &createdAlbum) } func getAlbumByID(c *gin.Context) { id := c.Param("id") var album Album row := db.QueryRow("SELECT * FROM album WHERE id = ?", id) if err := row.Scan(&album.ID, &album.Title, &album.Artist, &album.Price); err != nil { if err == sql.ErrNoRows { c.IndentedJSON(http.StatusNotFound, gin.H{"message": err}) return } } c.IndentedJSON(http.StatusOK, &album) } func updateAlbumByID(c *gin.Context) { var updateAlbum Album id := c.Param("id") if err := c.BindJSON(&updateAlbum); err != nil { c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "bind error"}) return } _, err := db.Exec("UPDATE album SET title = ?, artist = ?, price = ? WHERE id = ?", updateAlbum.Title, updateAlbum.Artist, updateAlbum.Price, id) if err != nil { c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "update error"}) return } c.IndentedJSON(http.StatusNoContent, &updateAlbum) } func deleteAlbumByID(c *gin.Context) { id := c.Param("id") var album Album row := db.QueryRow("SELECT * FROM album WHERE id = ?", id) if err := row.Scan(&album.ID, &album.Title, &album.Artist, &album.Price); err != nil { if err == sql.ErrNoRows { c.IndentedJSON(http.StatusNotFound, gin.H{"message": err}) return } } if _, err := db.Exec("DELETE FROM album WHERE id = ?", id); err != nil { c.IndentedJSON(http.StatusUnprocessableEntity, gin.H{"message": err}) return } c.IndentedJSON(http.StatusNoContent, &album) } func main() { config := mysql.Config{ User: os.Getenv("DB_USER"), Passwd: os.Getenv("DB_PASSWORD"), Net: "tcp", Addr: fmt.Sprintf("%s:%s", os.Getenv("DB_HOST"), os.Getenv("DB_PORT")), DBName: os.Getenv("DB_NAME"), AllowNativePasswords: false, } fmt.Printf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME"), ) fmt.Println(config.FormatDSN()) var err error db, err = sql.Open("mysql", config.FormatDSN()) if err != nil { log.Fatal(err) } pingErr := db.Ping() if pingErr != nil { log.Fatal(pingErr) } fmt.Println("Connected!") router := gin.Default() router.GET("/", home) router.GET("/albums", getAlbums) router.GET("/albums/:id", getAlbumByID) router.POST("/albums", postAlbums) router.PATCH("/albums/:id", updateAlbumByID) router.DELETE("/albums/:id", deleteAlbumByID) router.Run() }
- envファイルをまず記載する
DB_USER=root DB_PASSWORD=password DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=testdb
- 次にDockerfile作成
// Dockerfile # 注意 go.mod記載のversionと合わせる # 軽量にするためalpineを使用 FROM golang:1.18-alpine # コンテナ内の作業ディレクトリを/appに設定 WORKDIR /app # ホストマシンからtodo-app内の全ファイルを、コンテナ内の作業ディレクトリ(/app)にコピー COPY . . # go.modファイルに記載された依存パッケージをdownload RUN go mod download # main.goファイルをコンパイルして、実行ファイルを作成 RUN go build -o main /app/main.go # コンテナ起動後に、実行ファイルを実行 CMD ["/app/main"]
- docker runで起動!
docker run -p 8080:80 --name web-server-gin2 web-server-gin tcp(:)/?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0 2025/07/18 22:58:15 dial tcp :0: connect: connection refused
ありゃ?接続拒否?? いやそうじゃん。環境変数渡せてないじゃん。。
- docker runで起動!(2回目)
web-service-gin % docker run -p 8080:80 \ -e DB_USER=root \ -e DB_PASSWORD=password \ -e DB_HOST=127.0.0.1 \ -e DB_PORT=3306 \ -e DB_NAME=testdb \ --name web-server-gin web-server-gin root:password@tcp(127.0.0.1:3306)/testdb?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0 2025/07/18 23:36:17 dial tcp 127.0.0.1:3306: connect: connection refused
あれ???
また接続拒否。。。
てか-eオプションめんどくさいな
調べると以下の書き方があるらしいので試す
docker run -p 8080:80 --env-file .env --name web-server-gin web-server-gin root:password@tcp(127.0.0.1:3306)/testdb?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0 2025/07/18 23:41:48 dial tcp 127.0.0.1:3306: connect: connection refused
まあ結果変わらんよな。
調べると、コンテナ上でのlovalhostはコンテナ自身を指しているのでhostを指す値を指定する必要があるらしい
な訳でDB _HOSTを「host.docker.internal」に修正
DB_USER=root DB_PASSWORD=password DB_HOST=host.docker.internal DB_PORT=3306 DB_NAME=testdb
- docker runで起動(30回目)
k_tanaka@tanakakenyanoMacBook-Air web-service-gin % docker run -p 8080:80 --env-file .env --name web-server-gin web-server-gin root:password@tcp(host.docker.internal:3306)/testdb?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0 Connected! [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> main.home (3 handlers) [GIN-debug] GET /albums --> main.getAlbums (3 handlers) [GIN-debug] GET /albums/:id --> main.getAlbumByID (3 handlers) [GIN-debug] POST /albums --> main.postAlbums (3 handlers) [GIN-debug] PATCH /albums/:id --> main.updateAlbumByID (3 handlers) [GIN-debug] DELETE /albums/:id --> main.deleteAlbumByID (3 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
無事起動したようです(汗)
- いざ,localhost:8080にアクセス!

なるほど?
なんで??
docker ps をみても
k_tanaka@tanakakenyanoMacBook-Air web-service-gin % docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1d811899b90a web-server-gin "/app/main" 30 minutes ago Up 30 minutes 0.0.0.0:8080->80/tcp web-server-gin d6b7ba7a6739 postgres:16 "docker-entrypoint.s…" 5 weeks ago Up 37 hours 0.0.0.0:5432->5432/tcp chat_db_pg 929c1c56e132 mysql:8.0 "docker-entrypoint.s…" 6 weeks ago Up 37 hours 33060/tcp, 0.0.0.0:3307->3306/tcp my_project_db k_tanaka@tanakakenyanoMacBook-Air web-service-gin %
ちゃんと起動してるが??
と思いきや、
8080 -> 80のバインディングになってるので、
localhost:8080でアクセスすると、コンテナ内の80ポートにアクセスしていますが、 Goのアプリはコンテナ内で8080で起動していますね。。。そりゃレスポンスないよな
というわけでポートを「8080:8080」で起動し直す
k_tanaka@tanakakenyanoMacBook-Air web-service-gin % docker run -p 8080:8080 --env-file .env --name web-server-gin web-server-gin root:password@tcp(host.docker.internal:3306)/testdb?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0 Connected! [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> main.home (3 handlers) [GIN-debug] GET /albums --> main.getAlbums (3 handlers) [GIN-debug] GET /albums/:id --> main.getAlbumByID (3 handlers) [GIN-debug] POST /albums --> main.postAlbums (3 handlers) [GIN-debug] PATCH /albums/:id --> main.updateAlbumByID (3 handlers) [GIN-debug] DELETE /albums/:id --> main.deleteAlbumByID (3 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
いざアクセス、、、

きました!コンテナ内で8080で起動してる時はポートバインディングも8080:8080のように設定すべきなんですね。
docker compose upで起動する
続いてdokcer compose upでDBコンテナとアプリコンテナを起動しようと思います。
まずdocker compose.yamlファイルから。
一旦以下を作ってみた。
https://www.youtube.com/watch?v=ZfUdjOAawTE
version: "3.7" services: go: build: ./docker/go/Dockerfile image: golang:1.18-alpine ## コンテナの元になるイメージを指定しま container_name: web-server-gin ##任意のコンテナ名を指定する ports: #ホストのポートとコンテナのポートの対応繋ぎこみ - "8080:8080" volumes: # コンテナがアクセスできるようにする 必要がある、 ホスト上のパスか 名前付きボリュームvolume を定義 - ./:/app links: - db environment: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - DB_HOST=${DB_HOST} - DB_PORT=${DB_PORT} - DB_NAME=${DB_NAME} db: image: mysql:latest container_name: web-server-gin-mysql ports: - "3306:3306" volumes: - my-db:/var/lib/mysql #ここわからん command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --skip-character-set-client-handshake #ここわからん volumes: #永続化の設定 /var/lib/mysqlはコンテナ内でMySQLがデータを保存するパス。 my-db: # どうなるの?→コンテナを削除しても、MySQLのデータベースの中身はmy-dbに保存され続ける。docker compose downしても消えない
以下参考
いざdocker compose up -d !!!
DBコンテナの方でエラーが吐かれた。
2025-07-19 03:29:49+00:00 [ERROR] [Entrypoint]: Database is uninitialized and password option is not specified
You need to specify one of the following as an environment variable:
- MYSQL_ROOT_PASSWORD
- MYSQL_ALLOW_EMPTY_PASSWORD
- MYSQL_RANDOM_ROOT_PASSWORD
なるほどなるほど。
ようわからんが、環境変数書けと怒られている。DB初期化のために必要ってことか
MYSQL_ROOT_PASSWORDを設定すると、設定したパスワードでのログインが可能となる。 MYSQL_ALLOW_EMPTY_PASSWORDを設定すると、パスワードなしでのログインが可能となる。 MYSQL_RANDOM_ROOT_PASSWORDを設定すると、コンテナ起動時にランダム文字列でパスワードが設定される。パスワードはログで出力される。
version: "3.7" services: go: build: ./docker/go/Dockerfile image: golang:1.18-alpine ## コンテナの元になるイメージを指定しま container_name: web-server-gin ##任意のコンテナ名を指定する ports: #ホストのポートとコンテナのポートの対応繋ぎこみ - "8080:8080" volumes: # コンテナがアクセスできるようにする 必要がある、 ホスト上のパスか 名前付きボリュームvolume を定義 - ./:/app links: - db environment: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - DB_HOST=${DB_HOST} - DB_PORT=${DB_PORT} - DB_NAME=${DB_NAME} db: image: mysql:latest container_name: web-server-gin-mysql ports: - "3306:3306" volumes: - my-db:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD ##MySQLにおけるスーパーユーザであるrootアカウントに設定するためのパスワードを指定 - MYSQL_ALLOW_EMPTY_PASSWORD #オプション変数。yesを設定することで、rootユーザに空のパスワードを設定してコンテナを起動することを許可 - MYSQL_RANDOM_ROOT_PASSWORD # オプションの変数です。yesを設定することで、rootユーザのための初期パスワードを(pwgenを利用して)ランダムで生成します。生成されたパスワードは標準出力に表示されます volumes: #永続化の設定 /var/lib/mysqlはコンテナ内でMySQLがデータを保存するパス。 my-db: # どうなるの?→コンテナを削除しても、MySQLのデータベースの中身はmy-dbに保存され続ける。docker compose downしても消えない
上記のように修正しました。dbセクションにenvironment項目を追加です。
いざdocker compose up -d !!
2025-07-19T04:20:03.426726Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. mysqld: Table 'mysql.plugin' doesn't exist
今度は~どれどれ。 なんやこれ。
一旦ボリューム消して再度docker compose up してみる
docker-compose down -v
再度起動
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
c856a72ee617 golang:1.18-alpine "/bin/sh" 6 minut
es ago Up 6 minutes 0.0.0.0:8080->8080/tcp web-serv
er-gin
e12582144888 mysql:latest "docker-entrypoint.s…" 6 minut
es ago Up 6 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp web-serv
er-gin-mysql
起動しましたね👏
とりあえずlocalhost:8080にアクセスしてみましょう

またお前か。。。
なんで?????
一旦gin のコンテナに入ってみます。。
k_tanaka@tanakakenyanoMacBook-Air web-service-gin % docker exec -it c856a72ee617 sh /go # ls bin src /go # cd .. / # ls app bin dev etc go home lib media mnt opt proc root run sbin srv sys tmp usr var
んん??goフォルダにはいってる???
ルート/app配下にmain.goを配置したので、そもそもアプリ起動できてないようですね。
なのでとりあえずgo/srcはいかに全部置くようにしてみます
とりあえずdocker-compose.ymlにworking_dirを追加します
version: "3.7"
services:
go:
build:
dockerfile: ./docker/go/Dockerfile
context: ./docker/go
image: golang:1.18-alpine ## コンテナの元になるイメージを指定しま
container_name: web-server-gin ##任意のコンテナ名を指定する
ports: #ホストのポートとコンテナのポートの対応繋ぎこみ
- "8080:8080"
volumes: # コンテナがアクセスできるようにする 必要がある、 ホスト上のパスか 名前付きボリュームvolume を定義
- ./:/go/src
links:
- db
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
working_dir: /go/src ←追加!!
stdin_open: true
・
・
・
とはやったものの、コンテナが正常に起動しますが、肝心のmainが実行されません。
はあ。。
なんで。。
調べると
Docker fileで既にimageとvolume指定してるのに
docker-compose.yaml 内で再度volumeやimage定義するのは上書きされてしまうためNGとのことだったので 以下の通りimageとvolumesをコメントアウトしました
services: go: build: dockerfile: ./docker/go/Dockerfile context: . # image: golang:1.18-alpine container_name: web-server-gin ports: - "8080:8080" # volumes: # - ./:/go/src links: - db environment: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - DB_HOST=${DB_HOST} - DB_PORT=${DB_PORT} - DB_NAME=${DB_NAME} working_dir: /go/src stdin_open: true ・ ・ ・ ・
結果、docker compose up -dで2コンテナとも起動でき
localhost:8080アクセス時に「helloworld」の文字が表示されました!
DBコンテナを起動
というわけで次はDBコンテナを起動しようと思います。
no matching manifest for linux/arm64/v8 in the manifest list entriesエラー コンテナ起動時に出たエラー。
docker compose up db --build
当方M3Macで開発しているため、過去にもよく見たエラーだ。
k_tanaka@tanakakenyanoMacBook-Air web-service-gin % docker compose up db [+] Running 0/1 ⠼ db Pulling 2.4s no matching manifest for linux/arm64/v8 in the manifest list entries
解決方法はdocker-compose.ymlに以下を追記すればOK
db:
image: mysql:5.7
platform: linux/x86_64 ←これを追記
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-init.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- initdb.d の初期化でデータが作られない。
docker-compose.ymlでdbのサービスを以下のように記載し
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d ←これ!!!
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD}
- MYSQL_RANDOM_ROOT_PASSWORD=${MYSQL_RANDOM_ROOT_PASSWORD}
- ./sql/init:/docker-entrypoint-initdb.dをかき
create database testdb; user testdb DROP TABLE IF EXISTS album; CREATE TABLE album ( id INT AUTO_INCREMENT NOT NULL, title VARCHAR(128) NOT NULL, artist VARCHAR(255) NOT NULL, price DECIMAL(5,2) NOT NULL, PRIMARY KEY (id) ); INSERT INTO album (title, artist, price) VALUES ('Blue Train', 'John Coltrane', 56.99), ('Giant Steps', 'John Coltrane', 63.99), ('Jeru', 'Gerry Mulligan', 17.99), ('Sarah Vaughan', 'Sarah Vaughan', 34.98);
て書いているのに、docker compose up してDBコンテナの中身に行っても、
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | +--------------------+ 1 row in set (0.02 sec)
なんでや(泣)(泣)(泣)(泣)(泣)(泣)
と色々調べていると
MYSQL_DATABASEという環境変数を設定すればDB初期化時にCREATE DATABASEを勝手にしてくれるとのこと。。
db:
image: mysql:5.7
platform: linux/x86_64
container_name: web-server-gin-mysql
ports:
- "3306:3306"
volumes:
- my-db:/var/lib/mysql/
- ./sql/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE} ←追加!!!
これでdocker compose upすると無事DB作られました!!
【DB】一意性制約(UNIQUE KEY)と主キー制約 (PRIMARY KEY)は違う
まず一意制約から
以下のようなテーブルを作成。
create table staff(id int , name varchar(10), age int, position varchar(10) unique);

ここで以下のSQLを実行してみる
insert into staff values(4,'鈴木', 21,'cc'); // 20:32:01 insert into staff values(4,'鈴木', 21,'cc') Error Code: 1062. Duplicate entry 'cc' for key 'staff.position' 0.0018 sec
「duplicate entry」エラーが出ました。
では複数のカラムにuniqueキーを設定するとどうなるのでしょうか
create table staff2(id int , name varchar(10) unique, age int, position varchar(10) unique);
以下のようなテーブルを作りました、

ここで以下のようなSQLを実行します
insert into staff2 values(3,'山田', 30,'bbb'); // 20:38:06 insert into staff2 values(3,'山田', 30,'bbb') Error Code: 1062. Duplicate entry '山田' for key 'staff2.name' 0.0019 sec
エラーが出ました。
insert into staff2 values(3,'安田', 30,'bbb'); // 20:38:47 insert into staff2 values(3,'安田', 30,'bbb') Error Code: 1062. Duplicate entry 'bbb' for key 'staff2.position' 0.0021 sec
これもエラー。どうやらunique key製薬をつけたカラムは独立して重複制限かかるみたいですね。
insert into staff2 values(3,'安田', 30,'ccc'); // ok
これは無事実行できました
あれ、nameとpositionカラムの複合UNIQUEKEYにしたつもりなのだが、、それぞれ単独で一意制約がかかっていますね
CREATE TABLE staff2 ( id INT, name VARCHAR(10) UNIQUE, age INT, position VARCHAR(10) UNIQUE );
実はこの定義だと:
name は 単独で UNIQUE
position も 単独で UNIQUE
つまり、
同じ name を持つ行は1つだけ
で
同じ position を持つ行も1つだけ
なのでこれらは別々に独立して制約されている状態のようです。
じゃあ複合UNIQUE KEYはどうやって作るの?
CREATE TABLE staff2 ( id INT, name VARCHAR(10), age INT, position VARCHAR(10), UNIQUE (name, position) ←複合UNIQUE );

上記のようにUNIQUE句に複数カラム指定すれば複合UNIQUEになるようです。
試しにINSERTしてみます
insert into staff3 values(1,'田中', 30,'aaa');
20:55:58 insert into staff3 values(3,'田中', 33,'aaa') Error Code: 1062. Duplicate entry '田中-aaa' for key 'staff3.name' 0.0013 sec
エラー発生です。しっかりエラー分にも「田中」「aaa」の2カラム分の重複が書かれていますね!
そして今回は複合UNIQUEなので、2カラムの組み合わせの単位での重複を見ています。
なので以下のようなINSERT分は実行可能です。
insert into staff3 values(3,'松丸', 33,'aaa'); //ok insert into staff3 values(3,'田中', 33,'zzz'); //ok
ちなみに、UNIQUE制約ではNULL値は複数INSERTしても、重複としてはみなされません。いくつでもいNSERTできます。
と言うわけでUNIQUE KEY制約とは?と聞かれれば以下のように定義できるかと思います、
UNIQUE制約は、MySQLで特定のカラムやカラムの組み合わせにおいて、重複する値を禁止するための制約。 この制約を適用することで、同じ値が二度と挿入されないようにすることができる。
UNIQUE制約が利用される場面
以下のような場合にUNIQUE制約がよく使われます:
メールアドレスやユーザー名の登録
- 各ユーザーが固有のメールアドレスを持つ必要がある場合。
商品番号や注文番号の管理
- 商品番号や注文番号が重複してはならない場合。
複合条件の制約
- 2つ以上のカラムを組み合わせて一意性を保証したい場合。
主キー制約(PRIMARY KEY)との違いは?
MySQLには、PRIMARY KEYという制約も存在しますが、UNIQUE制約とはいくつかの違いがあります。
PRIMARY KEYは必ずNOT NULL
- PRIMARY KEYは一意性を保証するだけでなく、値がNULLになることも禁止します。一方、UNIQUE制約では、NULL値が許容されます。
PRIMARY KEYは1つのみ
- テーブルごとにPRIMARY KEYは1つしか設定できませんが、UNIQUE制約は同じテーブル内に複数設定することが可能です。
このような違いがあります。
【DB】UPSERT
今回は実務で見かけたUPSERTについて紹介します。
最近実務の方でAPIの詳細設計をしており、そこで登録処理と更新処理を兼ねたAPのI設計を担当しました。
そこで出てきたのがタイトルの「UPSERT」。
こちら学びとして書き記しておこうと思います。
UPSERTとは?
- INSERT文を実行する際、すでに対象のレコードが存在すれば UPDATEを実行し、なければそのままINSERT文を実行する
というものになります。
実際に試してみる(※mysql@8.0で実験)
以下のようなemployeesテーブルを作りました。

これでUPSERTをやってみましょう。
SQLは以下の通り。
insert INTO employees (id, name, department_id,hire_date, salary) values(14,'田中太郎',3,'2025-04-01',300000) ON DUPLICATE KEY UPDATE id=id+1;
通常のインサート文の後ろに何やらついていますね。
「ON DUPLICATE KEY UPDATE id = id +1」です。

idが14のレコードが登録されました。
INSERTされたようです、
ここでもう一度同じSQLを実行しましょう。

idが15のレコードに変更されていますね。
そういえばBULKUPSERTってのもあったな
試してみます。
調べた感じ、 BULKUPSERTは一つのレコードに対してだけでなく、複数のレコードに対して一括で実行することも可能のようです。これにより、データベース操作の効率を大幅に向上させることができま
以下のようなテーブルがあります。

ここに以下のBULKUpsert文を実行してみます。
insert INTO employees (id, name, department_id,hire_date, salary) values (16,'田中太郎',3,'2025-04-01',333), (17,'田中太郎',3,'2025-04-01',444) as new ON DUPLICATE KEY UPDATE salary = new.salary;

はい、上記のようにidの16,17がすでに存在しているため、INSERTできず、id ga16,17のレコードをsalaryのみ更新しています。
UPSERTの複数同時バージョンって感じですかね。
- 利用例としては以下のような場合に使ったりします。
① ユーザーデータの更新/登録
INSERT INTO users (id, name, last_login) VALUES (1, '田中', NOW()) ON DUPLICATE KEY UPDATE last_login = NOW();
→ id=1 のユーザーがいなければ新規追加、 いれば last_login を更新。
② 集計カウントなどの「存在すれば加算」
INSERT INTO access_counts (url, count) VALUES ('/home', 1) ON DUPLICATE KEY UPDATE count = count + 1;
→ ページが初アクセスなら追加、既にあればカウント加算。
③ キャッシュテーブルの再書き込み
INSERT INTO cache (key, value) VALUES ('weather_tokyo', '晴れ') ON DUPLICATE KEY UPDATE value = '晴れ';
→ キーがあれば上書き、なければ挿入。
あれば更新、なければ登録のような動きの必要な機能になりますね。
- もしUPSERT使わないと??
✅ UPSERTを使わない場合のコード例
Optional<User> existingUser = userRepository.findById(id); if (existingUser.isPresent()) { User user = existingUser.get(); user.setLastLogin(LocalDateTime.now()); userRepository.save(user); // UPDATE } else { User user = new User(); user.setId(id); user.setName("田中"); user.setLastLogin(LocalDateTime.now()); userRepository.save(user); // INSERT }
このように:
1回目: findById で存在確認
2回目: save() で登録 or 更新
のようにクエリを2回発行しないといけなくなります。
これの 問題点・デメリットとしては、
と言った感じになります。
余談ですが、API詳細設計書に、DBからの取得値がテーブルごとに表で書かれており、
その表のUPSERTで更新するカラムの備考欄には「ON DUPLIACTE KEY UPDATE」と書かれていました。
いやUPSERTって上の方で書いてるしなんでわざわざ構文書くんやろと思ってましたが、
POSTGRESではUPSERTは「ON CONFLICT」って書くようですね。
なのでON DUPLIACTE KEY UPDATEとかけばmysqlだよ〜と伝えたい意図なのでしょうか。
分かりませんが、知識が増えるたびに設計書から読み取れる量が増えるのは楽しいですね。
【Go】チャネルのcloseについて整理する
以下のソースを実行すると
ch := make(chan struct{}) close(ch) ch <-struct{}{}
エラーが起きます⇩
基本的に閉じられているチャネルには値は送信できないよう。
panic: send on closed channel
では閉じられたチャネルから受信してみる
ch := make(chan int) close(ch) // ch <-struct{}{} fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)
以下のようにチャネルの値の型のゼロ値が帰ってくるようだ。
値が送信されていなくても、何回でも受信でき、送信されてない場合はゼロ値が入るよう。
0 0 0 0 0 0 0 0 0 0 0 0
じゃあクローズ前に値を送信してみる
ch := make(chan int)
ch<-1
close(ch)
// ch <-struct{}{}
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
これはデッドロックになる
fatal error: all goroutines are asleep - deadlock!
原因は容量無指定(unbuffered)のチャネルの場合は、送信する前に受信の処理を書いておく必要があるから
ch := make(chan int) // ← unbuffered ch <- 1 // ここでブロック(受信者がいない!) close(ch)
なので送信前に受信用のGoルーチンを書いておけなOK
ch := make(chan int)
go func (){
fmt.Println(<-ch)
}()
ch<-1
ch<-2
close(ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
1 0 0 0 0 0 0 0 0 0 0 0 0
もう一つ送信増やしてみる
ch := make(chan int) go func (){ fmt.Println(<-ch) }() ch<-1 ch<-2 close(ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)
1 fatal error: all goroutines are asleep - deadlock!
またデッドロック。
それもそのはず。
受信処理が一つしかないから当たり前っすね。
ちなみに、
go func (){ fmt.Println(<-ch) }()
ここの部分、もし ch に送られた値がまだない場合、
この goroutine はここで 待ち続け(ブロック状態)
誰かが ch <- value と 送信したタイミングで 受信が成立し、処理が再開される仕組み。
なので受信処理をもう一つ増やすと動きます
ch := make(chan int) go func (){ fmt.Println(<-ch) }() go func (){ fmt.Println(<-ch) }() ch<-1 ch<-2 close(ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)
1 0 0 0 0 0 0 0 2 0 0 0 0 0
【Go】Goのチャネルとselect
selectが必要になる場面
package main import "fmt" func main(){ ch1 := make(chan int ,2) ch2 := make(chan string ,2) ch2 <- "A" fmt.Println(1) v1:= <-ch1 fmt.Println(111) v2:= <-ch2 fmt.Println(v1) fmt.Println(v2)
上記のソースだと、どのような出力になるだろうか。 以下その出力。
k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go 1 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /Users/k_tanaka/Repository/Golang-Practice/practice/8/main.go:12 +0x98 exit status 2
fmt.Println(1)まで出力され、
v1:= <-ch1部分でデッドロックが発生、処理が止まってしまうのです。
原因はそもそもch1に値を何も送信しておらず、何も持っていないチャネルからひたすら受信しようとしていて、デッドロックになってしまうから。
この場合、v1:= <-ch1で処理が止まってしまい、 v2:= <-ch2の処理が実行されずに終わっている。
これはまずいと言うところで登場、selectくんです
selectは
複数のチャネル操作の中から、準備ができたものを1つ選んで実行する構文です。 非同期処理や複数のチャネルからの読み取りを効率的に制御したいときに使います。
具体的にみていきましょう。
func main(){ ch1 := make(chan int ,2) ch2 := make(chan string ,2) ch2 <- "A" ch1 <-1 ch2 <-"B" ch1 <-2 select{ case v1 := <- ch1: //ch1 に値が入ったらその処理を実行する fmt.Println(v1 + 1000) case v2 := <- ch2://ch2 に値が入ったらその処理を実行する fmt.Println(v2 + "!?") } }
出力結果はバラバラ。
それもそのはず。
Goのselectでは、最初に成立したケースが優先されるのではなく、どのケースが実行されるかはランダム
と言う性質があるからだ。
k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go 1001 k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go 1001 k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go A!? k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go 1001 k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go A!? k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go A!?
ちなみselect構文にもtry-catch のようなdefaultがある。
これはどちらのケースにも当てはまらない場合に呼ばれる処理。
package main import "fmt" func main(){ ch1 := make(chan int ,2) ch2 := make(chan string ,2) // ch2 <- "A" // ch1 <-1 // ch2 <-"B" // ch1 <-2 select{ case v1 := <- ch1: //ch1 に値が入ったらその処理を実行する fmt.Println(v1 + 1000) case v2 := <- ch2://ch2 に値が入ったらその処理を実行する fmt.Println(v2 + "!?") default: fmt.Println("どちらでもない") } }
k_tanaka@tanakakenyanoMacBook-Air 8 % go run main.go どちらでもない
chan 型を使ってGoroutine間でデータをやりとりしてみる
package main import "fmt" func main(){ ch3 := make(chan int) ch4 := make(chan int) ch5 := make(chan int) go func (){ for{ i := <-ch3 ch4 <- i * 2 } }() go func (){ for { i2 := <-ch4 ch5 <- i2 -1 } }() n:=0 for{ select{ case ch3 <- n: n++ case i3 := <- ch5 : fmt.Println("recieved", i3) } if n > 5 { break } } } // 例えば n = 0 のときの処理の流れ // main の select で ch3 <- 0 が成功(空いてるので送信できる) // goroutine① が ch3 から 0 を受信し、0 * 2 = 0 を ch4 へ送信 // goroutine② が ch4 から 0 を受信し、0 - 1 = -1 を ch5 へ送信 // 次に select の case i3 := <-ch5 が実行され、main が -1 を受け取る // received -1 が出力される // n = 1 のときも同じ: // main で ch3 <- 1 が送信される // goroutine①: 1 を受信して 2 を ch4 に送る // goroutine②: 2 を受信して 1 を ch5 に送る // main で 1 を受信して出力 received 1
ch3,ch4,ch5のチャネルを初期化する
2つのレシーバ処理を並列処理としてGoルーチンで生成
変数nを初期化
for文がスタート
select分に入るがこの時、実行可能なcaseが1つだけであれば、それが実行される。
select { case ch <- val: // 実行可能! case x := <-ch2: // ブロック中 } → ch への送信が実行される
また、実行可能なcaseが複数ある場合はその中からランダムに1つ選ばれる
select { case ch1 <- val: // 実行可能 case ch2 <- val: // 実行可能 } → ch1 または ch2 のどちらかが「ランダムに」選ばれる
そして、すべてのケースが実行不可の場合はselect自体をブロック状態にする
select { case x := <-ch: // ch が空で送信もない → ブロック } → 何もできずに止まる(デッドロックの原因にもなる)
話を戻して元のソースで言うと、
select{ case ch3 <- n: n++ case i3 := <- ch5 : fmt.Println("recieved", i3) }
こちらのcase ch3 <- nが実行できるかどうかは、 「ch3が受信準備できているかどうか」つまりごルーチンで受信処理を先にかけていれば実行可能。
i3 := <-ch5 は、ch5 に値が送信されてきていれば実行可能
といった形になる