プログラミング言語goを使ってWeb API開発する記事はいろいろとありますが、できるだけ外部パッケージを使わない方針で開発しましたので報告します。goを使ったWeb API開発する際の参考になれば幸いです。
また、Web APIを公開する場合、Webサーバー(nginx) + アプリケーションサーバー(go)という構成が一般的(?)かと思いましたので、設定についての記載を最後に追加しました。
ソースコードはGithubにあります。
https://github.com/unokun/go-todo-api
環境
項目 | バージョン |
---|---|
OS | Centos7 |
go | 1.11.4 |
mysql(maria DB) | 5.5.60 |
TODO管理 Web API
baseurl(/todo/api/v1)にPathを加えたURLで各機能を実装しています。
Path | HTTPメソッド | 機能 |
---|---|---|
/tasks | GET | タスク一覧取得 |
/tasks | POST | タスク登録 |
/tasks/:id | PATCH | タスク更新 |
/tasks/:id | DELETE | タスク削除 |
インストール
goのインストールは、以下の記事を、
CentOSにGo言語のインストール - Qiita
また、Web APIの実装については以下の記事を参考にさせていただきました。
GoでWebアプリを作ろう 第一回 : Goで簡単なCRUD - Studio Andy
使った外部パッケージは、以下の二つです。
- gin
- mysql
ginは、go製のWebフレームワークです。jsonレスポンス処理機能も持っている優れものです。
mysqlはDBに合わせて変更してください。
$ go get github.com/gin-gonic/gin
$ go get github.com/go-sql-driver/mysql
ファイル構成
$ tree go-todo-api
go-todo-api
├── db
│ └── createTask.sql
└── src
├── controller
│ └── task.go
├── main.go
└── model
├── model.go
└── task.go
DB
データベース(tododb)作成
専用のユーザー(todo)を作成し権限を付与します。
$ mysql -u root -p
MariaDB [(none)]> create database tododb default charset utf8;
MariaDB [(none)]> create user 'todo'@'localhost' identified by 'todo';
MariaDB [(none)]> grant all on `tododb`.* to 'todo'@'localhost';
テーブルはタイトルのみ持つシンプルな構成です。
USE tododb
DROP TABLE IF EXISTS task;
CREATE TABLE IF NOT EXISTS task (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
title VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
);
model
DBアクセス処理を作成します。
task.goにJson返却用のstructを定義しています。
package model
import (
"time"
)
type Task struct {
ID uint `json:"id"` // id
CreatedAt time.Time `json:"created_at"` // created_at
UpdatedAt time.Time `json:"updated_at"` // updated_at
Title string `json:"title"` // title
}
model.goにDB接続用関数を作成しています。
package model
import (
"database/sql"
"log"
"os"
// mysql driver
_ "github.com/go-sql-driver/mysql"
)
// DBConnect returns *sql.DB
func DBConnect() (db *sql.DB) {
dbDriver := "mysql"
dbUser := "todo"
dbPass := os.Getenv("MYSQL_TODO_PASSWORD") // 環境変数から取得
dbName := "tododb"
dbOption := "?parseTime=true"
db, err := sql.Open(dbDriver, dbUser+":"+dbPass+"@/"+dbName+dbOption)
if err != nil {
log.Fatal(err)
}
return db
}
controller
mainから呼び出すタスク処理を記述します。
package controller
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/unokun/go-todo-api/src/model"
)
// タスク一覧
func TasksGET(c *gin.Context) {
db := model.DBConnect()
result, err := db.Query("SELECT * FROM task ORDER BY id DESC")
if err != nil {
panic(err.Error())
}
// json返却用
tasks := []model.Task{}
for result.Next() {
task := model.Task{}
var id uint
var createdAt, updatedAt time.Time
var title string
err = result.Scan(&id, &createdAt, &updatedAt, &title)
if err != nil {
panic(err.Error())
}
task.ID = id
task.CreatedAt = createdAt
task.UpdatedAt = updatedAt
task.Title = title
tasks = append(tasks, task)
}
c.JSON(http.StatusOK, gin.H{"tasks": tasks})
}
// タスク検索
func FindByID(id uint) model.Task {
db := model.DBConnect()
result, err := db.Query("SELECT * FROM task WHERE id = ?", id)
if err != nil {
panic(err.Error())
}
task := model.Task{}
for result.Next() {
var createdAt, updatedAt time.Time
var title string
err = result.Scan(&id, &createdAt, &updatedAt, &title)
if err != nil {
panic(err.Error())
}
task.ID = id
task.CreatedAt = createdAt
task.UpdatedAt = updatedAt
task.Title = title
}
return task
}
// タスク登録
func TaskPOST(c *gin.Context) {
db := model.DBConnect()
title := c.PostForm("title")
now := time.Now()
_, err := db.Exec("INSERT INTO task (title, created_at, updated_at) VALUES(?, ?, ?)", title, now, now)
if err != nil {
panic(err.Error())
}
fmt.Printf("post sent. title: %s", title)
}
// タスク更新
func TaskPATCH(c *gin.Context) {
db := model.DBConnect()
id, _ := strconv.Atoi(c.Param("id"))
title := c.PostForm("title")
now := time.Now()
_, err := db.Exec("UPDATE task SET title = ?, updated_at=? WHERE id = ?", title, now, id)
if err != nil {
panic(err.Error())
}
task := FindByID(uint(id))
fmt.Println(task)
c.JSON(http.StatusOK, gin.H{"task": task})
}
// タスク削除
func TaskDELETE(c *gin.Context) {
db := model.DBConnect()
id, _ := strconv.Atoi(c.Param("id"))
_, err := db.Query("DELETE FROM task WHERE id = ?", id)
if err != nil {
panic(err.Error())
}
c.JSON(http.StatusOK, "deleted")
}
main
メイン処理を記述します。
package main
import (
"github.com/gin-gonic/gin"
"github.com/unokun/go-todo-api/src/controller"
)
func main() {
router := gin.Default()
v1 := router.Group("/todo/api/v1")
{
v1.GET("/tasks", controller.TasksGET)
v1.POST("/tasks", controller.TaskPOST)
v1.PATCH("/tasks/:id", controller.TaskPATCH)
v1.DELETE("/tasks/:id", controller.TaskDELETE)
}
// nginxのreverse proxy設定
router.Run(":9000")
}
実行
バックグランドジョブとして実行します。
$ MYSQL_TODO_PASSWORD=xxx go run main.go &
API呼び出し
API呼び出しを実行する方法はいろいろありますが、Google Chromeの拡張機能である「Restlet Client」が使いやすいです。
Restlet Client - REST API Testing - Chrome ウェブストア
nginxの設定
Webサーバー(nginx) + アプリケーションサーバー(go webapi)という構成にする場合、リバースプロキシーとして動作させます。locationの記述についてはここを参考にしてください。
$ cat /etc/nginx/conf.d/https.conf
location ^~ /todo/ {
proxy_pass http://127.0.0.1:9000;
}