0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【go/goa】Goaの example コマンドを使いやすくするカスタマイズ手法

Last updated at Posted at 2025-11-05

Goaの example コマンドを使いやすくするカスタマイズ手法

Goaフレームワークには example コマンドがあり、API設計(DSL)から実行可能なGoコードの例を自動生成できます。
これを使うと、cmd ディレクトリ配下に main.gohttp.go などのエントリポイントが生成され、各サービスの実装を記述するためのサンプルコードも出力されます。

しかし、実際の開発で使おうとすると不便な点があります。
example コマンドをより実用的にする方法を紹介します。

example コマンドの課題

  1. 既存ファイルの上書きができない
    goa example は一度出力したファイルを上書きしません。
    そのため、サービスやメソッドを追加した際には、以下のような手作業が必要になります

    • 新しいメソッドを追加した場合:
      • 既存のサービス実装ファイルに手動で追記する必要があります。
    • 新しいサービスを追加した場合:
      • main.gohttp.go に手動でサービス登録コードを追加しなければなりません。

    これらは、プロジェクトが大きくなるほど面倒で、メンテナンスミスの温床になります。

  2. 出力ディレクトリとインポートパスの扱いが不便
    デフォルトでは example コマンドの出力がプロジェクトのルートディレクトリに展開されます。
    --output オプションを使えば出力場所を変更できますが、インポートパスに自動的に example が含まれてしまうため、そのままでは正しく動作しません。

    たとえば、adapter ディレクトリ配下にサービス実装ファイルをまとめたい場合、
    example が生成する cmd 配下の main.go のサービス実装ファイル参照先は <プロジェクト名>/examples となるため <プロジェクト名>/adapter にしたいです。
    また、サービス実装を記述するためのサンプルコードの genへの参照は <プロジェクト名>/examples/gen/... を参照してしまいます。
    理想的には <プロジェクト名>/gen/... にしたいところです。

解決策:example コマンドをカスタマイズする

Goaはカスタムプラグインを登録することで、生成処理を自由に拡張できます。
ここでは custom/generate.go に独自のプラグインを追加し、example 出力時のインポートパスを自動で書き換えるようにします。

前提

go version go1.24.9 linux/amd64
Goa version v3.2.5

ディレクトリ構造
goa-test
├── adapter // exampleで吐き出されたサービス実装ファイルを格納するディレクトリ
├── custom // goaをカスタムするコードを格納する
│   └── generate.go
├── design // goaのデザインファイル
│   └── design.go
├── go.mod
└── go.sum
custom/generate.go
package custom

import (
	"fmt"
	"strings"

	"goa.design/goa/v3/codegen"
	"goa.design/goa/v3/eval"
)

func init() {
	// Goaの標準コード生成が終わった最後に goatestExample が呼ばれるよう登録します
	codegen.RegisterPluginLast("goa-test-example", "example", nil, goatestExample)
}

func goatestExample(genpkg string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
	for _, f := range files {
		// import文の書き換え
		st := f.SectionTemplates[0]
		stData, ok := st.Data.(map[string]any)
		if !ok {
			fmt.Printf("invalid section template data: %T\n", st.Data)
			continue
		}
		importSpecs, ok := stData["Imports"].([]*codegen.ImportSpec)
		if !ok {
			fmt.Printf("invalid imports data: %T\n", stData["Imports"])
			continue
		}
		for _, is := range importSpecs {
			if is.Name == "goatest" {
				// import文の "goa-test/examples"	--> "goa-test/adapter"
				is.Path = "goa-test/adapter"
			} else {
				// import文の "goa-test/examples/gen/..." --> "goa-test/gen/..."
				is.Path = strings.ReplaceAll(is.Path, "examples/", "")
			}
		}
	}

	return files, nil
}

このプラグインを読み込むために、designファイル の先頭で custom パッケージを _ インポートします。

design.go
import (
	_ "goa-test/custom" // 生成時にカスタムプラグインを読み込む
)

Makefileの整備

毎回手で削除・コピーを行うのは手間なので、コマンドをMakefileでまとめます。

Makefile
_gen:
	goa gen goa-test/design
example:
	rm -rf examples
	rm -rf cmd
	goa example goa-test/design --output=examples
	cp -r ./examples/cmd ./
