GoでREST APIを開発する方法①~ブログアプリ編~(Go + React )

Golang_react

こんにちは、ミナトです。

本記事では、Go言語の標準ライブラリを利用してREST APIを実装する方法を解説します。

バックエンドにGo言語、フロントエンドにReactを利用して、簡易的なブログアプリを開発します。

まずは、Go言語を用いてAPIの実装を行い、次回の記事でフロントエンドの実装を行います。

https://github.com/Minato000/go_blog_api

Golang_react

GoでREST APIを開発する方法②~ブログアプリ編~(Go + React )

2021年11月2日

ライブラリのインストール

今回はDBにSQLiteを利用しますので、以下コマンドを実行してライブラリをインストールしてください。

go get github.com/mattn/go-sqlite3

DBへの接続とDB操作

作業ディレクトリにmodel.goという名前で新規にファイルを作成します。

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
)

type Post struct {
	Id     int    `json:"id"`
	Title  string `json:"title"`
	Body   string `json:"body"`
	Author string `json:"author"`
}

var Db *sql.DB
func init() {
	var err error
	Db, err = sql.Open("sqlite3", "./example.sqlite")

	if err != nil {
		panic(err)
	}
}

func getPosts(limit int) (posts []Post, err error) {
	stmt := "SELECT id, title, body, author FROM posts LIMIT $1"
	rows, err := Db.Query(stmt, limit)
	if err != nil {
		return
	}

	for rows.Next() {
		post := Post{}
		err = rows.Scan(&post.Id, &post.Title, &post.Body, &post.Author)
		if err != nil {
			return
		}
		posts = append(posts, post)
	}
	rows.Close()
	return
}

// retrieve get a specified post.
func retrieve(id int) (post Post, err error) {
	post = Post{}
	stmt := "SELECT id, title, body, author FROM posts WHERE id = $1"
	err = Db.QueryRow(stmt, id).Scan(&post.Id, &post.Title, &post.Body, &post.Author)
	return
}

// create a new post.
func (post *Post) create() (err error) {
	stmt := "INSERT INTO posts (title, body, author) values ($1, $2, $3) RETURNING id"
	err = Db.QueryRow(stmt, post.Title, post.Body, post.Author).Scan(&post.Id)
	return
}

// update a specified post.
func (post *Post) update() (err error) {
	stmt := "UPDATE posts set title = $1, body = $2, author = $3 WHERE id = $4"
	_, err = Db.Exec(stmt, post.Title, post.Body, post.Author, post.Id)
	return
}

// delete a specified post.
func (post *Post) delete() (err error) {
	stmt := "DELETE FROM posts WHERE id = $1"
	_, err = Db.Exec(stmt, post.Id)
	return
}

パッケージは後ほど作成するmain.goと同じmainとしておきます。

package main

まずは、以下で必要なライブラリをインポートしています。

database/sqlはSQLを利用してDBを操作するために必要なライブラリです。

また、github.com/mattn/go-sqlite3はsqlite3を操作するためのドライバを提供します。sqlite3を扱う場合はこちらが必要になりますので、併せてインポートしておきます。

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
)

続いて、投稿(post)のデータを格納する型(struct)を作成しておきます。structは他の言語でのClassのようなものだと思ってください。

フィールド名 データ型 jsonのキー名の形式でフィールドを定義します。

jsonの部分は省略可能ですが、指定しておくとjson出力した際のキーを明示的に指定することができます。指定しない場合はstructのフィールド名で出力されます。

type Post struct {
	Id     int    `json:"id"`
	Title  string `json:"title"`
	Body   string `json:"body"`
	Author string `json:"author"`
}

続いてinit()を利用して、DBとのコネクションを作成します。

init()はパッケージの初期化などに利用される関数で、後で実装するmain.goでmain()が実行される前に実行されます。

DBとの接続にはsql.Open()を利用します。第一引数にドライバを第二引数にデータソース名を指定します。

main()を実行した際にexample.sqliteが作成されます。

var Db *sql.DB
func init() {
	var err error
	Db, err = sql.Open("sqlite3", "./example.sqlite")

	if err != nil {
		panic(err)
	}
}

以下の関数では最大の件数limitを引数に指定して、Postのスライスを返します。

Db.Query(SQL文, パラメータ)を利用して、DBからデータを取得します。SQLに動的に値を渡したい場合は$1のようなプレースホルダーを利用します。第二引数に渡した値が$1にセットされてSQLが実行されます。

次にDBから取得したrowsをfor文で繰り返し、postsに追加していきます。

rows.Scan()を実行することでPost型の構造体postに値をセットできます。

func getPosts(limit int) (posts []Post, err error) {
	stmt := "SELECT id, title, body, author FROM posts LIMIT $1"
	rows, err := Db.Query(stmt, limit)
	if err != nil {
		return
	}

	for rows.Next() {
		post := Post{}
		err = rows.Scan(&post.Id, &post.Title, &post.Body, &post.Author)
		if err != nil {
			return
		}
		posts = append(posts, post)
	}
	rows.Close()
	return
}

