LoginSignup
2
6

More than 1 year has passed since last update.

Go で API 作る

Last updated at Posted at 2021-10-22

はじめに

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

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

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

早速作ってみる

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

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

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

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

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

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

スクリーンショット 2021-10-17 18.39.08.png

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

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

スクリーンショット 2021-10-17 18.40.00.png

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

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

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

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

Airをインストール

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

airコマンドで起動させる

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

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

サンプルをコピペしてプロジェクトディレクトリ直下に.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を実行します。

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

go.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のファイルが置いてあるところを見るようにします。

.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

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

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

ginを使ってみる

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

ginを取得する

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

ginの書き方に修正

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

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

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

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

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

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" },
    }
}

/api/todosのソースコード
cmd/main.go
+r.GET("/api/todos", getTodos)
cmd/main.go
func getTodos(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H { "todos": todos() })
}

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

スクリーンショット 2021-10-17 22.39.42.png

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

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

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

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

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

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

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

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

/api/todos/:Idのソースコード
cmd/main.go
import (
    "net/http"
    "os"
+   "strconv"

    "github.com/gin-gonic/gin"
)
cmd/main.go
+r.GET("/api/todos/:Id", getTodo)
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 })
}

指定したIdTodoが返ってきました。

スクリーンショット 2021-10-18 0.25.34.png

スクリーンショット 2021-10-18 0.25.50.png

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

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

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

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

{ "Title": "aaa" }

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

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

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

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

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

POST /api/todosのソースコード
cmd/main.go
+r.POST("/api/todos", addTodo)
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 })
}

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

スクリーンショット 2021-10-18 22.05.12.png

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

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

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

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

{ "Title": "aaa" }

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

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

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

PATCH /api/todos/:Idのソースコード
cmd/main.go
+r.PATCH("/api/todos/:Id", updateTodo)
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 })
}

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

スクリーンショット 2021-10-18 22.41.17.png

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

スクリーンショット 2021-10-18 22.41.31.png

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

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

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

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

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

DELETE /api/todos/:Idのソースコード
cmd/main.go
+r.DELETE("/api/todos/:Id", deleteTodo)
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 })
}

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

スクリーンショット 2021-10-18 22.31.31.png

スクリーンショット 2021-10-18 22.31.43.png

おわりに

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

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

参考文献

2
6
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
2
6