「詳解Go言語Webアプリケーション開発」を読んだ #golang

仕事では中々 Go を書く機会がないので、手を動かしながら学べる本がないかなと手に取った。

初心者向けの入門書かな、くらいの気持ちだったのだけれど、いい意味で期待を裏切られた。

かなり実践的な内容に踏み込んでいるし、解説も丁寧で参考になることがたくさんあった。

今後自分で Go の Web 開発をするときはこの本をベースにして考える気がする。

気になった点

ハンズオンを写経しながら進めていたが、書籍だけだとどのファイルに書くのかちょっと分かりづらいことがある。

また、ところどころ間違っていると思われるところもある。

ただ GitHub リポジトリでコードと正誤表が公開されているので、詰まったりよく分からないときは参照することで問題なく解決できた。

あとテストもあるのが非常に助かるのだけれど、網羅的に用意されている訳ではないので、省略しているだけなのか、意図的に書いてないのかは気になった。

おまけ

とてもいい本だった。

(実務含め) Go で書く機会を積極的に増やしたいなという気持ちになった。

副業とかないかな。

GitHub Actions で service のコンテナにリポジトリのファイルをマウントしようとするとエラーになる #GitHub #Actions

概要

GitHub Actions では、ジョブのサービスとして MySQL や Redis が使える。

volumes ( jobs.<job_id>.services.<service_id>.volumes ) を指定することで、サービス間やステップ間でデータを共有できる。

ただソースコードは通常 actions/checkout を利用すると思うが、リポジトリで管理しているようなファイルパスをそのままマウントしようとエラーになる。

rm: cannot remove '/home/runner/work/your_repo/path/to/mysql/conf.d': Permission denied

たとえば下記のような Workflow である。

jobs:
  job-name:
    runs-on: ubuntu-latest
    services:  
      mysql:  
        image: mysql
        ports:  
          - 3306:3306  
        volumes:  
          - ${{github.workspace}}/path/to/mysql/conf.d:/etc/mysql/conf.d

steps:  
  - uses: actions/checkout@v3  

原因

Workflow のジョブはサービスが立ち上がった後に動くため、 actions/checkout はマウント後に動く。

そうするとチェックアウトするディレクトリにマウントするディレクトリが存在する(空ではない)とされて、 actions/checkout は一度対象ディレクトリの中を空にしようとするようだ。

その際、サービスを動かすユーザーと actions/checkout を動かすユーザー(権限)異なるため、エラーになってしまう。

解決策

リポジトリをチェックアウトするディレクトリと、マウントする場所を分ければ解決する。

  • jobs.<job_id>.defaults.run の working-directory を設定する
  • チェックアウト場所を working-directory にする
  • 対象ファイルを cp などで移動する
jobs:
  job-name:
    runs-on: ubuntu-latest
  defaults:
    run:  
      working-directory: my_target
    services:  
      mysql:  
        image: mysql
        ports:  
          - 3306:3306
        volumes:  
          - ${{github.workspace}}/path/to/mysql/conf.d:/etc/mysql/conf.d

steps:  
  - uses: actions/checkout@v3
    with:  
      path: my_target
  - run: |  
      cp -r path/to/mysql/conf.d ${{github.workspace}}/path/to/mysql/conf.d

補足

working-directory の default を指定しているのは、 step の追加・変更時にパスをできるだけ意識せずに済むようにするため。

別に workflow 単位でも指定できるし、 step の中で cd してもよい。

例では MySQL の設定ファイルをマウントしているが、反映するためには大抵再起動が必要になるので、実際には data ファイルとかになると思う(テストデータを step 間で使い回すとか?)。

その他参考

docs.github.com

GitHub Actions 上で MySQL のシステム変数をカスタマイズする #GitHub #Actions #MySQL

概要

GitHub Actions の Workflow では、 Services として MySQL が利用できる。

character_set_server などのシステム変数を変更する方法についてまとめる。

採用した方法: SQL ファイルでグローバル変数を書き換える

Workflow で使う MySQL は基本使い捨てになるため、グローバル変数を書き換える方式にした(ボツ案は後述)。

グローバル変数の書き換えは再起動時に失われるが、そもそも使い捨てであることを考えると、この方式が一番シンプルになると思われたため。

