本記事ではGoの人気なフレームワークの1つであるgo-zeroを用いて簡単なAPIを作成し、その過程でgo-zeroの特徴を解説します。最後に使用感とgo-zeroが向いている開発について考察します。(go-zeroの記事って意外と少ない...?)
対象読者
- go-zeroとはなんぞやの人
- go-zeroの主要機能を一通り理解したい人
- go-zeroを軽く動かしてみたい人
go-zeroとは
go-zeroはTiktok(ByteDance)の元エンジニアによって開発された高性能・高生産性のGoフレームワークです。
従来のモノリシックなシステムをマイクロサービスシステムへ移行させるために開発され、2018年8月にデプロイされました。
以下のような設計原則に基づいて設計されいます。(公式ドキュメント引用)
・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と自動生成を用いた高速な開発が可能」 なところです。
以下のような手順で進めることが一般的です。
- API定義ファイル(
.api
)を書く - コード自動生成
- DBモデル(MySQLなど)の構造体自動生成
- ロジック実装
API定義ファイルとはAPIの仕様を記述したファイルのことで、エンドポイントやリクエスト/レスポンス型などを定義します。
API定義ファイルをもとにした自動生成を用いることで 高速かつ統一されたアーキテクチャで開発することができます。
そのため、特に大規模開発やチーム開発にむいているといえます。
環境構築
goctl
まずはgoctlをインストールします。
$ go install github.com/zeromicro/go-zero/tools/goctl@latest
インストールしたバイナリへのパスを環境変数PATH
に追加する必要があります。
まだ追加してない場合は以下のコマンドで追加します。
export PATH=$PATH:<your_main_gopath>/bin
GOPATHの確認方法
your_main_gopath
の部分は以下のコマンドで確認できます。
$ go env GOPATH
goctl
を実行して以下のようになれば正しく認識されています。
$ 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: プロジェクトの作成
毎度おなじみの呪文でプロジェクトを作成します。
mkdir gozero-sample
cd gozero-sample
go mod init
Step0-2: DBの準備
MySQLのコンテナをdocker-composeで起動します。
compose.yaml
とinit.sql
を作成し、それぞれ以下の内容を記載します。
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:
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');
コンテナを起動します。
$ 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
に記載されている通りです。
$ 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
を作成します。
$ goctl api -o user.api
Done.
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
を以下のように修正します。
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定義ファイルをもとにコードを自動生成します。
以下のコマンドを実行するだけです。(便利~!)
$ 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コマンドを用います。
$ 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
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))
}
// 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への接続情報を追加します。
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
を以下のように修正します。
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()
は空っぽになっているはずです。それを以下のように修正します。
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
は以下のような構造体です。
type GetUserLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
このsvcCtx
を用いることでDBからデータを取得できます。
取得したデータ(user
)をレスポンスに詰めて返却。エラーハンドリングもつけて終了です。
挙動確認
実際に動かしてみます。
まずは以下のコマンドでMySQLのDockerコンテナを起動します。
docker compose up -d
続いて以下のコマンドでサーバを起動します。
$ go run user/user.go -f user/etc/user-api.yaml
Starting server at 0.0.0.0:8888...
別タブでcurlを使ってGETリクエストを投げてみます。
以下のように情報が取得できれば正しく動いています。
$ 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サーバーを作りたい
- マイクロサービスを整理された構造で開発したい
- チームで統一された開発スタイルを保ちたい