Help us understand the problem. What is going on with this article?

golangでREST APIをやってみた①

はじめに

この記事は JSL (日本システム技研) Advent Calendar 2018 - Qiita 20日目の記事です。

昨年同様、一人アニバーサリー記念記事です。色々ありましたが、なんとか本厄も乗り切れそうです!

golangでREST APIをやってみた話を書きます。最近ちらほらPythonからgolangへ移行した
話を聞くので、お試し的なノリでやってみたいと思います。

今回は、「ORMで取得したデータをGETメソッドで返すところまで」を目標にします。
弊社のプロダクトでDjango restframeworkを使用してバックエンドを実装しているものがいくつかあるので、今後は、そちらのAPIとのパフォーマンス比較をしたりしてみたいです。

※golangデビューなので、手探り感満載なので、お気づきの点あればフォローもらえるとありがたいです!!

環境周り

まずは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の実装のみで終わってしまいましたが、引き続き、その他のメソッドについても実装したいです。また、ページネーションが非対応のためこの辺りも次回は、触れてみたいと思います。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away