run:
	go run goa-test/cmd/goa_test --http-port=8080
build:
	go build -o bin/goa_test goa-test/cmd/goa_test
build-cli:
	go build -o bin/goa_test-cli goa-test/cmd/goa_test-cli

これにより、

  1. make _gen で goaの自動生成
  2. make example でクリーンなexample生成+cmd更新
  3. サービスを追加した場合は examples ディレクトリ から対象のファイルをコピーしてくる
  4. メソッドを追加した場合は、examples のメソッド部分だけコピぺしてくる
  5. make run で即時起動

という開発サイクルを実現できます。

最終ディレクトリ構造

ディレクトリ
goa-test
├── Makefile // コマンド群を記載
├── cmd
│   ├── goa_test
│   │   ├── http.go
│   │   └── main.go
│   └── goa_test-cli 
│       ├── http.go
│       └── main.go 
├── adapter  // サービス実装ファイルを格納する (サービスを追加するごとにexampleからコピペしてくる)
│   ├── health_check.go
│   └── hello.go
│   └── ...サービス単位で作成
├── custom // goaをカスタムする
│   └── generate.go
├── design
│   ├── api.go
│   └── design.go
├── examples // goaサンプルコード の出力先
├── gen  // goa自動生成 
├── go.mod
└── go.sum
design/design.go
package design

import (
	. "goa.design/goa/v3/dsl"
)

var _ = Service("health_check", func() {
	Description("サービスのヘルスチェックを行うサービス。")

	Method("health_check", func() {
		Result(String, "サービスの状態を示すメッセージ")
		HTTP(func() {
			GET("/health")
		})
	})
	Files("./openapi3.json", "./http/openapi3.json")
	Files("./openapi3.yaml", "./http/openapi3.yaml")
})

var _ = Service("hello", func() {
	Description("シンプルな挨拶を返すサービス。")
	Error("NotFound", String, "指定されたリソースが見つからない場合に返されるエラー。")
	HTTP(func() {
		Path("/hello")
		Response("NotFound", StatusNotFound)
	})
	Method("sayHello", func() {
		Payload(String, "挨拶する相手の名前")
		Result(String, "挨拶メッセージ")

		HTTP(func() {
			GET("/{name}")
		})
	})
})
design/api.go
package design

import (
	_ "goa-test/custom"  // goaの自動生成のカスタムを登録するためにインポート

	cors "goa.design/plugins/v3/cors/dsl"

	. "goa.design/goa/v3/dsl"
)

var _ = API("goa-test", func() {
	Title("Go Test API")
	Description("これはGoaフレームワークAPIの例です。")
	Version("1.0")
	cors.Origin("*", func() {
		cors.Headers("X-Authorization", "X-Time", "X-Api-Version",
			"Content-Type", "Origin", "Authorization")
		cors.Methods("GET", "POST", "PUT", "PATCH", "OPTIONS", "DELETE")
		cors.Expose("Content-Type", "Origin")
		cors.MaxAge(100)
		cors.Credentials()
	})
})
custom/generate.go
package custom

import (
	"fmt"
	"strings"

	"goa.design/goa/v3/codegen"
	"goa.design/goa/v3/eval"
)

func init() {
	// Goaの標準コード生成が終わった最後に goatestExample が呼ばれるよう登録します
	codegen.RegisterPluginLast("goa-test-example", "example", nil, goatestExample)
}

func goatestExample(genpkg string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
	for _, f := range files {
		// import文の書き換え
		st := f.SectionTemplates[0]
		stData, ok := st.Data.(map[string]any)
		if !ok {
			fmt.Printf("invalid section template data: %T\n", st.Data)
			continue
		}
		importSpecs, ok := stData["Imports"].([]*codegen.ImportSpec)
		if !ok {
			fmt.Printf("invalid imports data: %T\n", stData["Imports"])
			continue
		}
		for _, is := range importSpecs {
			if is.Name == "goatest" {
				// import文の "goa-test/examples"	--> "goa-test/adapter"
				is.Path = "goa-test/adapter"
			} else {
				// import文の "goa-test/examples/gen/..." --> "goa-test/gen/..."
				is.Path = strings.ReplaceAll(is.Path, "examples/", "")
			}
		}
	}

	return files, nil
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?