LoginSignup
39
40

More than 3 years have passed since last update.

「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する

Last updated at Posted at 2020-05-30

Why Golang?

Golangの特徴として「シンプル」「静的型付けのため高速」「マルチプロセッシングによる並列処理」があります。

このような特徴からDockerKubernetesなどの高速処理を要求されるインフラ基盤構築サービスでも使われており、またイーサリアムのGethなど「ブロックチェーン」の基盤として活用されているケースもあるようです。

また、C言語をベースに開発された言語のため、「構造体」「ポインタ」「チャネル」など普段TypescriptPythonなどのコードを書いていると若干とっつきにくさはあるのですが、C言語よりは数段理解がしやすく、短い記述で処理を書くことができます。
(私自身、C言語で挫折した経験があったので、若干不安ではありましたがコードがシンプルなため他の言語の経験があれば学習コストは非常に低いと思います。)

まとめると「比較的学習コストが低い」かつ「高速」... 最高ですね!

GoogleGolangときたらgRPCだろと突っ込まれるかもしれませんが、今回は基礎をおさえる意味で単純なREST APIで実装を行います。

何を作るか?

今回、ベタですが「商品管理システム」を開発すると仮定して、商品マスタより商品の閲覧・登録・削除・更新ができるような仕様を想定しています。

とりあえずはREST APIの機能に注力したいので、データベースは使わずシステム内で一時的に保持するような仕組みで作成します。

本来であれば、「生産地」や「工場情報」などを入れるべきですが、今回は構成をシンプルにするために最低限の要素に絞っています。また、カテゴリIDなどのリレーションシップも今回は省き、1テーブルのみで機能を実装します。

Properties    Types     Summaries    
id integer 商品ID
jan_code string JANコード
item_name string 商品名
price integer 価格
category_id integer カテゴリID
series_id integer シリーズID
stock integer 在庫数
discontinued boolean 廃番
release_date datetime 発売日
created_at datetime 作成日
updated_at datetime 更新日
deleted_at datetime 削除日

コード実装

バージョン情報

HTTPルーター

Ginなどのフルスタックフレームワークの導入も考えましたが、REST APIを作成するのに最低限の要素にしたかったのでルーター機能だけを提供するgorilla/muxを採用しました。
これは好みの問題ですが、個人的にReactのように「必要なときに必要な分だけ」みたいなパッケージの導入が一番ベストかなと考えています。

それでは早速gorilla/muxgo modulesを使って導入していきます。
go moduleを使うと、システム内で使われているパッケージを管理できるうえ、ビルド時に依存パッケージを自動インストールできます。(便利!)

shellです
go mod init { プロジェクト名 }
go get github.com/gorilla/mux

go.modgo.sumファイルが自動生成されます。

go.mod
module { プロジェクト名 }

go 1.14

require (
    github.com/gorilla/mux v1.7.4
)

フォルダ構成

MVCモデルです。ルートに配置したmain.goファイルからコントローラを呼び出し、サーバを起動します。

フォルダ構成
merchandise_control_system
├── controllers
│   └── webserver.go
├── go.mod
├── go.sum
└── main.go

コード全体

main.go
package main

// go modulesで管理すると絶対パスでのimportが必須です
import (
    "merchandise_control_system/controllers"
)

func main() {
    controllers.StartWebServer()
}
controllers/webserver.go
package controllers

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

// json: ~ をパラメータに付与すると、jsonエンコード時にパラメータ名を指定することができます。
// また、omitemptyを付与するとパラメータが空のときに、jsonのパラメータから消すことができます。
// これはクライアントアプリと仕様を統一する必要があります。
type ItemParams struct {
    Id           string    `json:"id"`
    JanCode      string    `json:"jan_code,omitempty"`
    ItemName     string    `json:"item_name,omitempty"`
    Price        int       `json:"price,omitempty"`
    CategoryId   int       `json:"category_id,omitempty"`
    SeriesId     int       `json:"series_id,omitempty"`
    Stock        int       `json:"stock,omitempty"`
    Discontinued bool      `json:"discontinued"`
    ReleaseDate  time.Time `json:"release_date,omitempty"`
    CreatedAt    time.Time `json:"created_at"`
    UpdatedAt    time.Time `json:"updated_at"`
    DeletedAt    time.Time `json:"deleted_at"`
}

// ポインタ型でitemsを定義します。今回はこのグローバル変数【配列】がデータベースの役割をします
var items []*ItemParams

func rootPage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the Go Api Server")
    fmt.Println("Root endpoint is hooked!")
}

func fetchAllItems(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items)
}