下記はチェックアウトして、直下の SQL を流すだけのサンプル。

jobs:  
  php:  
    runs-on: ubuntu-latest
    services:  
      mysql:  
        image: mysql
        ports:  
          - 3306:3306  
        env:  
          MYSQL_ALLOW_EMPTY_PASSWORD: yes

steps:  
  - uses: actions/checkout@v3  

  - run: |
      mysql -u root --protocol tcp < setup.sql

SQL は下記のような感じになる。

SET @@GLOBAL.character_set_server = 'utf8mb4';  
SET @@GLOBAL.collation_server = 'utf8mb4_general_ci';

ちなみにプロトコルを指定しているのは、 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) とか言われて怒られるため。

詳細は公式ドキュメント参照。

MySQL :: MySQL 8.0 Reference Manual :: 4.2.4 Connecting to the MySQL Server Using Command Options

この方式で最低限 SQL ファイルは構成管理されるが、あくまでユニットテストなどを流すために利用する使い方を想定しており、本番用の設定は別で管理するのを前提にしている。

採用しなかった方法①: 設定ファイルで書き換え

my.cnf のような設定ファイルを別途用意する方法も当初は考えたが、下記の理由で採用を見送った。

  • Workflow のジョブは MySQL サービスが立ち上がった後に動く
  • my.cnf の反映は大抵再起動が必要

再起動して、かつサービスがレディになるのを改めて待つのは複雑になったり Workflow の実行時間が伸びてしまうのに抵抗があった。

nao-y.hatenablog.com

採用しなかった方法②: options

MySQL の公式イメージを純粋に使用する場合、 CMD を上書きすることで設定ファイルなしのパラメータ変更方法( Configuration without a cnf file )が紹介されている。

https://hub.docker.com/_/mysql

ただし、GitHub Actions の services では CMD の変更方法は提供されていないようだ。

options (jobs.<job_id>.services.<service_id>.options) で docker create の option は設定できるため、ENTRYPOINT の上書きでどうにかならないか試行錯誤してみたが、どうも行き詰まったので諦めた。

docs.github.com

その他参考

DataGrip で sql_mode に STRICT_TRANS_TABLES が適用される #mysql

概要

DataGrip で MySQL に接続すると、 sql_mode を別途設定しているにも関わらず、STRICT_TRANS_TABLES が勝手に適用されていた。

グローバルの設定を確認すると、意図したものが設定されているが、セッションの設定は STRICT_TRANS_TABLES になってしまう。

DataGrip に限らず、 IntelliJ IDEA とか JetBrains 系 IDE の DB ツールで同じことが起きると思う。

-- グローバルは問題ないけど
SELECT @@GLOBAL.sql_mode;
-- SESSION だと何故か STRICT_TRANS_TABLES
SELECT @@SESSION.sql_mode;

原因

DataGrip が MySQL の接続に JDBC ドライバを使用していたため。

JDBC は STRICT_TRANS_TABLES を有効にするようになっている。

以下はチケットに対して、バグじゃないよ、という話がされているのが下記リンク。

bugs.mysql.com

STRICT_TRANS_TABLES とは何か

sql_mod に設定することで、 SQL を Strict (厳密) にチェックしてくれるようになる。

逆にオフにしていると、たとえば文字列長超えを切り捨ててデータ登録できたり、デフォルト値を持つカラムに Null でクエリを発行した場合にデフォルト値で更新してくれたりする (警告は出る)。

MySQL 5.7.5 からはデフォルトで有効になっている。

dev.mysql.com

JDBC は何故勝手に有効にするのか

jdbcCompliantTruncation を有効にするためには、 STRICT_TRANS_TABLES を有効にしておく必要があるため、jdbcCompliantTruncation を有効にすると自動的に sql_mode に設定されるようだ。

jdbcCompliantTruncation は JDBC の設定値で、前述のような値切り捨てが発生した場合に例外を投げるかどうかを設定できる。

リファレンスにも STRICT_TRANS_TABLES を有効にしないと効果がないことが書かれている。

dev.mysql.com

DataGrip 等での設定のカスタマイズ方法

データソースの設定の「Advanced」でカスタマイズできる。

jdbcCompliantTruncation は true / false で指定すればよい。

