Skip to content

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