【Go入門】go-zeroを使って爆速でAPIをつくってみた

本記事ではGoの人気なフレームワークの1つである[go-zero](https://github.com/zeromicro/go-zero?tab=readme-ov-file)を用いて簡単なAPIを作成し、その過程でgo-zeroの特徴を解説します。最後に使用感とgo-zeroが向いている開発について考察します。（go-zeroの記事って意外と少ない...？）

# 対象読者
- go-zeroとはなんぞやの人
- go-zeroの主要機能を一通り理解したい人
- go-zeroを軽く動かしてみたい人

# go-zeroとは
[go-zero](https://github.com/zeromicro/go-zero?tab=readme-ov-file)はTiktok（ByteDance）の元エンジニアによって開発された高性能・高生産性のGoフレームワークです。

従来のモノリシックなシステムをマイクロサービスシステムへ移行させるために開発され、2018年8月にデプロイされました。

以下のような設計原則に基づいて設計されいます。（[公式ドキュメント](https://github.com/zeromicro/zero-examples?tab=readme-ov-file)引用）
> ・keep it simple（シンプルに保つ）
・high availability（高い可用性）
・stable on high concurrency（高い並行性での安定性）
・easy to extend（高い拡張性）
・resilience design, failure-oriented programming（レジリエンスデザイン、障害駆動プログラミング）
・try best to be friendly to the business logic development, encapsulate the complexity（複雑さをカプセル化し、ビジネスロジックの開発をよりしやすく）
・one thing, one way（一つのことを一つのほう）

これを見る感じはGoの設計思想に近く、かなりよさげなフレームワークです。

# go-zeroによる開発の進め方
go-zeroの強みは **「APIと自動生成を用いた高速な開発が可能」** なところです。
以下のような手順で進めることが一般的です。
1. API定義ファイル(`.api`)を書く
1. コード自動生成
1. DBモデル（MySQLなど）の構造体自動生成
1. ロジック実装

API定義ファイルとはAPIの仕様を記述したファイルのことで、エンドポイントやリクエスト/レスポンス型などを定義します。

API定義ファイルをもとにした自動生成を用いることで **高速かつ統一されたアーキテクチャで開発することができます。**

そのため、特に大規模開発やチーム開発にむいているといえます。

# 環境構築
## goctl
まずはgoctlをインストールします。
```shell
$ go install github.com/zeromicro/go-zero/tools/goctl@latest
```

インストールしたバイナリへのパスを環境変数`PATH`に追加する必要があります。
まだ追加してない場合は以下のコマンドで追加します。

```shell
export PATH=$PATH:<your_main_gopath>/bin
```

:::note info
**GOPATHの確認方法**
`your_main_gopath`の部分は以下のコマンドで確認できます。
```shell
$ go env GOPATH
```
:::

`goctl`を実行して以下のようになれば正しく認識されています。
```shell
$ goctl
A cli tool to generate api, zrpc, model code

GitHub: https://github.com/zeromicro/go-zero
Site:   https://go-zero.dev

Usage:
  goctl [command]

Available Commands:
  api               Generate api related files
  bug               Report a bug
  completion        Generate the autocompletion script for the specified shell
  config            
  docker            Generate Dockerfile
  env               Check or edit goctl environment
  gateway           gateway is a tool to generate gateway code
  help              Help about any command
  kube              Generate kubernetes files
  migrate           Migrate from tal-tech to zeromicro
  model             Generate model code
  quickstart        quickly start a project
  rpc               Generate rpc code
  template          Template operation
  upgrade           Upgrade goctl to latest version

Flags:
  -h, --help      help for goctl
  -v, --version   version for goctl


Use "goctl [command] --help" for more information about a command.
```

goctlには上記のようにいろんなコマンドが存在します。
それぞれの簡単な説明と使用場面をまとめると以下のようになります。
| コマンド      | 説明                                           | 使用場面                                                    |
|---------------|------------------------------------------------|-------------------------------------------------------------|
| api           | API 関連のファイルを生成します。                | API サーバーやサービスの雛形コードを自動生成したいとき。    |
| bug           | バグを報告します。                              | goctl の不具合を公式に報告したいとき。                      |
| completion    | 指定したシェル用の自動補完スクリプトを生成します。| ターミナルでコマンド補完を有効にしたいとき。                |
| config        | 設定ファイルの確認や編集を行います。            | goctl の設定を確認・変更したいとき。                        |
| docker        | Dockerfile を生成します。                       | プロジェクトを Docker コンテナで動かしたいとき。            |
| env           | goctl の環境設定を確認・編集します。            | goctl の動作環境を調整したいとき。                          |
| gateway       | Gateway コードを生成します。                    | API Gateway の雛形コードを作成したいとき。                  |
| help          | 各コマンドのヘルプを表示します。                | コマンドの使い方を知りたいとき。                            |
| kube          | Kubernetes 用のファイルを生成します。           | Kubernetes でデプロイするための設定ファイルを作りたいとき。  |
| migrate       | tal-tech から zeromicro への移行を行います。    | 旧 go-zero プロジェクトを新しいバージョンへ移行したいとき。 |
| model         | モデルコードを生成します。                      | DB テーブル定義から Go のモデルコードを自動生成したいとき。  |
| quickstart    | プロジェクトのクイックスタートを行います。      | 新しいプロジェクトを素早く立ち上げたいとき。                |
| rpc           | RPC コードを生成します。                        | RPC サービスの雛形コードを作成したいとき。                  |
| template      | テンプレート操作を行います。                    | 独自テンプレートの管理や利用をしたいとき。                  |
| upgrade       | goctl を最新版にアップグレードします。           | goctl の新機能やバグ修正を取り込みたいとき。                |

特に`api`や`model`はそれぞれAPI定義ファイル、モデルコードの自動生成コマンドであり、開発中によく用いられます。

# 開発
今回は（も？）/users/:idにGETリクエストを投げると該当するユーザの情報をDBから取得して返すAPIを作成したいと思います。

DBには、Dockerで起動したMySQLを使用します。

## Step0-1: プロジェクトの作成
毎度おなじみの呪文でプロジェクトを作成します。
```shell
mkdir gozero-sample
cd gozero-sample
go mod init
```

## Step0-2: DBの準備
MySQLのコンテナをdocker-composeで起動します。
`compose.yaml`と`init.sql`を作成し、それぞれ以下の内容を記載します。
```compose.yaml
services:
  mysql:
    image: mysql:8.0
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: my_database
      MYSQL_USER: user
      MYSQL_PASSWORD: user_password
    ports:
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql                       # ホストのDockerボリュームであるmysql_dataをコンテナ環境内の/var/lib/mysqlにマウントする、これによりデータが永続化される
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql # テーブル作成や初期データ登録用のSQLを実行

volumes:
  mysql_data:

```

```init.sql
CREATE DATABASE IF NOT EXISTS my_database;
USE my_database;

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name, email) VALUES
('Taro Yamada', 'taro@example.com'),
('Hanako Suzuki', 'hanako@example.com');
```

コンテナを起動します。
```shell
$ docker compose up -d
[+] Running 3/3
 ✔ Network gozero-sample_default      Created                                                            0.0s 
 ✔ Volume "gozero-sample_mysql_data"  Created                                                            0.0s 
 ✔ Container mysql                    Started                                                            0.5s
```

初期化用データが正しく格納されているかも確認しておきます。
ユーザ名やパスワードなどはすべて`compose.yaml`に記載されている通りです。
```shell
$ docker exec -it mysql bash
bash-5.1# mysql -u user -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.42 MySQL Community Server - GPL

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use my_database
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from users;
+----+---------------+--------------------+---------------------+
| id | name          | email              | created_at          |
+----+---------------+--------------------+---------------------+
|  1 | Taro Yamada   | taro@example.com   | 2025-05-20 07:44:15 |
|  2 | Hanako Suzuki | hanako@example.com | 2025-05-20 07:44:15 |
+----+---------------+--------------------+---------------------+
2 rows in set (0.00 sec)
```

正しく登録できていることがわかります。

## Step1: API定義ファイルを作成
ようやくgoctlの登場です。
以下のコマンドで`user.api`を作成します。
```shell
$ goctl api -o user.api
Done.
```
`user.api`は以下のような内容になっているはずです。
```user.api
syntax = "v1"

info (
	title: // TODO: add title
	desc: // TODO: add description
	author: "{ユーザ名}"
	email: "{メールアドレス}"
)

type request {
	// TODO: add members here and delete this comment
}

type response {
	// TODO: add members here and delete this comment
}

service user-api {
	@handler GetUser // TODO: set handler name and delete this comment
	get /users/id/:userId(request) returns(response)

	@handler CreateUser // TODO: set handler name and delete this comment
	post /users/create(request)
}

```
各セクションの詳細は以下の通りです。

| セクション         | 内容・役割                                                                                   |
|--------------------|---------------------------------------------------------------------------------------------|
| syntax             | API仕様ファイルのバージョン指定（ここでは "v1"）                                            |
| info               | APIのタイトルや説明などのメタ情報                                        |
| type request       | リクエストの型定義。APIに渡すパラメータやボディの構造を記述                        |
| type response      | レスポンスの型定義。APIから返すデータの構造を記述                                  |
| service user-api   | サービス名とエンドポイントの定義                                                             |
| @handler GetUser   | GetUserエンドポイントのハンドラー名                                                          |
| get /users/id/:userId(request) returns(response) | ユーザーIDでユーザー情報を取得するGETエンドポイント。request型を受け取り、response型を返す。 |
| @handler CreateUser| CreateUserエンドポイントのハンドラー名                                                       |
| post /users/create(request)                  | ユーザー作成用のPOSTエンドポイント。request型を受け取る。                   |

今回はこの`user.api`を以下のように修正します。
```user.api
type (
  GetUserRequest {
    Id int64 `path:"id"`
  }

  GetUserResponse {
    Id    int64  `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
  }
)

service user-api {
  @handler GetUser
  get /users/:id (GetUserRequest) returns (GetUserResponse)
}
```
この定義をもとにした自動生成（次Step）により、`GetUser`というハンドラが作成されます。
`GetUser`は`/users/:id`へのGETリクエストを受け取ると、`id`に紐づくユーザの情報を返すハンドラです。
これでAPI定義ファイルの作成は完了です。

## Step2: コード自動生成
続いてStep1で作成したAPI定義ファイルをもとにコードを自動生成します。
以下のコマンドを実行するだけです。（便利～！）
```shell
$ goctl api go -api user.api -dir user
Done.
```
新たに`user`というディレクトリが作成されているはずです。
（importで読み込みエラーが出ている場合は`go mod tidy`を実行してください。）
```
.
├── compose.yaml
├── go.mod
├── go.sum
├── init.sql
├── user (new!!)
│   ├── etc
│   │   └── user-api.yaml
│   ├── internal
│   │   ├── config
│   │   │   └── config.go
│   │   ├── handler
│   │   │   ├── getuserhandler.go
│   │   │   └── routes.go
│   │   ├── logic
│   │   │   └── getuserlogic.go
│   │   ├── svc
│   │   │   └── servicecontext.go
│   │   └── types
│   │       └── types.go
│   └── user.go
└── user.api
```

各ファイルについて簡単に説明します。
| ディレクトリ/ファイル                | 役割・内容                                                                                   |
|--------------------------------------|---------------------------------------------------------------------------------------------|
| internal/logic/                      | ビジネスロジック（サービスの本体処理）を実装するディレクトリ。                              |
| internal/handler/                    | 各APIエンドポイントのハンドラー（リクエスト受付・レスポンス返却）を実装するディレクトリ。    |
| internal/svc/                        | サービスコンテキスト（DB接続や外部サービスなどの依存性管理）をまとめるディレクトリ。         |
| internal/types/                      | APIで使うリクエスト・レスポンス型などの型定義をまとめるディレクトリ。                        |
| etc/                                 | 設定ファイル（YAMLなど）を配置するディレクトリ。                                             |
| user.go                              | サーバーのエントリーポイント。APIサーバーの起動処理を記述。                                 |

## Step3: DBモデル（MySQLなど）の構造体自動生成
次にユーザーモデルを作成します。
これもgoctlコマンドを用います。
```shell
$ goctl model mysql datasource -url="user:user_password@tcp(localhost:3307)/my_database" -table="users" -dir user/internal/mode
l
Done.
```

`/user`配下に新たに`model`ディレクトリが作成されています
（importで読み込みエラーが出ている場合は`go mod tidy`を実行してください。）

```
.
├── compose.yaml
├── go.mod
├── go.sum
├── init.sql
├── user
│   ├── etc
│   │   └── user-api.yaml
│   ├── internal
│   │   ├── config
│   │   │   └── config.go
│   │   ├── handler
│   │   │   ├── getuserhandler.go
│   │   │   └── routes.go
│   │   ├── logic
│   │   │   └── getuserlogic.go
│   │   ├── model (new!!)
│   │   │   ├── usersmodel.go
│   │   │   ├── usersmodel_gen.go
│   │   │   └── vars.go
│   │   ├── svc
│   │   │   └── servicecontext.go
│   │   └── types
│   │       └── types.go
│   └── user.go
└── user.api
```
```usermodel.go
package model

import "github.com/zeromicro/go-zero/core/stores/sqlx"

var _ UsersModel = (*customUsersModel)(nil)

type (
	// UsersModel is an interface to be customized, add more methods here,
	// and implement the added methods in customUsersModel.
	UsersModel interface {
		usersModel
		withSession(session sqlx.Session) UsersModel
	}

	customUsersModel struct {
		*defaultUsersModel
	}
)

// NewUsersModel returns a model for the database table.
func NewUsersModel(conn sqlx.SqlConn) UsersModel {
	return &customUsersModel{
		defaultUsersModel: newUsersModel(conn),
	}
}

func (m *customUsersModel) withSession(session sqlx.Session) UsersModel {
	return NewUsersModel(sqlx.NewSqlConnFromSession(session))
}

```

```usermodel_gen.go
// Code generated by goctl. DO NOT EDIT.
// versions:
//  goctl version: 1.8.3

package model

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/zeromicro/go-zero/core/stores/builder"
	"github.com/zeromicro/go-zero/core/stores/sqlx"
	"github.com/zeromicro/go-zero/core/stringx"
)