sql_mode を変更したいときは sessionVariables に sql_mode='aaa,bbb' のような形式で設定すれば OK。(まるっと無効にしたいときは sql_mode=0

stackoverflow.com

終わりに

  • JDBC を使っている DB クライアントやアプリケーションでは同じことが起きそう
  • Strict でも問題なく動くように常日頃から実装していきたい

参考

Goland で Docker を使った開発環境を作成する #golang #goland

概要

ネットに十分な情報はあるのだが、なんやかんや毎回詰まったりちょこちょこ調べたりしているので自分用にまとめておく。

  • Goland を使う
  • Docker で動かす Web アプリケーション
  • air でホットリロード
  • delve でリモートデバッグ
  • テストも Docker 経由でデバッグ

今回のサンプルは GitHub に置いてある。

github.com

雛形の作成

mkdir go-dev-starter
cd go-dev-starter
# 初期化
go mod init

Hello World を表示するだけの Web アプリケーションをサンプルとして作成する。

ポートだけ環境変数で変えられるようにしてある。

package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
    err := http.ListenAndServe(
        fmt.Sprintf(":%s", getEnv("PORT", "18080")),
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintln(w, "Hello, world!")
        }))

    if err != nil {
        fmt.Printf("failed to terminate server: %v", err)
        os.Exit(1)
    }
}

// getEnv は指定した key の環境変数を取得します
// 環境変数が未設定の場合 fallback の値を返します
func getEnv(key, fallback string) string {
    if value, ok := os.LookupEnv(key); ok {
        return value
    }
    return fallback
}

Dockerfile の作成

delve と air を入れた Dockerfile を作成する。

今回は分かりやすくするために、開発用のみ記載している。

FROM golang:1.19  
  
WORKDIR /app  
RUN go install github.com/cosmtrek/air@latest  
RUN go install github.com/go-delve/delve/cmd/dlv@latest  
  
CMD ["air"]

docker-compose.yml の作成

docker-compose.yml を用意する。

security_opt と cap_add は delve のリモートデバッグ用に設定してある。

40000 番のポートはリモートデバッグで Listen する用。

version: "3.9"  
services:  
  app:  
    container_name: dev-server  
    build:  
      context: .  
    environment:  
      PORT: 8080  
    volumes:  
      - .:/app  
    ports:  
      - "18000:8080"  
      - "40000:40000"  
    security_opt:  
      - "apparmor=unconfined"  
    cap_add:  
      - SYS_PTRACE

air の設定ファイルの作成

コンテナ内で air init すると設定ファイルの雛形が作成される。

docker compose up -d
docker compose exec app air init

ビルドコマンドと full_bin を delve を使うように書き換える。

--- a/.air.toml
+++ b/.air.toml
@@ -5,14 +5,14 @@ tmp_dir = "tmp"
 [build]
   args_bin = []
   bin = "./tmp/main"
-  cmd = "go build -o ./tmp/main ."
+  cmd = "go build -gcflags=\"all=-N -l\" -v -o ./tmp/main ."
   delay = 1000
   exclude_dir = ["assets", "tmp", "vendor", "testdata"]
   exclude_file = []
   exclude_regex = ["_test.go"]
   exclude_unchanged = false
   follow_symlink = false
-  full_bin = ""
+  full_bin = "dlv --listen=:40000 --headless=true --api-version=2 --accept-multiclient exec ./tmp/main"
   include_dir = []
   include_ext = ["go", "tpl", "tmpl", "html"]
   kill_delay = "0s"

Docker の実行構成を作成

Goland 側から実行できるようにしておく。

リモートデバッグの実行構成を作成

ここでリモートデバッグ用のポートを設定しておく。

今回の場合だと 40000 番。

デバッグ実行を試す

作成した Docker のリモートデバッグの構成を実行し、ブレイクポイントを貼る。

エンドポイントにアクセスするとブレイクポイントで止まってくれる。

curl localhost:18000

ホットリロードを試す

適当に書き換えると、air が自動でリビルドしてアプリケーションを起動し直してくれる。

ただリモートデバッグが一度切れてしまうので再実行する必要がある。

少し面倒だが、 Ctrl + D のショートカットを使えるので許容範囲かな。。(うまい方法あったら教えてほしい)

