はじめに
この記事は JSL (日本システム技研) Advent Calendar 2018 - Qiita 20日目の記事です。
昨年同様、一人アニバーサリー記念記事です。色々ありましたが、なんとか本厄も乗り切れそうです!
golangでREST APIをやってみた話を書きます。最近ちらほらPythonからgolangへ移行した
話を聞くので、お試し的なノリでやってみたいと思います。
今回は、「ORMで取得したデータをGETメソッドで返すところまで」を目標にします。
弊社のプロダクトでDjango restframeworkを使用してバックエンドを実装しているものがいくつかあるので、今後は、そちらのAPIとのパフォーマンス比較をしたりしてみたいです。
※golangデビューなので、手探り感満載なので、お気づきの点あればフォローもらえるとありがたいです!!
環境周り
- macOS 10.2.6
- go 1.11.2
- dep : パッケージ管理
- go-json-rest : REST API(https://github.com/ant0ine/go-json-rest)
- gorm : ORM(https://github.com/jinzhu/gorm)
まずはHello world!
まずは、go-json-restの公式にある「Hello world!」を試してみます。
package main
import (
"github.com/ant0ine/go-json-rest/rest"
"log"
"net/http"
)
func main() {
api := rest.NewApi()
api.Use(rest.DefaultDevStack...)
api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) {
w.WriteJson(map[string]string{"Body": "Hello World!"})
}))
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}
実行して、ブラウザから 「http://127.0.0.1:8080」 を叩いて見ます。
$ go run helloworld.go
"Body": "Hello World!"が返されたことを書くに出来ました。
REST APIを試す
同じく、go-json-restの公式にある「Countries」を試してみます。
サンプルコードは、DBで永続化はせずに、メモリ上にデータを置く感じです。
package main
import (
"github.com/ant0ine/go-json-rest/rest"
"log"
"net/http"
"sync"
)
func main() {
api := rest.NewApi()
api.Use(rest.DefaultDevStack...)
router, err := rest.MakeRouter(
rest.Get("/countries", GetAllCountries),
rest.Post("/countries", PostCountry),
rest.Get("/countries/:code", GetCountry),
rest.Delete("/countries/:code", DeleteCountry),
)
if err != nil {
log.Fatal(err)
}
api.SetApp(router)
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}
type Country struct {
Code string
Name string
}
var store = map[string]*Country{}
var lock = sync.RWMutex{}
func GetCountry(w rest.ResponseWriter, r *rest.Request) {
code := r.PathParam("code")
lock.RLock()
var country *Country
if store[code] != nil {
country = &Country{}
*country = *store[code]
}
lock.RUnlock()
if country == nil {
rest.NotFound(w, r)
return
}
w.WriteJson(country)
}
func GetAllCountries(w rest.ResponseWriter, r *rest.Request) {
lock.RLock()
countries := make([]Country, len(store))
i := 0
for _, country := range store {
countries[i] = *country
i++
}
lock.RUnlock()
w.WriteJson(&countries)
}
func PostCountry(w rest.ResponseWriter, r *rest.Request) {
country := Country{}
err := r.DecodeJsonPayload(&country)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if country.Code == "" {
rest.Error(w, "country code required", 400)
return
}
if country.Name == "" {
rest.Error(w, "country name required", 400)
return
}
lock.Lock()
store[country.Code] = &country
lock.Unlock()
w.WriteJson(&country)
}
func DeleteCountry(w rest.ResponseWriter, r *rest.Request) {
code := r.PathParam("code")
lock.Lock()
delete(store, code)
lock.Unlock()
w.WriteHeader(http.StatusOK)
}
curlコマンドで以下のような感じで叩きます。
# POST
$ curl -i -H 'Content-Type: application/json' \
-d '{"Code":"JP","Name":"Japan"}' http://127.0.0.1:8080/countries
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: Tue, 18 Dec 2018 05:52:20 GMT
Content-Length: 37
{
"Code": "JP",
"Name": "Japan"
}
# GET
$ curl -i http://127.0.0.1:8080/countries/JP
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: Tue, 18 Dec 2018 05:53:45 GMT
Content-Length: 37
{
"Code": "JP",
"Name": "Japan"
}
$ curl -i http://127.0.0.1:8080/countries
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: Tue, 18 Dec 2018 05:56:39 GMT
Content-Length: 104
[
{
"Code": "JP",
"Name": "Japan"
},
{
"Code": "US",
"Name": "United States"
}
]⏎ ⏎
ORMを試す
なんとなくRESTぽいことが出来ることが分かったの、実際に既存のMySQLよりでORMでデータ取得してみます。まずはお試しなので、Django標準で作成されるテーブル
auth_user より全件取得してみました。
注意点として、GORM は、デフォルトだとModel名に対して、「s」を付与したテーブル名を探しにいくため(例:UserであればUsersを探しにいく)、今回のように既存のテーブルを読む場合は、別途 **TableName()**というメソッドを用意してあげる必要があります。
package main
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"fmt"
)
type User struct {
ID int64 `gorm:"primary_key"`
Username string
}
func (s *User) TableName() string {
return "auth_user"
}
func gormConnect() *gorm.DB {
DBMS := "mysql"
USER := "user"
PASS := "password"
PROTOCOL := "tcp(127.0.0.1:3306)"
DBNAME := "dbname"
CONNECT := USER+":"+PASS+"@"+PROTOCOL+"/"+DBNAME
db,err := gorm.Open(DBMS, CONNECT)
if err != nil {
panic(err.Error())
}
return db
}
func main() {
db := gormConnect()
// 全件取得
var allUsers []User
db.Find(&allUsers)
fmt.Println(allUsers)
defer db.Close()
}
実行すると以下のようなフォーマットで取得することが出来ました。
$ go run gorm.go
[{1 admin} {2 katekichi} {3 nakazawa} {4 hogefuga} {6 fugahoge}]
ORMで取得したデータをGETで返す
ORMから取得する箇所は、先ほどのものと同じです。
package main
import (
"github.com/ant0ine/go-json-rest/rest"
"log"
"net/http"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"fmt"
)
type User struct {
ID int64 `gorm:"primary_key"`
Username string
}
func (s *User) TableName() string {
return "auth_user"
}
func gormConnect() *gorm.DB {
DBMS := "mysql"
USER := "user"
PASS := "password"
PROTOCOL := "tcp(127.0.0.1:3306)"
DBNAME := "dbname"
CONNECT := USER+":"+PASS+"@"+PROTOCOL+"/"+DBNAME
db,err := gorm.Open(DBMS, CONNECT)
if err != nil {
panic(err.Error())
}
return db
}
func main() {
api := rest.NewApi()
api.Use(rest.DefaultDevStack...)
router, err := rest.MakeRouter(
rest.Get("/users", GetAllUsers),
)
if err != nil {
log.Fatal(err)
}
api.SetApp(router)
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}
func GetAllUsers(w rest.ResponseWriter, r *rest.Request) {
db := gormConnect()
defer db.Close()
// 全件取得
var allUsers []User
db.Find(&allUsers)
fmt.Println(allUsers)
w.WriteHeader(http.StatusOK)
w.WriteJson(&allUsers)
}
curlコマンドで叩きます。
$ curl -i http://127.0.0.1:8080/users
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: Wed, 19 Dec 2018 04:47:35 GMT
Content-Length: 245
[
{
"ID": 1,
"Username": "admin"
},
{
"ID": 2,
"Username": "katekichi"
},
{
"ID": 3,
"Username": "nakazawa"
},
{
"ID": 4,
"Username": "hogefuga"
},
{
"ID": 6,
"Username": "fugahoge"
}
]⏎
まとめ
今回は、時間の都合上、GETの実装のみで終わってしまいましたが、引き続き、その他のメソッドについても実装したいです。また、ページネーションが非対応のためこの辺りも次回は、触れてみたいと思います。