Go で API 作る

# はじめに

https://qiita.com/pure-adachi/items/3732d7d488c6dce8c060

`Go`を勉強し始めたので、`API`サーバーも体験しておこうということで、作ってみました。

`Docker`で環境構築して動かして、`Insomnia`とかから`API`の結果を見れるようにしてみます。

※途中から`Todo`データを`CRUD`する感じの`API`を作り始めますが、`DB`の話はしないので、ご了承ください。

# 早速作ってみる

https://github.com/pure-adachi/go-sample-api-server

`/api/sample`にアクセスすると、`{ "message": "Hello World!!" }`が帰ってくるサンプルを作ってみました。

`Docker`でのサーバー起動コマンドは`go run cmd/main.go`で動かしています。

とりあえず標準ライブラリの`net/http`を使ってます。

```go:cmd/main.go
package main

import (
	"encoding/json"
	"net/http"
	"os"
)

func main() {
	// 環境変数のPORTを取得しています。(PORT=8000）
	port := os.Getenv("PORT")

	// `/api/sample`にアクセスがあるとgetHelloWorldメソッドが呼び出されます。
	http.HandleFunc("/api/sample", getHelloWorld)
	// 8000番ポートでWebサーバが起動し、リクエスト受付状態となります。
	http.ListenAndServe(":" + port, nil)
}

func getHelloWorld(w http.ResponseWriter, _r *http.Request) {
	// レスポンスで返す値を作ります。
	ping := map[string]string{"message": "Hello World!!"}
	// レスポンス値をjsonに変換します。
	res, _ := json.Marshal(ping)
	w.Write(res)
}
```

`/api/sample`にアクセスすると、レスポンスが帰ってきました。

![スクリーンショット 2021-10-17 18.35.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/a6d0ade3-92ac-f629-2d86-35c17f1931a9.png)

# レスポンス値を変えてみる

```diff
-ping := map[string]string{"message": "Hello World!!"}
+ping := map[string]string{"message": "Hello World!"}
```

![スクリーンショット 2021-10-17 18.39.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/63809259-3387-488c-f41d-2a01d4c78bbf.png)

レスポンス値が変わりませんね。。。

`Docker`で起動しているコンテナを再起動して再チャレンジ

![スクリーンショット 2021-10-17 18.40.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/a82be1db-c128-1f04-9ccd-f282c3769ee9.png)

値が変わってくれました。

ソースコードを変更するたびに`Docker`コンテナを再起動するのはめんどくさいですね。。。

# Air を使ってホットリロードさせる

https://github.com/cosmtrek/air

コンテナの再起動がめんどくさいのでソースコードを変更したらホットリロードするようにしてみます。

`Air`をインストール

```Dockerfile:Dockerfile
RUN apk update \
  && go get -u github.com/cosmtrek/air \
  && chmod +x ${GOPATH}/bin/air
```

`air`コマンドで起動させる

```diff:docker-compose.yml
-command: go run cmd/main.go
+command: air
```

`Air`の設定ファイルを追加します。

