[初心者向け] Golang でシンプルな JSON API を作る

5 min read読了の目安(約4500字

Go で超シンプルな JSON API を作成します。
基礎的な内容ですが、応用が効くと思います😊

この記事は

Go 1.15.7

を利用しています。

https://github.com/tatsuro-m/golang_json_api
公開しているので、動かしてみたい方はこちらをどうぞ!

完成形

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"
)

type Task struct {
	ID      int       `json:"id"`
	Title   string    `json:"title"`
	Content string    `json:"content"`
	DueDate time.Time `json:"due_date"`
}

var tasks = []Task{{
	ID:      1,
	Title:   "A",
	Content: "Aタスク",
	DueDate: time.Now(),
}, {
	ID:      2,
	Title:   "B",
	Content: "Bタスク",
	DueDate: time.Now(),
}, {
	ID:      3,
	Title:   "C",
	Content: "Cタスク",
	DueDate: time.Now(),
}}

func main() {
	handler1 := func(w http.ResponseWriter, r *http.Request) {
		var buf bytes.Buffer
		enc := json.NewEncoder(&buf)
		if err := enc.Encode(&tasks); err != nil {
			log.Fatal(err)
		}
		fmt.Println(buf.String())

		_, err := fmt.Fprint(w, buf.String())
		if err != nil {
			return
		}
	}

	// GET /tasks
	http.HandleFunc("/tasks", handler1)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

解説

上から解説していきます。

type Task struct {
	ID      int       `json:"id"`
	Title   string    `json:"title"`
	Content string    `json:"content"`
	DueDate time.Time `json:"due_date"`
}

まずは API で返却する構造体を定義します。
Goでは構造体のフィールド名をパスカルケースで書くのがお作法です。しかし json は一般的にスネークケースで扱うのがお作法です。
そのズレを解消するために構造体のフィールドに json タグをうちます。

`json:"id"`

こうしておくことによって、構造体のフィールド名と json のキーをマッピングすることができます。
つまりこの構造体を json にエンコードした時のキー名はタグで指定した値と等しくなります👇

{"id":1,"title":"A","content":"Aタスク","due_date":"2021-05-05T09:54:06.882051+09:00"}

次に Task 構造体のスライスを初期化しています。
make 関数を使って初期化しても良いでしょうが、ここではリテラルで書いています。

var tasks = []Task{{
	ID:      1,
	Title:   "A",
	Content: "Aタスク",
	DueDate: time.Now(),
}, {
	ID:      2,
	Title:   "B",
	Content: "Bタスク",
	DueDate: time.Now(),
}, {
	ID:      3,
	Title:   "C",
	Content: "Cタスク",
	DueDate: time.Now(),
}}

これは API で返却するデータにあたるので、実際のアプリケーションではこのように決め打ちで書くのではなく DB から取得してきたデータを構造体に入れることになると思います。


http.HandleFunc("/tasks", handler1)

は何をしているのでしょうか?
公式ドキュメントを読んでみましょう。

https://golang.org/pkg/net/http/#example_Handle
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

HandleFunc registers the handler function for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.

これはいわゆるルーティングのマッピングです。
pattern にマッチしたリクエストが送られてきた時、どのハンドラー(関数です)を実行するかというマッピングです。
第二引数の handler は「引数に ResponseWriter, *Request の2つを取り、戻り値は何も無い関数型」と読み下せます。
つまり handler として登録する関数はどんなものでも良いのではなく、この型を満たしている必要があります。

その関数が以下です。

handler1 := func(w http.ResponseWriter, r *http.Request) {
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	if err := enc.Encode(&tasks); err != nil {
		log.Fatal(err)
	}
	fmt.Println(buf.String())

	_, err := fmt.Fprint(w, buf.String())
	if err != nil {
		return
	}
}

まずこのハンドラーの目的を整理しましょう。どんなことを叶えるための API エンドポイントなのかということです。

"/tasks" にアクセスがあった時、全てのタスクを json 形式で返却する。

が目的です。

となると、この場合のやるべきことは以下の通りです。

  1. Task を全て取ってくる
  2. 2で取得した Task を json にエンコードする
  3. 3で作成した json をレスポンスとして返す

今回は既にタスク全体は取得済み(スライスにして変数に入れてあるのでした)なので1は省略します。
2 です。

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(&tasks); err != nil {
	log.Fatal(err)
}

まずはバッファを作成する必要があります。
値を json にエンコードしたとして、その値をメモリ上のどこに保存して良いのか分からないので、保存先を伝えてやる必要があります。

enc := json.NewEncoder(&buf)

書き込み先を指定するのですから、 &buf のように必ずポインタを渡すことに注意してください🙌

enc.Encode(&tasks)

で実際にエンコードします。引数に渡すのはエンコードしたい値のポインタです。今回の場合は先ほど定義した Task のスライスです。
これでエンコードは完了です。


3はこの部分です。

fmt.Fprint(w, buf.String())

どうしてこれで json を返却したことになるのでしょうか?
それは、第一引数の w が IO インターフェースを上手く抽象化したものになっているからです。

これに対して「書き込む」ことによってレスポンスになります。
書き込み先を指定するため、 fmt.Fprint を利用します。
書き込む内容はエンコードした json が保存されている buf を文字列に変換したものを指定します。


これでハンドラーとルートのマッピングまで終わりました。
最後にサーバーを起動して終わりです😎

http.ListenAndServe(":8080", nil)

動作確認

$ go run main.go


json が正しく返却されていますね。

おまけ

Rails からやってきた人などは /tasks/:id のようなリクエストを受け取って、id パラメータに応じてデータを検索して返すというのをやりたくなるでしょう。
パラメータに応じてリソースを特定するというのは必ずと言って良いほどある要件だと思います。

ただ、残念ながら調べたところでは Golang は標準でそのような機能を提供していません。
もしこういうことをやりたいのなら、フレームワークに頼るのが良いみたいです🧐

例えば人気の gin というフレームワークでは、パラメータ機能がサポートされています。

https://github.com/gin-gonic/gin#parameters-in-path