Go REST API プロジェクトセットアップ
プロジェクト作成
- 下記コマンドで Go モジュールを初期化します。
go mod init easyapp
- mysql 向けのドライバをインストールします。
go get github.com/go-sql-driver/mysql
実装
サンプルコードを掲載します。ここでは擬似的なログイン API を実装します。
事前準備
データベースを docker で構築します。
docker-compose.yaml
services:
eadb:
image: mariadb:latest
container_name: eadb
ports:
- 3306:3306
volumes:
- ./volumes/initdb.d:/docker-entrypoint-initdb.d
environment:
- MYSQL_ROOT_PASSWORD=password
volumes/initdb.d/create-database.sql
CREATE DATABASE eadb;
USE eadb;
CREATE TABLE users (
name VARCHAR(8) PRIMARY KEY
, password VARCHAR(32)
, age INT
);
INSERT INTO users (
name
, password
, age
) VALUES (
'nob'
, 'passwd'
, 13
);
パッケージ構成
.
├── cmd
│ └── main.go # アプリのエントリポイント
└── internal
├── domain
│ └── users.go # ドメイン定義およびrepositoryのインターフェース
├── handler
│ ├── auth_handler.go # APIとしてのインターフェースおよび実装
│ ├── model
│ │ └── auth_model.go # APIのリクエスト・レスポンス構造体
│ └── router
│ ├── auth_router.go # 業務処理ごとのルーター
│ └── base.go # エンドポイントのルーター取りまとめ
├── infrastructure
│ ├── db.go # データベース接続設定
│ └── repository
│ └── users_repository.go # データベース操作の実装
└── usecase
├── auth_usecase.go # 業務処理のインターフェースおよび実装
└── payload
└── auth_payload.go # 業務処理の入力・出力モデル構造体
パッケージ一覧
internal/domain/
業務処理の中心となるドメインおよびそれをデータベースから取得する repository のインターフェースを定義します。
- users.go
package domain
// usersテーブル向けエンティティです。
type Users struct {
name string // ユーザ名
password string // パスワード
age int // 年齢
}
func NewUsers(name string, password string, age int) Users {
return Users{name: name, password: password, age: age}
}
func (u Users) Name() string {
return u.name
}
func (u Users) Password() string {
return u.password
}
func (u Users) Age() int {
return u.age
}
// usersテーブル向けrepositoryのインターフェースです。
type UsersRepository interface {
// ユーザ情報を取得します。
FindByName(targetName string) Users
}
internal/infrastructure/
データベースへの接続設定を記載します。
- db.go
package infrastructure
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
// データベースに接続します。
func ConnectDB() *sql.DB {
const (
user string = "root"
password string = "password"
domain string = "localhost:3306"
dbName string = "eadb"
driverName string = "mysql"
)
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s", user, password, domain, dbName)
db, err := sql.Open(driverName, dsn)
if err != nil {
log.Fatal("Fail to connect to Database")
}
// 実際に接続できるかを確認
err = db.Ping()
if err != nil {
log.Fatal("Fail to connect to Database")
}
return db
}
internal/infrastructure/repository/
データベースを操作する repository を実装します。
- users_repository.go
package repository
import (
"database/sql"
"easyapp/internal/domain"
)
type usersRepository struct {
db *sql.DB
}
func NewUsersRepository(db *sql.DB) domain.UsersRepository {
return &usersRepository{db: db}
}
func (r *usersRepository) FindByName(targetName string) domain.Users {
const sql string = "SELECT * FROM users WHERE name = ?"
// クエリ実行
row := r.db.QueryRow(sql, targetName)
var name string
var password string
var age int
row.Scan(&name, &password, &age)
return domain.NewUsers(name, password, age)
}
internal/usecase/
usecase を定義・実装します。アプリの業務はここで処理されます。
- auth_usecase.go
package usecase
import (
"easyapp/internal/domain"
"easyapp/internal/usecase/payload"
"errors"
)
// 認証のusecaseインターフェースです。
type AuthUsecase interface {
// 認証処理を行います。
Login(in payload.LoginIn) payload.LoginOut
// ユーザ情報を取得します。
Me(in payload.MeIn) (payload.MeOut, error)
}
type authUsecase struct {
usersRepository domain.UsersRepository
}
func NewAuthUsecase(usersRepository domain.UsersRepository) AuthUsecase {
return &authUsecase{usersRepository: usersRepository}
}
func (u *authUsecase) Login(in payload.LoginIn) payload.LoginOut {
users := u.usersRepository.FindByName(in.Name())
if users.Name() == "" {
return payload.NewLoginOut(false)
}
return payload.NewLoginOut(users.Password() == in.Password())
}
func (u *authUsecase) Me(in payload.MeIn) (payload.MeOut, error) {
users := u.usersRepository.FindByName(in.Name())
if users.Name() == "" {
return *new(payload.MeOut), errors.New("no such user")
}
return payload.NewMeOut(users.Name(), users.Age()), nil
}
internal/usecase/payload/
usecase 向けの関数の入力・出力モデル構造体を定義します。
- auth_payload.go
package payload
// 認証向けの入力モデルです。
type LoginIn struct {
name string // ユーザ名
password string // パスワード
}
func NewLoginIn(name string, password string) LoginIn {
return LoginIn{name: name, password: password}
}
func (i LoginIn) Name() string {
return i.name
}
func (i LoginIn) Password() string {
return i.password
}
// 認証向けの出力モデルです。
type LoginOut struct {
valid bool // 認証可否
}
func NewLoginOut(valid bool) LoginOut {
return LoginOut{valid: valid}
}
func (o LoginOut) Valid() bool {
return o.valid
}
// ユーザ情報取得向けの入力モデルです。
type MeIn struct {
name string // ユーザ名
}
func NewMeIn(name string) MeIn {
return MeIn{name: name}
}
func (i MeIn) Name() string {
return i.name
}
// ユーザ情報取得向けの出力モデルです。
type MeOut struct {
name string // ユーザ名
age int // 年齢
}
func NewMeOut(name string, age int) MeOut {
return MeOut{name: name, age: age}
}
func (o MeOut) Name() string {
return o.name
}
func (o MeOut) Age() int {
return o.age
}
internal/handler/
handler を定義・実装します。usecase を呼び出し、レスポンスを作成します。
- auth_handler.go
package handler
import (
"easyapp/internal/handler/model"
"easyapp/internal/usecase"
"easyapp/internal/usecase/payload"
"encoding/json"
"net/http"
)
// 認証のhandlerインターフェースです。
type AuthHandler interface {
// 認証処理を呼び出します。
Login(w http.ResponseWriter, r *http.Request)
// ユーザ情報取得処理を呼び出します。
Me(w http.ResponseWriter, r *http.Request)
}
type authHandler struct {
authUsecase usecase.AuthUsecase
}
func NewAuthHandler(authUsecase usecase.AuthUsecase) AuthHandler {
return &authHandler{authUsecase: authUsecase}
}
func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) {
req := model.NewLoginReq(r)
out := h.authUsecase.Login(payload.NewLoginIn(req.Name, req.Password))
res := model.NewLoginRes(out.Valid())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
}
func (h *authHandler) Me(w http.ResponseWriter, r *http.Request) {
req := model.NewMeReq(r)
out, err := h.authUsecase.Me(payload.NewMeIn(req.Name))
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(struct {
Message string `json:"message"`
}{
Message: err.Error(),
})
return
}
res := model.NewMeRes(out.Name(), out.Age())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
}
internal/handler/model/
handler 向けの関数の入力・出力モデル構造体を定義します。
- auth_model.go
package model
import (
"encoding/json"
"net/http"
)
// 認証向けのリクエストモデルです。
type LoginReq struct {
Name string `json:"name"` // ユーザ名
Password string `json:"password"` // パスワード
}
func NewLoginReq(r *http.Request) LoginReq {
var req LoginReq
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
return *new(LoginReq)
}
return req
}
// 認証向けのレスポンスモデルです。
type LoginRes struct {
Valid bool `json:"valid"` // 認証可否
}
func NewLoginRes(valid bool) LoginRes {
return LoginRes{Valid: valid}
}
// ユーザ情報取得向けのリクエストモデルです。
type MeReq struct {
Name string `json:"name"` // ユーザ名
}
func NewMeReq(r *http.Request) MeReq {
return MeReq{Name: r.URL.Query().Get("name")}
}
// ユーザ情報取得向けのレスポンスモデルです。
type MeRes struct {
Name string `json:"name"` // ユーザ名
Age int `json:"age"` // 年齢
}
func NewMeRes(name string, age int) MeRes {
return MeRes{Name: name, Age: age}
}
internal/handler/router/
リクエストのルーティングを実装します。
- base.go
package router
import (
"easyapp/internal/infrastructure"
"net/http"
)
// routerのインターフェースです。
type Router interface {
// ルーティング情報をセットします。
SetRouting(m *http.ServeMux)
}
// APIのベースURI
const basePath string = "/api/v1"
// ルーティングを設定します。
func Routing() *http.ServeMux {
// データベースに接続
db := infrastructure.ConnectDB()
// 各handlerに紐づくルーティングを設定
m := http.NewServeMux()
// auth
NewAuthRouter(db).SetRouting(m)
return m
}
- auth_router.go
package router
import (
"database/sql"
"easyapp/internal/handler"
"easyapp/internal/infrastructure/repository"
"easyapp/internal/usecase"
"net/http"
)
type authRouter struct {
db *sql.DB
}
func NewAuthRouter(db *sql.DB) Router {
return &authRouter{db: db}
}
func (r *authRouter) SetRouting(m *http.ServeMux) {
h := handler.NewAuthHandler(usecase.NewAuthUsecase(repository.NewUsersRepository(r.db)))
// カスタムルータ
m.HandleFunc(basePath+"/login", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
h.Login(w, r)
default:
http.Error(w, "Forbidden", http.StatusForbidden)
}
})
m.HandleFunc(basePath+"/me", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.Me(w, r)
default:
http.Error(w, "Forbidden", http.StatusForbidden)
}
})
}
cmd/
アプリケーションのエントリポイントです。
- main.go
package main
import (
"easyapp/internal/handler/router"
"fmt"
"log"
"net/http"
)
func main() {
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", router.Routing()))
}
起動
下記コマンドでアプリを起動します。
go run cmd/main.go
下記コマンドで API を打鍵できます。
# /login
curl -X POST -H 'Content-Type: application/json' -d '{"name": "nob", "password": "passwd"}' localhost:8080/api/v1/login
# /me
curl -X GET localhost:8080/api/v1/me?name=nob