Go(echo)とMySQLでAPIサーバー構築してみた

# はじめに
Goを使ってみたいな〜と思っていた矢先、就職先の新人研修でAPIサーバーを構築する機会があったので、練習として簡単に作ってみました。  

概要としては、以下の流れを実行するAPIサーバーを作ってみました。
1. POSTメソッドで送られてくるデータを受け取る
2. 受け取ったデータを元に、SQL側にリアクションを求める
3. 求めたリアクションによる結果をjson形式で返す

## ディレクトリ構造
以下の通り。同じディレクトリ内で作りました。
```
.
├─ api.go
├─ main.go
├─ go.mod
└─ go.sum
```

## データベース構造
データベース名は`sample`で、テーブル名は`employee`にしました。
``` 
// データベース作成
CREATE DATABASE sample;

// 使用するデータベースの選択
USE sample;

// テーブル作成
CREATE TABLE employee (id integer PRIMARY KEY, name varchar(10));
```

## ソースコード

### api.go

`api.go`では、POSTメソッドで送られてきたjson情報を元に、データベースに対して特定の処理を実行する様になっています。今回は、データベースへデータの追加(`INSERT`)、削除(`DELETE`)、検索(`SELECT`)です。
``` api.go
package main

import (
	"fmt"
	"database/sql"
	"net/http"

	"github.com/labstack/echo"
	_ "github.com/lib/pq"
	"github.com/go-sql-driver/mysql"
)

/* 受け取る用のjson */
type Employee struct {
	Id int `json:"id"`
	Name string `json:"name"`
}

/* 受け取ったjson情報を判定するフラグ */
type ReturnJson struct {
	Flag int `json:"flag"`
}

var db *sql.DB

func init() {
	// 使用するデータベースの情報
	config := mysql.Config{
		DBName: "sample",
		User: "root",
	}
	
	var err error
	db, err = sql.Open("mysql", config.FormatDSN())
	if err != nil {
		panic(err)
	}
}

func Post(c echo.Context, exec_type string) error {
	post := new(Employee)
	if err := c.Bind(post); err != nil {
		return err
	}

	var where string
	if post.Id != 0 && post.Name != "" {
		where = fmt.Sprintf("WHERE id=%d AND name='%s'", post.Id, post.Name)
	} else if post.Id != 0 {
		where = fmt.Sprintf("WHERE id=%d", post.Id)
	} else if post.Name != "" {
		where = fmt.Sprintf("WHERE name='%s'", post.Name)
	} else {
		panic("Not enough data.")
	}

	table_name := "employee" // 使用するテーブル名
	var exec_sentence string
	switch exec_type{
		case "add":
			exec_sentence = fmt.Sprintf("INSERT INTO %s VALUES (%d, '%s');", table_name, post.Id, post.Name)
		case "delete":
			exec_sentence = fmt.Sprintf("DELETE FROM %s %s;", table_name, where)
		case "select":
			exec_sentence = fmt.Sprintf("SELECT id, name FROM %s %s;", table_name, where)
		default:
			panic("Maybe you have the wrong URL?")
	}

	fmt.Println(exec_sentence)
	resJson := ReturnJson{}
	rows, err := db.Query(exec_sentence)
	if err != nil {
		resJson.Flag = 0
	} else {
		resJson.Flag = 1
	}

	if exec_type == "select" {
		count_sentence := fmt.Sprintf("SELECT COUNT(*) FROM %s %s;", table_name, where)
		var cnt int
		err = db.QueryRow(count_sentence).Scan(&cnt)
		resJsonSelect := make([]Employee, cnt)
		for rows.Next() {
			cnt --
			rows.Scan(&resJsonSelect[cnt].Id, &resJsonSelect[cnt].Name)
		}
		fmt.Println(resJsonSelect)
		return c.JSON(http.StatusCreated, &resJsonSelect)
	}

	return c.JSON(http.StatusCreated, resJson)
}

/* POSTメソッドで送られてきた情報をデータベースに追加する */
func PostAddMember(c echo.Context) error {
	return Post(c, "add")
}

/* POSTメソッドで送られてきた情報を元に、データベースから削除する */
func PostDeleteMember(c echo.Context) error {
	return Post(c, "delete")
}

/* POSTメソッドで送られてきた情報をデータベースから探索する */
func PostSelectMember(c echo.Context) error {
	return Post(c, "select")
}
```  
個人的に苦戦した箇所は、`SELECT`によって複数の結果が返ってくる場面(同じデータのときとか)に対して、どうやってjsonファイルを送り返そうか、という点でした。  
色々調べてみたところ、make関数を使う方法が良さそうだと思ったのですが、どうやら[SQL側で取得した結果が何個であるかを調べる術はGo側にない](https://stackoverflow.com/questions/37629357/how-to-get-count-of-sql-rows-without-using-next)らしく、SQL側で`SELECT COUNT(*) ~`することによって解消しました。  

途中でswitch文が出現していますが、これは気分です。普段Pythonばっかり触っているので、switch文が新鮮で使ってみたくなった。ただそれだけです。

### main.go

`main.go`では、APIサーバーの制限やどういった情報を受け取るか、といったAPIに関する設定が主です。
``` main.go
package main

import (
	"net/http"
	"github.com/labstack/echo/middleware"
	"github.com/labstack/echo"
)

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"*"},
		AllowMethods: []string{
			http.MethodPost,
		},
	}))

	e.POST("/add", PostAddMember)
	e.POST("/delete", PostDeleteMember)
	e.POST("/select", PostSelectMember)

	e.Logger.Fatal(e.Start(":8080"))
}
```

### go.mod

Goのモジュールとかの環境を整理してくれるファイルです。
``` go.mod
module main

go 1.20

require (
	github.com/go-sql-driver/mysql v1.7.1
	github.com/labstack/echo v3.3.10+incompatible
	github.com/lib/pq v1.10.9
)

require (
	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
	github.com/labstack/gommon v0.4.0 // indirect
	github.com/mattn/go-colorable v0.1.11 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	github.com/valyala/bytebufferpool v1.0.0 // indirect
	github.com/valyala/fasttemplate v1.2.1 // indirect
	golang.org/x/crypto v0.11.0 // indirect
	golang.org/x/net v0.10.0 // indirect
	golang.org/x/sys v0.10.0 // indirect
	golang.org/x/text v0.11.0 // indirect
)
```

### go.sum
気づいたら勝手に作成されています。

## 実際に実行してみた
一方のターミナルで、今回のGoファイルがある階層まで移動して、以下を実行。
```
go run *.go
```
これで、カレントディレクトリ直下にあるGoファイルが全て実行できるみたいです。  

そして、他方のターミナルを用意して、そこから`curl`コマンドを叩いて、実行確認。

### データを追加

```
curl -X POST http://localhost:8080/add -d "id=1" -d "name=taro"
curl -X POST http://localhost:8080/add -d "id=2" -d "name=hanako"
curl -X POST http://localhost:8080/add -d "id=3" -d "name=kenta"
curl -X POST http://localhost:8080/add -d "id=4" -d "name=hanako"
```
SQL側を確認してみる。
```
mysql> SELECT * FROM employee;
+----+--------+
| id | name   |
+----+--------+
|  1 | taro   |
|  2 | hanako |
|  3 | kenta  |
|  4 | hanako |
+----+--------+
4 rows in set (0.00 sec)
```

### データを削除
```
curl -X POST http://localhost:8080/delete -d "id=3"
```
SQL側を確認してみる。
```
mysql> SELECT * FROM employee;
+----+--------+
| id | name   |
+----+--------+
|  1 | taro   |
|  2 | hanako |
|  4 | hanako |
+----+--------+
```

### データを検索
```
curl -X POST http://localhost:8080/select -d "id=1" -d "name=taro"
[{"id":1,"name":"taro"}]

curl -X POST http://localhost:8080/select -d "name=hanako"
[{"id":4,"name":"hanako"},{"id":2,"name":"hanako"}]
```

URLを変えたり、データを変えたりして、SQL側に反映されていることが確認できました！

# おわりに
どうやら、`curl`コマンドだとAPIリクエストのヘッダー情報とかを細かく指定せずとも、APIサーバーへアクセスできるのですが、実際にwebサーバーとかからアクセスする際には、ヘッダー情報をきちんと設定しないといけないらしいです。その辺は、追って勉強します。  

また、今回はMySQLをSQL文を使って操作しましたが、余裕があれば、GoのORMである[GORM](https://gorm.io/ja_JP/)も使ってみたいです。  
個人的には、SQLの中ではMySQLが慣れているので、ORMを使わずとも苦労はしませんでしたが、他のSQLを使うとなると、ちょっとMySQLと文法が変わるので、そういった場面でGORMを試してみたいです。 
 
[こちらの記事](https://qiita.com/Ping/items/d5d8468dadd9c1287f5e)を読んでみて、MySQLが少数派の文法だと知りました..。