Appearance
Go REST APIプロジェクトセットアップ
プロジェクト作成
- 下記コマンドでGoモジュールを初期化します。
shell
go mod init easyapp- mysql向けのドライバをインストールします。
shell
go get github.com/go-sql-driver/mysql実装
サンプルコードを掲載します。ここでは擬似的なログインAPIを実装します。
事前準備
データベースをdockerで構築します。
docker-compose.yaml
yaml
services:
eadb:
image: mariadb:latest
container_name: eadb
ports:
- 3306:3306
volumes:
- ./volumes/initdb.d:/docker-entrypoint-initdb.d
environment:
- MYSQL_ROOT_PASSWORD=passwordvolumes/initdb.d/create-database.sql
sql
CREATE DATABASE eadb;
CREATE TABLE eadb.users (
name VARCHAR(8) PRIMARY KEY
, password VARCHAR(32) NOT NULL
, age INT NOT NULL
);
INSERT INTO eadb.users (
name
, password
, age
) VALUES (
'nob'
, 'passwd'
, 13
);
CREATE USER eadbuser;
GRANT ALL ON eadb.* TO eadbuser@'%' IDENTIFIED BY 'eadbpass';パッケージ構成
shell
.
├── cmd
│ └── server
│ └── main.go # アプリのエントリポイント
└── internal
├── apperrors
│ └── apperrors.go # 汎用的なアプリエラー定義
├── application
│ └── usecase
│ ├── params
│ │ └── user_params.go # 業務処理の入力・出力モデル構造体
│ └── user_usecase.go # 業務処理のインターフェースおよび実装
├── bootstrap
│ └── server.go # 依存性の注入およびルーティング設定
├── domain
│ └── user.go # ドメイン定義およびrepositoryのインターフェース
├── infrastructure
│ ├── db.go # データベース接続設定
│ └── repository
│ └── user_repository.go # ドメインの取得/永続化
└── presentation
├── handler
│ ├── httperror
│ │ └── httperror.go # エラーハンドリング
│ ├── model
│ │ └── user_model.go # APIのリクエスト・レスポンス構造体
│ └── user_handler.go # APIとしての外部契約
└── router
└── router.go # エンドポイントとハンドラの紐付けパッケージ一覧
internal/domain/
業務処理の中心となるドメインおよびそれをデータベースから取得するrepositoryのインターフェースを定義します。
user.go
go
package domain
import (
"context"
"errors"
)
// ユーザ情報ドメインです。
type User struct {
name Name // ユーザ名
password Password // パスワード
age Age // 年齢
}
func NewUser(name string, password string, age int) (User, error) {
userName, err := NewName(name)
if err != nil {
return User{}, err
}
userPassword, err := NewPassword(password)
if err != nil {
return User{}, err
}
userAge, err := NewAge(age)
if err != nil {
return User{}, err
}
return User{name: userName, password: userPassword, age: userAge}, nil
}
func (u User) Name() Name {
return u.name
}
func (u User) Age() Age {
return u.age
}
// パスワードが正しいかを判定します。
func (u User) VerifyPassword(password string) bool {
return u.password.verify(password)
}
// ユーザ情報ドメイン向けrepositoryのインターフェースです。
type UserRepository interface {
// ユーザ情報を取得します。
FindByName(ctx context.Context, target Name) (User, error)
}
var (
ErrInvalidName = errors.New("invalid name")
ErrInvalidPassword = errors.New("invalid password")
ErrInvalidAge = errors.New("invalid age")
ErrNoSuchUser = errors.New("no such user")
)
// ユーザ名
type Name struct {
value string
}
func NewName(value string) (Name, error) {
if value == "" {
return Name{}, ErrInvalidName
}
return Name{value: value}, nil
}
func (v Name) Value() string {
return v.value
}
// パスワード
type Password struct {
value string
}
func NewPassword(value string) (Password, error) {
if value == "" {
return Password{}, ErrInvalidPassword
}
return Password{value: value}, nil
}
// パスワードが正しいかを判定します。
func (v Password) verify(password string) bool {
return v.value == password
}
// 年齢
type Age struct {
value int
}
func (v Age) Value() int {
return v.value
}
func NewAge(value int) (Age, error) {
if value < 0 {
return Age{}, ErrInvalidAge
}
return Age{value: value}, nil
}internal/apperrors/
汎用的なアプリケーションエラー文言を定義します。
apperrors.go
go
package apperrors
import "errors"
var (
ErrBadRequest = errors.New("bad request")
ErrInvalidInput = errors.New("invalid input")
ErrDatabaseOperation = errors.New("database error")
)internal/infrastructure/
データベースへの接続設定を記載します。
db.go
go
package infrastructure
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
// データベースに接続します。
func ConnectDB() *sql.DB {
const (
user string = "eadbuser"
password string = "eadbpass"
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/
SQLを実行してドメイン・テーブル間のデータをやり取りします。
user_repository.go
go
package repository
import (
"context"
"database/sql"
"easyapp/internal/apperrors"
"easyapp/internal/domain"
"errors"
)
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) domain.UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByName(ctx context.Context, target domain.Name) (domain.User, error) {
const query string = "SELECT name, password, age FROM users WHERE name = ?"
// クエリ実行
row := r.db.QueryRowContext(ctx, query, target.Value())
var name string
var password string
var age int
err := row.Scan(&name, &password, &age)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, domain.ErrNoSuchUser
}
return domain.User{}, apperrors.ErrDatabaseOperation
}
return domain.NewUser(name, password, age)
}internal/application/usecase/
usecaseを定義・実装します。アプリの業務はここで処理されます。
user_usecase.go
go
package usecase
import (
"context"
"easyapp/internal/apperrors"
"easyapp/internal/application/usecase/params"
"easyapp/internal/domain"
"errors"
)
// 認証のusecaseインターフェースです。
type UserUsecase interface {
// 認証処理を行います。
Login(ctx context.Context, in params.LoginInput) (params.LoginOutput, error)
// ユーザ情報を取得します。
GetUser(ctx context.Context, in params.GetUserInput) (params.GetUserOutput, error)
}
type userUsecase struct {
userRepository domain.UserRepository
}
func NewUserUsecase(userRepository domain.UserRepository) UserUsecase {
return &userUsecase{userRepository: userRepository}
}
func (u *userUsecase) Login(ctx context.Context, in params.LoginInput) (params.LoginOutput, error) {
name, err := domain.NewName(in.Name())
if err != nil {
return params.LoginOutput{}, apperrors.ErrInvalidInput
}
user, err := u.userRepository.FindByName(ctx, name)
if err != nil {
if errors.Is(err, domain.ErrNoSuchUser) {
return params.LoginOutput{}, nil
}
return params.LoginOutput{}, apperrors.ErrDatabaseOperation
}
return params.NewLoginOutput(user.VerifyPassword(in.Password())), nil
}
func (u *userUsecase) GetUser(ctx context.Context, in params.GetUserInput) (params.GetUserOutput, error) {
name, err := domain.NewName(in.Name())
if err != nil {
return params.GetUserOutput{}, apperrors.ErrInvalidInput
}
user, err := u.userRepository.FindByName(ctx, name)
if err != nil {
if errors.Is(err, domain.ErrNoSuchUser) {
return params.GetUserOutput{}, nil
}
return params.GetUserOutput{}, apperrors.ErrDatabaseOperation
}
return params.NewGetUserOutput(user.Name().Value(), user.Age().Value()), nil
}internal/application/usecase/params/
usecase向けの関数の入力・出力モデル構造体を定義します。
user_params.go
go
package params
// 認証向けの入力モデルです。
type LoginInput struct {
name string // ユーザ名
password string // パスワード
}
func NewLoginInput(name string, password string) LoginInput {
return LoginInput{name: name, password: password}
}
func (i LoginInput) Name() string {
return i.name
}
func (i LoginInput) Password() string {
return i.password
}
// 認証向けの出力モデルです。
type LoginOutput struct {
valid bool // 認証可否
}
func NewLoginOutput(valid bool) LoginOutput {
return LoginOutput{valid: valid}
}
func (o LoginOutput) Valid() bool {
return o.valid
}
// ユーザ情報取得向けの入力モデルです。
type GetUserInput struct {
name string // ユーザ名
}
func NewGetUserInput(name string) GetUserInput {
return GetUserInput{name: name}
}
func (i GetUserInput) Name() string {
return i.name
}
// ユーザ情報取得向けの出力モデルです。
type GetUserOutput struct {
name string // ユーザ名
age int // 年齢
}
func NewGetUserOutput(name string, age int) GetUserOutput {
return GetUserOutput{name: name, age: age}
}
func (o GetUserOutput) Name() string {
return o.name
}
func (o GetUserOutput) Age() int {
return o.age
}internal/presentation/handler/
handlerを定義・実装します。usecaseを呼び出し、レスポンスを作成します。
user_handler.go
go
package handler
import (
"easyapp/internal/application/usecase"
"easyapp/internal/application/usecase/params"
"easyapp/internal/presentation/handler/httperror"
"easyapp/internal/presentation/handler/model"
"encoding/json"
"net/http"
)
// 認証のhandlerです。
type UserHandler struct {
userUsecase usecase.UserUsecase
}
func NewUserHandler(userUsecase usecase.UserUsecase) UserHandler {
return UserHandler{userUsecase: userUsecase}
}
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
req, err := model.NewLoginRequest(r)
if err != nil {
httpStatus, res := httperror.ToHttpErrorResponse(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
json.NewEncoder(w).Encode(res)
return
}
out, err := h.userUsecase.Login(r.Context(), params.NewLoginInput(req.Name, req.Password))
if err != nil {
httpStatus, res := httperror.ToHttpErrorResponse(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
json.NewEncoder(w).Encode(res)
return
}
res := model.NewLoginResponse(out.Valid())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
req := model.NewGetUserRequest(r)
out, err := h.userUsecase.GetUser(r.Context(), params.NewGetUserInput(req.Name))
if err != nil {
httpStatus, res := httperror.ToHttpErrorResponse(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
json.NewEncoder(w).Encode(res)
return
}
res := model.NewGetUserResponse(out.Name(), out.Age())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
}internal/presentation/handler/model/
handler向けの関数の入力・出力モデル構造体を定義します。
user_model.go
go
package model
import (
"easyapp/internal/apperrors"
"encoding/json"
"net/http"
)
// 認証向けのリクエストモデルです。
type loginRequest struct {
Name string `json:"name"` // ユーザ名
Password string `json:"password"` // パスワード
}
func NewLoginRequest(r *http.Request) (loginRequest, error) {
var req loginRequest
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
return loginRequest{}, apperrors.ErrBadRequest
}
return req, nil
}
// 認証向けのレスポンスモデルです。
type loginResponse struct {
Valid bool `json:"valid"` // 認証可否
}
func NewLoginResponse(valid bool) loginResponse {
return loginResponse{Valid: valid}
}
// ユーザ情報取得向けのリクエストモデルです。
type getUserRequest struct {
Name string `json:"name"` // ユーザ名
}
func NewGetUserRequest(r *http.Request) getUserRequest {
return getUserRequest{Name: r.URL.Query().Get("name")}
}
// ユーザ情報取得向けのレスポンスモデルです。
type getUserResponse struct {
Name string `json:"name"` // ユーザ名
Age int `json:"age"` // 年齢
}
func NewGetUserResponse(name string, age int) getUserResponse {
return getUserResponse{Name: name, Age: age}
}internal/presentation/handler/httperror
業務処理層から返ってきたエラーをハンドリングし、エラーレスポンスを作成します。
httperror.go
go
package httperror
import (
"easyapp/internal/apperrors"
"errors"
"net/http"
)
// エラー型に対し、そのHTTPステータスおよびレスポンス構造体を返します。
func ToHttpErrorResponse(err error) (int, any) {
switch {
case errors.Is(err, apperrors.ErrBadRequest):
return http.StatusBadRequest, errorResponse{Message: err.Error()}
case errors.Is(err, apperrors.ErrInvalidInput):
return http.StatusUnprocessableEntity, errorResponse{Message: err.Error()}
case errors.Is(err, apperrors.ErrDatabaseOperation):
return http.StatusInternalServerError, errorResponse{Message: err.Error()}
default:
return http.StatusInternalServerError, errorResponse{Message: "unknown error"}
}
}
// エラーレスポンスです。
type errorResponse struct {
Message string `json:"message"` // エラーメッセージ
}internal/presentation/router/
リクエストのルーティングを実装します。
router.go
go
package router
import (
"easyapp/internal/presentation/handler"
"net/http"
)
// APIのベースURI
const basePath string = "/api/v1"
// UserHandler向けのルーティングをセットします。
func SetUserHandlerRouting(m *http.ServeMux, h handler.UserHandler) {
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, "Method Not Allowed", http.StatusMethodNotAllowed)
}
})
m.HandleFunc(basePath+"/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.GetUser(w, r)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
})
}internal/bootstrap/
依存性の注入およびルーティングを行い、APIの実装を決定します。
server.go
go
package bootstrap
import (
"easyapp/internal/application/usecase"
"easyapp/internal/infrastructure"
"easyapp/internal/infrastructure/repository"
"easyapp/internal/presentation/handler"
"easyapp/internal/presentation/router"
"net/http"
)
// 依存性の注入を行い、アプリケーションの構築を行います。
func NewServer() http.Handler {
// データベースに接続
db := infrastructure.ConnectDB()
// 各handlerに紐づくルーティングを設定
m := http.NewServeMux()
// user
router.SetUserHandlerRouting(
m,
handler.NewUserHandler(
usecase.NewUserUsecase(
repository.NewUserRepository(
db,
),
),
),
)
return m
}cmd/server/
アプリケーションのエントリポイントです。
main.go
go
package main
import (
"easyapp/internal/bootstrap"
"fmt"
"log"
"net/http"
)
func main() {
fmt.Println("Server started at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", bootstrap.NewServer()))
}起動
下記コマンドでアプリを起動します。
shell
go run cmd/server/main.go下記コマンドでAPIを打鍵できます。
shell
# /login
curl -X POST -H 'Content-Type: application/json' -d '{"name": "nob", "password": "passwd"}' localhost:8080/api/v1/login
# /users
curl -X GET localhost:8080/api/v1/users?name=nob