テストの追加

とりあえず試すだけなので、環境変数設定のテストを書く。

package main  
  
import (  
   "os"  
   "testing")  
  
func Test_getEnv(t *testing.T) {  
   want := "9999"  
   key := "FOO"  
   err := os.Setenv(key, want)  
   if err != nil {  
      t.Fatalf("failed to set env: %v", err)  
   }  
  
   if got := getEnv(key, "8080"); got != want {  
      t.Errorf("getEnv() = %v, want %v", got, want)  
   }  
}

Run Target に Docker を追加

docker compose 経由で実行できるターゲットを追加しておく。

デフォルトターゲットも変更しておく。

テストのデバッグ

テストのデバッグ実行も Docker 経由で実行できることを確認する。

IDE からテストを実行してみる。

テスト実行後のログを見ると、Docker 経由で実行されていることが分かる(go や delve のパスで分かる)。

ブレイクポイントを認識してくれないことがあるが、一度止めずに実行すると認識してくれる。

参考

「プロフェッショナルWebプログラミング Laravel」を読んだ #Laravel

現職だと Web アプリのフレームワークCakePHP を利用してるんだけど、Laravel もう少し知っておきたいな〜、と思って読んでみた。

感想

本は実際に手を動かしながら基本的な機能を触っていく感じで、 Laravel の雰囲気を掴むのにいい感じ。

カバー範囲は広く、 テストや CI 、 Heroku を使ったデプロイまで言及されている。

Laravel はフルスタックフレームワークという感じで、一般的な Web アプリで必要になりそうな機能がかなり簡単に使えるようになってる。

メール送信や Queue を使った非同期処理だったり、スケジューラーだったり。

Docker での開発も sail ですぐ始められる。

テストもユニットテストやフィーチャーテスト、Laravel Dusk でのブラウザテストだったり。

その一方で使い手にとって簡単がゆえに、裏の仕組みは意図的に知ろうとする必要があるのと、そこに踏み込んだときの学習コストはそれなりにかかるかもしれない。

深堀りするというよりは、サクッと全体像を把握したり、取っ掛かりを作っておくのに良い本だなと思いました。

ハマったところ

前提として、手を動かす時は本のバージョン無視してとりあえず最新入れるようにしているので、場合によっては動かなかったりする。

これは自分が悪いんだけど、その上でつまずいたところを備忘録としてメモ。

Laravel Dusk でブラウザテスト

これは調べていた感じ、バージョンと言うよりM1 のせいかな?という雰囲気だったけど、ほどほどで諦めたので定かではない。

フロントエンドの開発

書籍では Laravel Mix を使う前提になっているが、 Laravel Breeze は最新だと Vite になっている。

いくつかエラーを踏んだり動かなくなったりしたけど、原因さえ分かっていると、エラー内容を愚直に対応していけばそれほど苦労はしなかったと思う。

書籍に合わせたかったので、 Mix を使うように修正していくことで解決した。

CakePHP の BelongsToMany でdependent は default が true になっている #CakePHP

アソシエーションの dependant

CakePHP のモデルのアソシエーションには dependant というキーが用意されており、 true に設定することで削除のときに関連付けたモデルのレコードもまとめて削除することができる。

book.cakephp.org

belongsToMany だけデフォルト true

ところが hasOne など他のアソシエーションはデフォルトが false になっているが、 belongsToMany はデフォルトが true になっている。

なので、明示的に設定しなくても関連したモデルのレコードが消されてしまう。

意図しない削除を避けたり他のアソシエーションと一貫性をもたせるたりするためにも、デフォルトは false の方がいいと思って違和感がある。

互換性が理由だった

そこで Cake の実装を見てみると、互換性のために true にしているとコメントがしてあった。

cakephp/BelongsToMany.php at 4.4.6 · cakephp/cakephp · GitHub

過去の PR や Issue を追っかけてみたところ、どうやら関連レコードをどうやっても消してしまうという問題があったようだ。

消さないようにする修正を入れたものの、消すほうが現行の挙動だから、互換性のために default を true にしているというのが経緯らしい。

修正自体はかなり前なので、 CakePHP を長く触っている人なら常識なのかもしれない。

github.com