func fetchSingleItem(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    key := vars["id"]

    for _, item := range items {
        if item.Id == key {
            json.NewEncoder(w).Encode(item)
        }
    }
}

func createItem(w http.ResponseWriter, r *http.Request) {
    reqBody, _ := ioutil.ReadAll(r.Body)
    var item ItemParams
    if err := json.Unmarshal(reqBody, &item); err != nil {
        log.Fatal(err)
    }

    items = append(items, &item)
    json.NewEncoder(w).Encode(item)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    for index, item := range items {
        if item.Id == id {
            items = append(items[:index], items[index+1:]...)
        }
    }
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    reqBody, _ := ioutil.ReadAll(r.Body)
    var updateItem ItemParams
    if err := json.Unmarshal(reqBody, &updateItem); err != nil {
        log.Fatal(err)
    }

    for index, item := range items {
        if item.Id == id {
            items[index] = &ItemParams{
                Id:           item.Id,
                JanCode:      updateItem.JanCode,
                ItemName:     updateItem.ItemName,
                Price:        updateItem.Price,
                CategoryId:   updateItem.CategoryId,
                SeriesId:     updateItem.SeriesId,
                Stock:        updateItem.Stock,
                Discontinued: updateItem.Discontinued,
                ReleaseDate:  updateItem.ReleaseDate,
                CreatedAt:    item.CreatedAt,
                UpdatedAt:    updateItem.UpdatedAt,
                DeletedAt:    item.DeletedAt,
            }
        }
    }
}

// 先頭を「大文字」にすると外部ファイルから読み込めるようになります。(export)
func StartWebServer() error {
    fmt.Println("Rest API with Mux Routers")
    router := mux.NewRouter().StrictSlash(true)

    // router.HandleFunc({ エンドポイント }, { レスポンス関数 }).Methods({ リクエストメソッド(複数可能) })
    router.HandleFunc("/", rootPage)
    router.HandleFunc("/items", fetchAllItems).Methods("GET")
    router.HandleFunc("/item/{id}", fetchSingleItem).Methods("GET")

    router.HandleFunc("/item", createItem).Methods("POST")
    router.HandleFunc("/item/{id}", deleteItem).Methods("DELETE")
    router.HandleFunc("/item/{id}", updateItem).Methods("PUT")

    return http.ListenAndServe(fmt.Sprintf(":%d", 8080), router)
}

// モックデータを初期値として読み込みます
func init() {
    items = []*ItemParams{
        &ItemParams{
            Id:           "1",
            JanCode:      "327390283080",
            ItemName:     "item_1",
            Price:        2500,
            CategoryId:   1,
            SeriesId:     1,
            Stock:        100,
            Discontinued: false,
            ReleaseDate:  time.Now(),
            CreatedAt:    time.Now(),
            UpdatedAt:    time.Now(),
            DeletedAt:    time.Now(),
        },
        &ItemParams{
            Id:           "2",
            JanCode:      "3273902878656",
            ItemName:     "item_2",
            Price:        1200,
            CategoryId:   2,
            SeriesId:     2,
            Stock:        200,
            Discontinued: false,
            ReleaseDate:  time.Now(),
            CreatedAt:    time.Now(),
            UpdatedAt:    time.Now(),
            DeletedAt:    time.Now(),
        },
    }
}

デモ

APIサーバを起動します。

go run main.go

Postmanからリクエストを送ってみます。(curlコマンドでもOKです)

【GET】 /items

f899b6d09ac1bc86c96424ab33fd5756.png

【GET】 /item/1

116bc4a421560f04cfb02a9b38327e75.png

【POST】 /item

1fc1dd1fc422be364916f0181e16b3bb.png

【DELETE】 /item/1

5a6ef4e714f695d6e6ab5c6cb0bb9ebc.png

33e7b45ab8ab23ec55c3931235dbad53.png

【PUT】 /item/2

700480fb85e54d8aabd5565e51b775c8.png
8b854b697612094b36a382f0ec5435cd.png

まとめ

今回はコントローラ部分のみで、とりあえず動くように実装してみました。
エラーハンドリングは甘々ですので、改善ポイントは多くあると思います。Golangはエラーハンドリングを行う部分が多く、どこまで徹底するのか判断が難しい気がしますね...

C言語をはじめ、静的型付け言語はどうしても難しそうという抵抗感がありますが、「安全」「高速」などメリットも多いかと思います。
Go言語は深く極めるのは難しいのですが、「とりあえず動くものを作る」ハードルは低く、手軽に試せるのかなという印象を受けました。

また、今回の記事では実装しませんでしたが、以下内容も開発予定です。

開発が終わり次第、こちらも記事にしていきたいと思います。

参考サイト

39
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
40