こちらの関数では引数に指定したIDのpostを1件取得します。

// retrieve get a specified post.
func retrieve(id int) (post Post, err error) {
	post = Post{}
	stmt := "SELECT id, title, body, author FROM posts WHERE id = $1"
	err = Db.QueryRow(stmt, id).Scan(&post.Id, &post.Title, &post.Body, &post.Author)
	return
}

こちらのメソッドでは新規にpostのデータを作成します。

retrieve()とは違い、 レシーバーを使い構造体Postのメソッドとして定義しています。(Go言語にはクラスの仕組みはありませんが、このように型にメソッドを定義できます。)

funcキーワードとメソッド名の間の引数がレシーバです。create()はpost と言う名前のPost型のレシーバーを持つことを意味しています。メソッドの中では、postという変数名で利用できます。

// create a new post.
func (post *Post) create() (err error) {
	stmt := "INSERT INTO posts (title, body, author) values ($1, $2, $3) RETURNING id"
	err = Db.QueryRow(stmt, post.Title, post.Body, post.Author).Scan(&post.Id)
	return
}

こちらのメソッドではpostのデータを更新します。こちらもPost型のメソッドとして定義しています。

// update a specified post.
func (post *Post) update() (err error) {
	stmt := "UPDATE posts set title = $1, body = $2, author = $3 WHERE id = $4"
	_, err = Db.Exec(stmt, post.Title, post.Body, post.Author, post.Id)
	return
}

こちらのメソッドではpostのデータを削除します。こちらもPost型のメソッドとして定義しています。

// delete a specified post.
func (post *Post) delete() (err error) {
	stmt := "DELETE FROM posts WHERE id = $1"
	_, err = Db.Exec(stmt, post.Id)
	return
}

REST APIの実装

投稿(post)を操作するREST APIを実装していきます。

作業ディレクトリにmain.go作成してください。以下がソースコードの全体です。

package main

import (
	"encoding/json"
	"net/http"
	"path"
	"strconv"
)

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}

	http.HandleFunc("/posts", handleGetList)
	http.HandleFunc("/posts/", handleRequest)

	server.ListenAndServe()
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
	var err error

	w.Header().Set("Access-Control-Allow-Headers", "*")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set( "Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS" )

	switch r.Method {
	case "GET":
		err = handleGet(w, r)
	case "POST":
		err = handlePost(w, r)
	case "PUT":
		err = handlePut(w, r)
	case "DELETE":
		err = handleDelete(w, r)
	}

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// postの一覧を取得するハンドラ
func handleGetList(w http.ResponseWriter, r *http.Request) {
	var err error

	posts, err := getPosts(100)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	output, err := json.MarshalIndent(&posts, "", "\t")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	w.Header().Set("Access-Control-Allow-Headers", "*")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set( "Access-Control-Allow-Methods","GET" )
	w.Write(output)
}

// GETで指定したidのpostを取得するハンドラ
func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
	// Base は,path の最後の要素を返します。 末尾のスラッシュは,最後の要素を抽出する前に削除されます。
	id, err := strconv.Atoi(path.Base(r.URL.Path))
	if err != nil {
		return
	}

	post, err := retrieve(id)
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

// POSTでpostを新規作成するハンドラ
func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
	contentLength := r.ContentLength
	contentBody := make([]byte, contentLength)
	r.Body.Read(contentBody)

	var post Post
	err = json.Unmarshal(contentBody, &post)
	if err != nil {
		return
	}

	err = post.create()
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

// PUTで指定したidのpostを更新するハンドラ
func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path))
	if err != nil {
		return
	}

	post, err := retrieve(id)
	if err != nil {
		return
	}

	contentLength := r.ContentLength
	contentBody := make([]byte, contentLength)
	r.Body.Read(contentBody)

	err = json.Unmarshal(contentBody, &post)
	if err != nil {
		return
	}

	err = post.update()
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

// DELETEで指定したidのpostを削除するハンドラ
func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path))
	if err != nil {
		return
	}

	post, err := retrieve(id)
	if err != nil {
		return
	}

	err = post.delete()
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

まずは必要なライブラリをインポートします。

import (
	"encoding/json"
	"net/http"
	"path"
	"strconv"
)

続いて、APIサーバーを作成して、 起動する処理を実装します。

http.Server{}で サーバーのアドレスとポートを指定します。

HandleFunc()を利用してルーティングの設定を行います。HandleFuncの第一引数にパスを第二引数にハンドラを登録します。

最後にListenAndServe()でサーバーを起動します。

unc main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}

	http.HandleFunc("/posts", handleGetList)
	http.HandleFunc("/posts/", handleRequest)

	server.ListenAndServe()
}

ここからは、各ハンドラについてみていきましょう。

まずは、投稿の一覧を取得するハンドラhandleGetListを確認します。

ハンドラは第一引数にhttp.ResponseWriter、第二引数にhttp.Requestのポインタをとる関数です。

