Why Golang?
Golang
の特徴として「シンプル」「静的型付けのため高速」「マルチプロセッシングによる並列処理」があります。
このような特徴からDocker
やKubernetes
などの高速処理を要求されるインフラ基盤構築サービスでも使われており、またイーサリアムのGeth
など「ブロックチェーン」の基盤として活用されているケースもあるようです。
また、C言語
をベースに開発された言語のため、「構造体」「ポインタ」「チャネル」など普段Typescript
やPython
などのコードを書いていると若干とっつきにくさはあるのですが、C言語よりは数段理解がしやすく、短い記述で処理を書くことができます。
(私自身、C言語で挫折した経験があったので、若干不安ではありましたがコードがシンプルなため他の言語の経験があれば学習コストは非常に低いと思います。)
まとめると「比較的学習コストが低い」かつ「高速」... 最高ですね!
Google
→Golang
ときたら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 | 削除日 |
コード実装
バージョン情報
- go 1.14
- github.com/gorilla/mux v1.7.4
HTTPルーター
Gin
などのフルスタックフレームワークの導入も考えましたが、REST API
を作成するのに最低限の要素にしたかったのでルーター機能だけを提供するgorilla/mux
を採用しました。
これは好みの問題ですが、個人的にReact
のように「必要なときに必要な分だけ」みたいなパッケージの導入が一番ベストかなと考えています。
それでは早速gorilla/mux
をgo modulesを使って導入していきます。
go module
を使うと、システム内で使われているパッケージを管理できるうえ、ビルド時に依存パッケージを自動インストールできます。(便利!)
go mod init { プロジェクト名 }
go get github.com/gorilla/mux
go.mod
・go.sum
ファイルが自動生成されます。
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
コード全体
package main
// go modulesで管理すると絶対パスでのimportが必須です
import (
"merchandise_control_system/controllers"
)
func main() {
controllers.StartWebServer()
}
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
【GET】 /item/1
【POST】 /item
【DELETE】 /item/1
【PUT】 /item/2
まとめ
今回はコントローラ部分のみで、とりあえず動くように実装してみました。
エラーハンドリングは甘々ですので、改善ポイントは多くあると思います。Golang
はエラーハンドリングを行う部分が多く、どこまで徹底するのか判断が難しい気がしますね...
C言語をはじめ、静的型付け言語はどうしても難しそうという抵抗感がありますが、「安全」「高速」などメリットも多いかと思います。
Go言語は深く極めるのは難しいのですが、「とりあえず動くものを作る」ハードルは低く、手軽に試せるのかなという印象を受けました。
また、今回の記事では実装しませんでしたが、以下内容も開発予定です。
- 「モデル」部分を
Gorm
を利用したMysql
へのデータベース保存(以下URLに、記事を投稿いたしました) - 「Golang」 ×「Gorm」でシンプルに「Mysql」を操作する
- 「ビュー」の部分は切り離して
ReactNative
で別途モバイルアプリ開発
開発が終わり次第、こちらも記事にしていきたいと思います。