var (
	usersFieldNames          = builder.RawFieldNames(&Users{})
	usersRows                = strings.Join(usersFieldNames, ",")
	usersRowsExpectAutoSet   = strings.Join(stringx.Remove(usersFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
	usersRowsWithPlaceHolder = strings.Join(stringx.Remove(usersFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
)

type (
	usersModel interface {
		Insert(ctx context.Context, data *Users) (sql.Result, error)
		FindOne(ctx context.Context, id uint64) (*Users, error)
		FindOneByEmail(ctx context.Context, email string) (*Users, error)
		Update(ctx context.Context, data *Users) error
		Delete(ctx context.Context, id uint64) error
	}

	defaultUsersModel struct {
		conn  sqlx.SqlConn
		table string
	}

	Users struct {
		Id        uint64    `db:"id"`
		Name      string    `db:"name"`
		Email     string    `db:"email"`
		CreatedAt time.Time `db:"created_at"`
	}
)

func newUsersModel(conn sqlx.SqlConn) *defaultUsersModel {
	return &defaultUsersModel{
		conn:  conn,
		table: "`users`",
	}
}

func (m *defaultUsersModel) Delete(ctx context.Context, id uint64) error {
	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
	_, err := m.conn.ExecCtx(ctx, query, id)
	return err
}

func (m *defaultUsersModel) FindOne(ctx context.Context, id uint64) (*Users, error) {
	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", usersRows, m.table)
	var resp Users
	err := m.conn.QueryRowCtx(ctx, &resp, query, id)
	switch err {
	case nil:
		return &resp, nil
	case sqlx.ErrNotFound:
		return nil, ErrNotFound
	default:
		return nil, err
	}
}

func (m *defaultUsersModel) FindOneByEmail(ctx context.Context, email string) (*Users, error) {
	var resp Users
	query := fmt.Sprintf("select %s from %s where `email` = ? limit 1", usersRows, m.table)
	err := m.conn.QueryRowCtx(ctx, &resp, query, email)
	switch err {
	case nil:
		return &resp, nil
	case sqlx.ErrNotFound:
		return nil, ErrNotFound
	default:
		return nil, err
	}
}

func (m *defaultUsersModel) Insert(ctx context.Context, data *Users) (sql.Result, error) {
	query := fmt.Sprintf("insert into %s (%s) values (?, ?)", m.table, usersRowsExpectAutoSet)
	ret, err := m.conn.ExecCtx(ctx, query, data.Name, data.Email)
	return ret, err
}

func (m *defaultUsersModel) Update(ctx context.Context, newData *Users) error {
	query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, usersRowsWithPlaceHolder)
	_, err := m.conn.ExecCtx(ctx, query, newData.Name, newData.Email, newData.Id)
	return err
}

func (m *defaultUsersModel) tableName() string {
	return m.table
}

```
このようにユーザモデルのインターフェースとそれを実装するデフォルトユーザモデルが自動で定義されます。

## Step4. ロジック実装
最後にDBへの接続やビジネスロジックを実装していきます。
まず、設定ファイルにDBへの接続情報を追加します。
```/etc/user-api.yaml
Name: user-api
Host: 0.0.0.0
Port: 8888

Mysql:
  DataSource: user:user_password@tcp(localhost:3307)/my_database?charset=utf8mb4&parseTime=true&loc=Asia%2FTokyo
```
?以降はクエリパラメータです。MySQLドライバに細かい設定情報を渡しています。
- charset=utf8mb4：文字コードをUTF-8に
- parseTime=true：DATETIME 型を time.Time に自動変換する
- loc=Asia%2FTokyo：タイムゾーンを「東京」に設定（URLエンコード済）

次に`servicecontext.go`を以下のように修正します。
```servicecontext.go
package svc

import (
	"gozerosample/user/internal/config"
	"gozerosample/user/internal/model"

	"github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
	Config    config.Config
	UserModel model.UsersModel
}

func NewServiceContext(c config.Config) *ServiceContext {
	conn := sqlx.NewMysql(c.Mysql.DataSource)
	return &ServiceContext{
		Config:    c,
		UserModel: model.NewUsersModel(conn),
	}
}
```
ここで **`ServiceContext`は依存関係（設定やDB接続など）を一元管理するための構造です。**
`Config`フィールドにはyamlファイルから読み込まれたアプリ全体の設定が入ります。
`UserModel`にはusersテーブルにアクセスするためのDBモデルです。

`NewServiceContext()`は`ServiceContext`のコンストラクタです。

最後の最後に、`logic/getuserlogic.go`にビジネスロジックを実装します。
現状`logic/getuserlogic.go`の`GetUser()`は空っぽになっているはずです。それを以下のように修正します。

```/logic/getuserlogic.go
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
	user, err := l.svcCtx.UserModel.FindOne(l.ctx, uint64(req.Id))
	if err != nil {
		if err == sqlc.ErrNotFound {
			return nil, errors.New("user not found")
		}
		return nil, err
	}
	return &types.GetUserResponse{
		Id:    int64(user.Id),
		Name:  user.Name,
		Email: user.Email,
	}, nil
}
```

ここで`GetUserLogic`は以下のような構造体です。
```/logic/getuserlogic.go
type GetUserLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}
```
この`svcCtx`を用いることでDBからデータを取得できます。
取得したデータ（`user`）をレスポンスに詰めて返却。エラーハンドリングもつけて終了です。

## 挙動確認
実際に動かしてみます。
まずは以下のコマンドでMySQLのDockerコンテナを起動します。
```shell
docker compose up -d
```

続いて以下のコマンドでサーバを起動します。
```shell
$ go run user/user.go -f user/etc/user-api.yaml 
Starting server at 0.0.0.0:8888...
```

別タブでcurlを使ってGETリクエストを投げてみます。
以下のように情報が取得できれば正しく動いています。
```shell
$ curl http://localhost:8888/users/1
{"id":1,"name":"Taro Yamada","email":"taro@example.com"}
```



# 感想

今回は簡単なAPIを作りながらgo-zeroの使い方について記載しました。
以下、実際に使ってみて感じたことです。

**cliツールの自動生成で枠組み構築が爆速**
- cliツールを用いてプロジェクト構造を一発で作れるのは、GinやEchoにはない大きな利点。Beegoにもcliはあるがgoctlほど柔軟ではない
- CRUDベースのWebアプリの原型を数分で構築可能

**アーキテクチャの分離が強力**
- go-zeroはhandler, logic, model, servicecontextなど、役割が明確に分離されている
- Gin/EchoはMVCではないため、設計力が求められる
- Beegoは"重いMVC"寄り

**学習コストは高い**
- `.api` → コード生成 → ServiceContext → Logic → Model の流れを理解するのに時間がかかる
- Gin/Echo は「ルーティング + ハンドラ書く」だけで始められるので圧倒的に学習コストが低い
- 公式ページにシンプルって書いてあったがほんとにシンプルと言えるのか？むしろボリュームたっぷりに感じた

また、本記事では触れていませんが、go-zeroには独自RPCであるZRPCがあり、容易にZRPCを使ったマイクロサービス間通信が実現できるのも大きな魅力です。


# 結論：こんなときにgo-zeroが向いている！
- スケーラブルで堅牢なAPIサーバーを作りたい
- マイクロサービスを整理された構造で開発したい
- チームで統一された開発スタイルを保ちたい