まずは、先程定義したgetPosts()で投稿を取得します。 エラーが発生した場合はhttp.StatusInternalServerErrorを返します。

次にjson.MarshalIndent()を利用してpostsをjsonに変換します。

http.ResponseWriterのHeader().Set()を利用してレスポンスヘッダをセットしています。こちらは最終的にReactで実装するフロントエンドのアプリケーションからアクセスする際にCORSの制限にかからないようにするために必要となります。

最後にhttp.ResponseWriterのWrite(output)で結果の出力を行います。

func handleGetList(w http.ResponseWriter, r *http.Request) {
	var err error

	posts, err := getPosts(100)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	output, err := json.MarshalIndent(&posts, "", "\t")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	w.Header().Set("Access-Control-Allow-Headers", "*")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set( "Access-Control-Allow-Methods","GET" )
	w.Write(output)
}

続いて、個別の投稿を操作するAPIの実装を行います。

こちらではswitch文を利用してリクエストメソッドに応じてハンドラーを切り分けています。実際の処理はswitch文の中で実行されている各ハンドラによって処理されます。

func handleRequest(w http.ResponseWriter, r *http.Request) {
	var err error

	w.Header().Set("Access-Control-Allow-Headers", "*")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set( "Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS" )

	switch r.Method {
	case "GET":
		err = handleGet(w, r)
	case "POST":
		err = handlePost(w, r)
	case "PUT":
		err = handlePut(w, r)
	case "DELETE":
		err = handleDelete(w, r)
	}

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

GET

指定したIDの投稿を取得する場合、http://localhost:8080/posts/{id}にGETでリクエストすることで、handleGet()が実行されます。

path.Base()でアクセスしたパスからidを取得します。(/posts/1なら1を取得)また、strconv.Atoi()を利用して取得したidを文字列から整数に変換しています。

次にmodel.goで定義したretrieve()にidを指定し、投稿の情報を取得します。

取得したPost型のデータをjson.MarshalIndent()でjsonに変換します。

最後にw.Write(output)で結果を返しています。

func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path))
	if err != nil {
		return
	}

	post, err := retrieve(id)
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

POST

新規に投稿を作成する場合、http://localhost:8080/posts/にPOSTでリクエストします。また、リクエストボディにjsonデータを渡します。

{
    "title": "GoでAPIを開発する",
    "body": "テスト",
    "author": "Minato"
}

r.ContentLengthでリクエストのContent-Lengthを取得します。

取得した長さのbyte型スライスを作成しておきます。リクエストのbodyを格納する

リクエストのbodyを格納するために取得した長さのbyte型スライスcontentBodyを作成しておきます。

json.Unmarshal(contentBody, &post)でリクエストされたjsonをPost型の変数postにマッピングします。

作成したpostのcreate()を実行して、DBに投稿のデータを登録します。

登録が成功したら、postを再度jsonに変換してレスポンスとして返します。

func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
	contentLength := r.ContentLength
	contentBody := make([]byte, contentLength)
	r.Body.Read(contentBody)

	var post Post
	err = json.Unmarshal(contentBody, &post)
	if err != nil {
		return
	}

	err = post.create()
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

PUT

投稿を更新する場合、http://localhost:8080/posts/{id}にPUTでリクエストします。また、作成時同様、リクエストボディにjsonデータを渡します。

こちらは、handleGet()とhandlePostを組み合わせたような処理になります。

まずリクエストされたパスからidを取得し、retrieve(id)で投稿データを取得します。

リクエストされたjsonを取得して、postにデータをマッピングします。

続いて、postのupdate()を利用してDBを更新します。

更新が完了したらpostをjsonに変換してレスポンスを返します。

func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path))
	if err != nil {
		return
	}

	post, err := retrieve(id)
	if err != nil {
		return
	}

	contentLength := r.ContentLength
	contentBody := make([]byte, contentLength)
	r.Body.Read(contentBody)

	err = json.Unmarshal(contentBody, &post)
	if err != nil {
		return
	}

	err = post.update()
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

DELETE

投稿を削除する場合、http://localhost:8080/posts/{id}にDELETEでリクエストします。

retrieve(id)で対象の投稿を取得し、delete()でDBからデータを削除します。

削除が完了したら、postをjsonに変換し、レスポンスを返します。

func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path))
	if err != nil {
		return
	}

	post, err := retrieve(id)
	if err != nil {
		return
	}

	err = post.delete()
	if err != nil {
		return
	}

	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}

	w.WriteHeader(200)
	w.Write(output)
	return
}

以上でAPIの実装は完了です。

ビルドと実行

ターミナルで以下のコマンドを実行してAPIを起動してください。

# ビルド
go build
# 実行
./コンパイル後のファイル名

PostmanなどのAPIクライアントを利用して、APIへリクエストしてみてください。データの作成、読み込み、更新、削除ができるはずです。

まとめ

次回はReactを利用してブログアプリのGUIを作成していきます。本記事と併せて是非ご確認ください。

Golang_react

GoでREST APIを開発する方法②~ブログアプリ編~(Go + React )

2021年11月2日