[サンプル](https://github.com/cosmtrek/air/blob/master/air_example.toml)をコピペしてプロジェクトディレクトリ直下に`.air.toml`ファイルを作成します。

この状態で起動すると`go mod init`しなさいと怒られました。
（プロジェクトを作った最初にやっとけって話ですよね。。。）

```
go: cannot find main module, but found .git/config in /myapp
  to create a module there, run:
  go mod init
failed to build, error: exit status 1
```

`go mod init`を実行します。

```sh
$ docker compose run app go mod init go-sample-api-server
```

`go.mod`が生成されました。


```mod:go.mod
module go-sample-api-server

go 1.17
```

もう一度起動すると別のエラーが出ました。

```
watching .
watching cmd
!exclude tmp
building...
no Go files in /myapp
failed to build, error: exit status 1
```

ルートディレクトリ見たけど、`go`のファイルが無かったよってことですかね。

`go`のファイルが置いてあるところを見るようにします。

```diff:.air.toml
-cmd = "go build -o ./tmp/main ."
+cmd = "go build -o ./tmp/main ./cmd"
```

もう一度起動すると動きました。

```
watching .
watching cmd
!exclude tmp
building...
running...
```

![スクリーンショット 2021-10-17 19.07.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/8066a593-453b-76b1-3b9f-4114ec7a694c.png)

ホットリロードされてるか確かめます。

```diff:cmd/main.go
-ping := map[string]string{"message": "Hello World!"}
+ping := map[string]string{"message": "Hello World!!"}
```

ファイルを保存した瞬間にビルドし直してくれました。

```
running...
cmd/main.go has changed
building...
running...
```

レスポンス値も変わってくれましたね。

![スクリーンショット 2021-10-17 19.09.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/42f45175-d9a3-6a6f-e3a3-93df9fe82840.png)

# ginを使ってみる

`net/http`でサンプルの`API`を作ったところではありますが、せっかくなら人気の`gin`を使ってみましょう。

## ginを取得する

```sh
$ docker compose run app go get -u github.com/gin-gonic/gin
```

## ginの書き方に修正

```diff:cmd/main.go
import (
-	"encoding/json"
	"net/http"
	"os"
+
+	"github.com/gin-gonic/gin"
)

func main() {
	port := os.Getenv("PORT")

+	r := gin.Default()
+	r.GET("/api/sample", getGinSample)
-	http.HandleFunc("/api/sample", getHelloWorld)
+	r.Run(":" + port)
-	http.ListenAndServe(":" + port, nil)
}

+func getGinSample(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H { "message": "Hello Gin World!" })
+}
```

反映されました。
レスポンスの書き方が楽な気がしてます。

![スクリーンショット 2021-10-17 19.41.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/a9274bad-ed50-bec0-604d-20761e910508.png)

# リスト取得APIを作ってみる

ありがちなAPIとして、`Todo`のリストを取得する`API`を作ってみます。

`/api/todos`にアクセスすると、`Todo`のリストが返されるようにしてみます。

レスポンスで返す`Todo`は`todos`メソッドから取得できるようにしておきます。

```go
type Todo struct {
	Id int
	Title string
}

func todos() [3]Todo {
	return [3] Todo {
		{ Id: 1, Title: "TODO 1" },
		{ Id: 2, Title: "TODO 2" },
		{ Id: 3, Title: "TODO 3" },
	}
}
```

<details><summary>`/api/todos`のソースコード</summary><div>

```diff:cmd/main.go
+r.GET("/api/todos", getTodos)
```

```go:cmd/main.go
func getTodos(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H { "todos": todos() })
}
```

</div></details>

`Todo`のリストが返ってきました。

![スクリーンショット 2021-10-17 22.39.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/2faee0d3-17c3-8dee-bd32-e1d13a42b378.png)

# 特定のTodoを取得するAPIを作ってみる

ありがちな`API`として、特定の`Todo`を取得する`API`を作ってみます。

`/api/todos/:Id` にアクセスすると、特定の`Todo`を返すようにします。

レスポンスで返す`Todo`は`todos`のうちパラメタの`Id`に一致するものを返すようにします。

```go
func findTodoById(id int) Todo {
	for _, todo := range todos() {
		if todo.Id == id {
			return todo
		}
	}

	// 一致するものがなければ、Idが-1のTodoを返しておく
	return Todo{ Id: -1 }
}
```

パラメータの`Id`は`Id := c.Param("Id")`で受け取れますが文字列として受け取ってしまいます。
`id, _ := strconv.Atoi(Id)`で数値に変換しましょう。

また、`strconv`を使うので、`import`もしておきます。

<details><summary>`/api/todos/:Id`のソースコード</summary><div>

```diff:cmd/main.go
import (
	"net/http"
	"os"
+	"strconv"

	"github.com/gin-gonic/gin"
)
```

```diff:cmd/main.go
+r.GET("/api/todos/:Id", getTodo)
```

```go:cmd/main.go
func getTodo(c *gin.Context) {
	Id := c.Param("Id")
	id, _ := strconv.Atoi(Id)

	todo := findTodoById(id)

	c.JSON(http.StatusOK, gin.H { "todo": todo })
}
```

</div></details>

指定した`Id`の`Todo`が返ってきました。

![スクリーンショット 2021-10-18 0.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/2b42e864-90a3-de17-f308-693c07d510e4.png)

![スクリーンショット 2021-10-18 0.25.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/631f4e39-9d27-95fe-c31b-0d720f5a9ede.png)

# Todoを追加するAPIを作ってみる

ありがちな`API`として、`Todo`を追加する`API`を作ってみます。

`/api/todos`に`POST`で投げると、`Todo`を追加できるようにします。

以下のデータを一緒に投げます。

```json
{ "Title": "aaa" }
```

受け取るデータの型を宣言します。

```go:cmd/main.go
type InputTodo struct {
	Title string
}
```

データの受け取りは、こんな感じで受け取ります。

```go:cmd/main.go
var inputTodo InputTodo
c.BindJSON(&inputTodo)
```

あとは、`todos`メソッドから既存データっぽく取ってきて連番の`Id`を用意して、渡された`Title`を使って新しい`Todo`を作り、それをレスポンスとしておきます。

<details><summary>POST `/api/todos`のソースコード</summary><div>

```diff:cmd/main.go
+r.POST("/api/todos", addTodo)
```

```go:cmd/main.go
func addTodo(c *gin.Context) {
	var inputTodo InputTodo
	c.BindJSON(&inputTodo)

	data := todos()
	count := len(data)
	newTodo := Todo { Id: count + 1, Title: inputTodo.Title }

	c.JSON(http.StatusOK, gin.H { "todo": newTodo })
}
```

</div></details>

追加した`Todo`が返ってきました。

![スクリーンショット 2021-10-18 22.05.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/5b11ef5d-651a-b3e6-3906-e78024669a3a.png)

# Todoを更新するAPIを作ってみる

ありがちな`API`として、`Todo`を更新する`API`を作ってみます。

`/api/todos/:Id`に`PATCH`で投げると、`Todo`を更新できるようにします。

追加`API`と同様で、以下のデータを一緒に投げます。

```json
{ "Title": "aaa" }
```

受け取るデータの型は追加時と同じで良さそうです。

[特定のTodoを取得するAPIを作ってみる](#特定のtodoを取得するapiを作ってみる)と同様に`Todo`を特定し、[Todoを追加するAPIを作ってみる](#todoを追加するapiを作ってみる)と同様にデータを受け取り、変更を加えた`Todo`を返してみます。

但し、パラメタの`Id`で指定した`Todo`が見つからなかった場合は、投げたデータで変更されないような制御でも入れておきます。

<details><summary>PATCH `/api/todos/:Id`のソースコード</summary><div>

```diff:cmd/main.go
+r.PATCH("/api/todos/:Id", updateTodo)
```

```go:cmd/main.go
func updateTodo(c *gin.Context) {
	Id := c.Param("Id")
	id, _ := strconv.Atoi(Id)

	todo := findTodoById(id)

	var inputTodo InputTodo
	c.BindJSON(&inputTodo)

	if todo.Id != -1 {
		todo.Title = inputTodo.Title
	}

	c.JSON(http.StatusOK, gin.H { "todo": todo })
}
```

</div></details>

指定した`Todo`が変更された状態で返ってきました。

![スクリーンショット 2021-10-18 22.41.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/6c7ae759-fe29-60ff-f45a-f04547e38d6f.png)

パラメタの`Id`で指定した`Todo`が見つからなかった場合は、`Title`が入ってないので想定通り動いていますね。

![スクリーンショット 2021-10-18 22.41.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/efa12f5c-0e71-e67c-4055-397fca70e74a.png)

# Todoを削除するAPIを作ってみる

ありがちな`API`として、`Todo`を削除する`API`を作ってみます。

`/api/todos/:Id`に`DELETE`で投げると、`Todo`を削除できるようにします。

パラメタの`Id`を受け取るだけなので、[特定のTodoを取得するAPIを作ってみる](#特定のtodoを取得するapiを作ってみる)と同様に`Todo`を特定します。

削除されたかどうかを判断するために、削除しようとしている`Todo`以外の`Todo`をリストで返す感じにしてみます。

<details><summary>DELETE `/api/todos/:Id`のソースコード</summary><div>

```diff:cmd/main.go
+r.DELETE("/api/todos/:Id", deleteTodo)
```

```go:cmd/main.go
func deleteTodo(c *gin.Context) {
	Id := c.Param("Id")
	id, _ := strconv.Atoi(Id)

	var activeTodo [] Todo

	for _, todo := range todos() {
		if todo.Id != id {
			activeTodo = append(activeTodo, todo)
		}
	}
	
	c.JSON(http.StatusOK, gin.H { "todos": activeTodo })
}
```

</div></details>

削除しようとしている`Todo`以外の`Todo`がリストで返ってきました。

![スクリーンショット 2021-10-18 22.31.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/c4ba2114-a01f-6a1f-86ea-c4a9c8a4aa1b.png)

![スクリーンショット 2021-10-18 22.31.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/7cf72eb1-d460-3c94-ad85-6736f4fd528c.png)

# おわりに

慣れてない言語で書くのは少し大変ですが、`Ruby`を知っているので「あれ」をやりたいと思えば、調べるとなんとかなるもんですね。

今回は`DB`関係の話をしていないので、そのあたりにもチャレンジできて行けたらいいなと思ってます。

# 参考文献

- [【Golang/Go言語】Airでホットリロードをする方法](https://awesome-linus.com/2020/09/13/go-air-hot-reload/)
- [Go言語 ginでのpost request受信とresponse](https://elsammit-beginnerblg.hatenablog.com/entry/2021/02/13/152036)