こんにちは、ミナトです。
本記事では、Go言語の標準ライブラリを利用してREST APIを実装する方法を解説します。
バックエンドにGo言語、フロントエンドにReactを利用して、簡易的なブログアプリを開発します。
まずは、Go言語を用いてAPIの実装を行い、次回の記事でフロントエンドの実装を行います。
https://github.com/Minato000/go_blog_api
ライブラリのインストール
今回は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を作成していきます。本記事と併せて是非ご確認ください。