GolangのgoaでAPIをデザインしよう(エンドポイント編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
エンドポイントにも色々ある
同じエンドポイントの指定でも、DSLの書き方で変わります。若干癖があったりするので、今回はそれについてまとめたいと思います。
例:ユーザーのフォロー操作。
同じ名前だけど、HTTPメソッドが違うので、操作の中身が違う。
PUT /users/follow // フォローする DELETE /users/follow // フォローを外す
例:なんかのリスト
/listというリソースに対してエンドポイントで操作を表現している。(途中まで操作同じだけど、若干違う)
GET /list/new // 新しいリスト GET /list/topic // 注目されてるリスト GET /list // リスト(全件)
例:ちょっと特殊ケース
GET /api/v1/method/users/:id/follow/:type
以上のエンドポイントをDSLでどうやって表現するかを紹介します。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
APIをデザインする
順を追って記事を読んでいればすんなりソースも読めると思います。(つまりコメントがなし)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = Resource("method", func() { BasePath("/method") Action("method", func() { Description("HTTPメソッド") Routing( GET("/get"), POST("/post"), DELETE("/delete"), PUT("/put"), ) Response(OK, MessageMedia) Response(BadRequest, ErrorMedia) }) Action("list", func() { Description("リストを返す") Routing( GET("/list"), GET("/list/new"), GET("/list/topic"), ) Response(OK, CollectionOf(UserMedia)) Response(BadRequest, ErrorMedia) }) Action("follow", func() { Description("フォロー操作") Routing( PUT("/users/follow"), DELETE("/users/follow"), ) Response(OK, MessageMedia) Response(BadRequest, ErrorMedia) }) Action("etc", func() { Routing(GET("/users/:id/follow/:type")) Description("ちょっと特殊ケース") Params(func() { Param("id", Integer, "id") Param("type", Integer, "タイプ", func() { Enum(1, 2, 3) }) }) Response(OK, "plain/text") Response(BadRequest, ErrorMedia) }) })
MediaTypeを定義する
以前記事で書いたコードです。
var UserMedia = MediaType("application/vnd.user+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("hoge") }) Attribute("email", String, "メールアドレス", func() { Example("satak47cpc@gmail.com") }) Required("id", "name", "email") }) // 特別な指定がない場合はdefaultのMediaType View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) // tinyという名前の場合は、簡潔なレスポンスフォーマットにすることが出来る View("tiny", func() { Attribute("id") Attribute("name") }) })
コントローラー定義
デザインを元にコードを生成しましょう。コントローラーは、値を返すだけの単純なものを作成します。
$ goagen bootstrap -d github.com/tikasan/goa-stater/design
package controller import ( "fmt" "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // MethodController implements the method resource. type MethodController struct { *goa.Controller } // NewMethodController creates a method controller. func NewMethodController(service *goa.Service) *MethodController { return &MethodController{Controller: service.NewController("MethodController")} } // Etc runs the etc action. func (c *MethodController) Etc(ctx *app.EtcMethodContext) error { // MethodController_Etc: start_implement // Put your logic here // MethodController_Etc: end_implement return ctx.OK([]byte(fmt.Sprintf("ID: %d, Type %d", ctx.ID, ctx.Type))) } // Follow runs the follow action. func (c *MethodController) Follow(ctx *app.FollowMethodContext) error { // MethodController_Follow: start_implement // Put your logic here var message string // 何かのフォロー操作 if "PUT" == ctx.Request.Method { message = "フォローした" } else if "DELETE" == ctx.Request.Method { message = "フォロー外した" } // MethodController_Follow: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) } // List runs the list action. func (c *MethodController) List(ctx *app.ListMethodContext) error { // MethodController_List: start_implement // Put your logic here var listType string switch ctx.RequestURI { //case client.ListMethodPath(): <---これでもいけるけど・・・って感じ 何かいい方法。 case "/api/v1/method/list": listType = "ただの" case "/api/v1/method/list/new": listType = "新しい" case "/api/v1/method/list/topic": listType = "注目されている" } // MethodController_List: end_implement res := make(app.UserTinyCollection, 2) u1 := &app.UserTiny{ ID: 1, Name: listType + "ユーザー1", } u2 := &app.UserTiny{ ID: 2, Name: listType + "ユーザー2", } res[0] = u1 res[1] = u2 return ctx.OKTiny(res) } // Method runs the method action. func (c *MethodController) Method(ctx *app.MethodMethodContext) error { // MethodController_Method: start_implement // Put your logic here message := ctx.RequestURI // MethodController_Method: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) }
実際に動かしてみよう
$ go run main.go
$ curl -v -X PUT 'http://localhost:8080/api/v1/method/users/follow' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > PUT /api/v1/method/users/follow HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.message+json < Date: Sat, 06 May 2017 06:34:09 GMT < Content-Length: 33 < {"message":"フォローした"} * Connection #0 to host localhost left intact
$ curl -v -X DELETE 'http://localhost:8080/api/v1/method/users/follow' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > DELETE /api/v1/method/users/follow HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.message+json < Date: Sat, 06 May 2017 06:35:05 GMT < Content-Length: 36 < {"message":"フォロー外した"} * Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/method/users/1/follow/3' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/method/users/1/follow/3 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: plain/text < Date: Sat, 06 May 2017 06:35:35 GMT < Content-Length: 13 < * Connection #0 to host localhost left intact ID: 1, Type 3%
これで出来上がりです。エンドポイントのURIによって処理を変えるところが少し不細工なんで、誰か良い案あれば教えて欲しいです。
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com
GolangのgoaでAPIをデザインしよう(クライアント編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
クライアント編
バックエンドをいい感じに出来るgoaですけど、欲を言えばクライアント側もいい感じにしてほしい。実はいい感じにしてくれます。とは言ってもちょっとした手助けをしてくれる感じです。
対応しているのは、JavascriptでのAPIへのリクエストです。
$ go get github.com/tikasan/goa-simple-sample または $ ghq get github.com/tikasan/goa-simple-sample
今回はgoaのサンプルを作成しているリポジトリを使って説明したいと思います。 本来は以下のコマンドで実行して、クライアント側のコードを生成しますが、出来上がっているので、生成されたものを見てみましょう。
goagen js -d github.com/tikasan/goa-simple-sample/design
// This module exports functions that give access to the goa simple sample API hosted at localhost:8080. // It uses the axios javascript library for making the actual HTTP requests. define(['axios'] , function (axios) { function merge(obj1, obj2) { var obj3 = {}; for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; } for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; } return obj3; } return function (scheme, host, timeout) { scheme = scheme || 'http'; host = host || 'localhost:8080'; timeout = timeout || 20000; // Client is the object returned by this module. var client = axios; // URL prefix for all API requests. var urlPrefix = scheme + '://' + host; // 複数アクション(:ID) // path is the request path, the format is "/api/v1/actions/:ID" // config is an optional object to be merged into the config built by the function prior to making the request. // The content of the config object is described here: https://github.com/mzabriskie/axios#request-api // This function returns a promise which raises an error if the HTTP response is a 4xx or 5xx. client.IDActions = function (path, config) { cfg = { timeout: timeout, url: urlPrefix + path, method: 'get', responseType: 'json' }; if (config) { cfg = merge(cfg, config); } return client(cfg); } ---------------------------------------------------------------- // Validation // path is the request path, the format is "/api/v1/validation" // ID, defaultType, email, enumType, integerType, reg, stringType are used to build the request query string. // config is an optional object to be merged into the config built by the function prior to making the request. // The content of the config object is described here: https://github.com/mzabriskie/axios#request-api // This function returns a promise which raises an error if the HTTP response is a 4xx or 5xx. client.validationValidation = function (path, ID, defaultType, email, enumType, integerType, reg, stringType, config) { cfg = { timeout: timeout, url: urlPrefix + path, method: 'get', params: { id: id, defaultType: defaultType, email: email, enumType: enumType, integerType: integerType, reg: reg, stringType: stringType }, responseType: 'json' }; if (config) { cfg = merge(cfg, config); } return client(cfg); } return client; }; });
jsでそのまま使えるリクエスト集が出来上がりました。
この機能を知った時は感動しましたが、あまりに楽すぎてダメ人間になりそうと思ったので、今後も使っていこうと思います。(?)
GolangのgoaでAPIをデザインしよう(レスポンス編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
レスポンス
いきなりですが、以下のエンドポイントを見てください。RESTを知ってる人は、レスポンスが違うことが分かると思います。
GET /users/1 GET /users
開発するチームにもよって変わる可能生はあると思いますが、GET /users/1
は恐らくひとりのユーザー情報だけを返す。そして、GET /users
はひとり以上のユーザー情報を返す。
また、GET /users
はIDや名前だけで良かったり、GET /users/1
は詳細な情報が欲しかったり、同じリソースに対しての操作でも、レスポンスは変える必要があります。(と思っています)
具体的にレスポンスの例を示すと以下のような感じになります。
GET /users
[ { "id": 1, "name": "ユーザー1" }, { "ID": 2, "name": "ユーザー2" } ]
GET /users/1
{ "id": 1, "email": "satak47cpc@gmail.com", "name": "ユーザー1" }
また、場合によってはIDの一覧だけ欲しい時もあったり
{"america":3,"japan":2,"korea":4}
キーに意味を持たせた連想配列が欲しかったり
[1,2,3]
上記のような需要全てに対応することが出来ます。実際に動かしてみましょう。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
APIデザインをする
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = Resource("response", func() { BasePath("/response") Action("list", func() { Description("ユーザー(複数)") Routing( GET("/users"), ) // 複数返す Response(OK, CollectionOf(UserMedia)) Response(BadRequest, ErrorMedia) }) Action("show", func() { Description("ユーザー(単数)") Routing( GET("/users/:id"), ) // 単一 Response(OK, UserMedia) Response(BadRequest, ErrorMedia) }) Action("hash", func() { Description("ユーザー(ハッシュ)") Routing( GET("/users/hash"), ) // 連想配列 Response(OK, HashOf(String, Integer)) Response(BadRequest, ErrorMedia) }) Action("array", func() { Description("ユーザー(配列)") Routing( GET("/users/array"), ) // 配列 Response(OK, ArrayOf(Integer)) Response(BadRequest, ErrorMedia) }) })
MediaTypeを定義する
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var UserMedia = MediaType("application/vnd.user+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("hoge") }) Attribute("email", String, "メールアドレス", func() { Example("satak47cpc@gmail.com") }) Required("id", "name", "email") }) // 特別な指定がない場合はdefaultのMediaType View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) // tinyという名前の場合は、簡潔なレスポンスフォーマットにすることが出来る View("tiny", func() { Attribute("id") Attribute("name") }) })
Controllerを定義する
goagen bootstrap -d github.com/tikasan/goa-stater/design
特にビジネスロジックなどはなしで、値を返すだけのコードを実装します。
package controller import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ResponseController implements the response resource. type ResponseController struct { *goa.Controller } // NewResponseController creates a response controller. func NewResponseController(service *goa.Service) *ResponseController { return &ResponseController{Controller: service.NewController("ResponseController")} } // Array runs the array action. func (c *ResponseController) Array(ctx *app.ArrayResponseContext) error { // ResponseController_Array: start_implement // Put your logic here // ResponseController_Array: end_implement res := make([]int, 3) res[0] = 1 res[1] = 2 res[2] = 3 return ctx.OK(res) } // Hash runs the hash action. func (c *ResponseController) Hash(ctx *app.HashResponseContext) error { // ResponseController_Hash: start_implement // Put your logic here // ResponseController_Hash: end_implement res := make(map[string]int, 3) res["japan"] = 2 res["america"] = 3 res["korea"] = 4 return ctx.OK(res) } // List runs the list action. func (c *ResponseController) List(ctx *app.ListResponseContext) error { // ResponseController_List: start_implement // Put your logic here // ResponseController_List: end_implement res := make(app.UserTinyCollection, 2) u1 := &app.UserTiny{ ID: 1, Name: "ユーザー1", } u2 := &app.UserTiny{ ID: 2, Name: "ユーザー2", } res[0] = u1 res[1] = u2 return ctx.OKTiny(res) } // Show runs the show action. func (c *ResponseController) Show(ctx *app.ShowResponseContext) error { // ResponseController_Show: start_implement // Put your logic here // ResponseController_Show: end_implement res := &app.User{} res.ID = 1 res.Name = "ユーザー1" res.Email = "satak47cpc@gmail.com" return ctx.OK(res) }
準備が整ったので、実行してリクエストを投げてみましょう!
$ go run main.go
$ curl -v 'http://localhost:8080/api/v1/response/users/1' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/response/users/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.user+json < Date: Sat, 06 May 2017 04:33:08 GMT < Content-Length: 63 < {"ID":1,"email":"satak47cpc@gmail.com","name":"ユーザー1"} * Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/response/users' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/response/users HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.user+json; type=collection < Date: Sat, 06 May 2017 04:35:02 GMT < Content-Length: 66 < [{"ID":1,"name":"ユーザー1"},{"ID":2,"name":"ユーザー2"}] * Connection #0 to host localhost left intact
いい感じのデータが返ってきました。goaはいいぞ!
GolangのgoaでAPIをデザインしよう(バリデーション編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
わかっちゃいるけど、さぼりたいバリデーション
APIは単純なJSONやText返したり、用途は色々あると思います。ですけど、実際返す処理の前にクライアントから飛んでくるリクエストが間違っていないかバリデーションが必要になりますよね。これがだるい!
しかも、プログラム側で実装するだけでも大変なのに、それをドキュメントにどういう条件でOKとするかとか書き出すと、結構細かく記述する必要があり、正直面倒でしかないと思います。そして、仕様変更があれば楽しい修正が始まります。無駄無駄無駄無駄無駄無駄無駄!!!!!!
goaでやったら秒で終わるよ
そこで、今回はgoaでバリデーションすると、幸せになれる一例を示したいと思います。サンプルコードと説明を貼っていくので、この楽さを実感してほしいです。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
デザイン定義
以下のような感じで、用意されているメソッドを使えば大体なんとかなります。
ちなみにFormay関数には、email以外にもいくつかあります。
- “date-time”: RFC3339 date time
- “email”: RFC5322 email address
- “hostname”: RFC1035 internet host name
- “ipv4”, “ipv6”, “ip”: RFC2373 IPv4, IPv6 address or either
- “uri”: RFC3986 URI
- “mac”: IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address
- “cidr”: RFC4632 or RFC4291 CIDR notation IP address
- “regexp”: RE2 regular expression
var _ = Resource("validation", func() { BasePath("/validation") Action("validation", func() { Description("Validation") Routing( GET("/"), ) Params(func() { // Integer型 Param("id", Integer, "id", func() { Example(1) }) // Integer型かつ1〜10以下 Param("integerType", Integer, "数字(1〜10)", func() { Minimum(0) Maximum(10) Example(2) }) // String型かつ1〜10文字以下 Param("stringType", String, "文字(1~10文字)", func() { MinLength(1) MaxLength(10) Example("あいうえお") }) // String型かつemailフォーマット Param("email", String, "メールアドレス", func() { Format("email") Example("example@gmail.com") }) // String型でEnumで指定されたいずれかの文字列 Param("enumType", String, "列挙型", func() { Enum("A", "B", "C") Example("A") }) // String型で何も指定が無ければ”でふぉ”という文字列が自動でセットされる Param("defaultType", String, "デフォルト値", func() { Default("でふぉ") Example("でふぉ") }) // String型で正規表現で指定したパターンの文字列 Param("reg", String, "正規表現", func() { Pattern("^[a-z0-9]{5}$") Example("12abc") }) // 全て必須パラメーター Required("id", "integerType", "stringType", "email", "enumType", "defaultType", "reg") }) Response(OK, ValidationMedia) Response(BadRequest, ErrorMedia) }) })
MediaTypeの定義
とりあえず、返すだけ。
var ValidationMedia = MediaType("application/vnd.validation+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("integerType", Integer, "数字(1〜10)", func() { Example(5) }) Attribute("stringType", String, "文字(1~10文字)", func() { Example("あいうえお") }) Attribute("email", String, "メールアドレス", func() { Example("example@gmail.com") }) Attribute("enumType", String, "列挙型", func() { Example("A") }) Attribute("defaultType", String, "デフォルト値", func() { Example("でふぉ") }) Attribute("reg", String, "デフォルト値", func() { Example("12abc") }) }) Required("id", "integerType", "stringType", "email", "enumType", "defaultType", "reg") View("default", func() { Attribute("id") Attribute("integerType") Attribute("stringType") Attribute("email") Attribute("enumType") Attribute("defaultType") Attribute("reg") }) })
コントローラー定義
コントローラーは、バリデーションが完了した値が来た時だけを想定した処理をすればおkです。デザイン定義が完了したら、デザインを元にコードを生成しましょう。
$ goagen bootstrap -d github.com/tikasan/goa-stater/design
package main import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ValidationController implements the validation resource. type ValidationController struct { *goa.Controller } // NewValidationController creates a validation controller. func NewValidationController(service *goa.Service) *ValidationController { return &ValidationController{Controller: service.NewController("ValidationController")} } // Validation runs the validation action. func (c *ValidationController) Validation(ctx *app.ValidationValidationContext) error { // ValidationController_Validation: start_implement // Put your logic here // ValidationController_Validation: end_implement res := &app.Validation{} res.ID = ctx.ID res.IntegerType = ctx.IntegerType res.StringType = ctx.StringType res.Email = ctx.Email res.EnumType = ctx.EnumType res.DefaultType = ctx.DefaultType res.Reg = ctx.Reg return ctx.OK(res) }
実際に動かしてみよう
$ go run main.go
$ curl -v 'http://localhost:8080/api/v1/validation?id=1&defaultType=&email=satak%40gmail.com&enumType=A&integerType=10&stringType=foo®=12abc' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/validation?id=1&defaultType=&email=satak%40gmail.com&enumType=A&integerType=10&stringType=foo®=12abc HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.validation+json < Date: Sat, 06 May 2017 01:47:26 GMT < Content-Length: 117 < {"id":1,"defaultType":"","email":"satak@gmail.com","enumType":"A","integerType":10,"reg":"12abc","stringType":"foo"} * Connection #0 to host localhost left intact
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com
GolangのgoaでAPIをデザインしよう(基本編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
goでAPIを作成する場合は、必ずといっていいくらいgoaでやっている私ですが、日本ではあまり使っている人が居ません。恐らくこの理由は日本語の情報があまり無いことやシンプルなサンプル集がないからかもしれない・・・と思いまして、軽量なサンプルを定期的に紹介していくことにしました。
API設計フェーズ
goaは最初に設計を行ってから、その設計書を元に実装を行っていきます。設計はDSLと呼ばれるもので、最初はこの定義に慣れないですが、読み方さえ分かれば容易にAPIを設計出来ます。
何が嬉しいの?
例えば、GET /users/:ID
というエンドポイントがあるとします。どのようなロジックを組む必要があるでしょう?
例 curl http://localhost/users/1
でリクエストする
/users/1
の1の部分を取得する- :IDから取得したものが文字なのか数字なのかチェックする
- 取得した:IDを使って、データベースなりにSELECT句などで検索する
- 該当するものがなかったら、404ステータスを返す
- 正しく取得できたら、200ステータスとレコードを返す。
goaだとどうなるか?
/users/1
の1の部分を取得する:IDから取得したものが文字なのか数字なのかチェックする- 取得した:IDを使って、データベースなりにSELECT句などで検索する
- 該当するものがなかったら、404ステータスを返す
- 正しく取得できたら、200ステータスとレコードを返す。
1,2に当たる値チェックなどが一切必要なくなります。上記のように少ないパラメータならそこまでの労力ではありませんが、正規表現が必要だったり、複数だったりすると・・・面倒ですよね?
つまり、goaはビズネスロジックオンリーの開発が出来るようになるので、人が組むコードの部分が非常にシンプルになります。
しかも、swaggerのドキュメントが自動生成されるので、ドキュメント更新地獄から解放されます。
フォルダ構成
以下のようなファイル構成を構築してください。
※designフォルダ内のファイルは実際は一つでもokですが、分ける方が私は好きです。
. ├── LICENSE ├── Makefile ├── README.md ├── design │ ├── api_definition.go │ ├── media_types.go │ └── resources.go └── public └── swagger
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
go get github.com/tikasan/goa-stater または ghq get github.com/tikasan/goa-stater
デザインする
準備が整ったので、さっそくデザインをしていきたいと思います。
今回はとりあえず動かそうということで、サンプルソースをひたすら貼ります。
細かい説明は順を追って記事を作成して、紹介致します。
APIの基本情報(api_definition.go)
APIの実際の動作ではなく、どういったAPIか?やグローバルな設定などを行います。ここに紹介しているもの以外にも関数は存在します。
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = API("goa simple sample", func() { // APIのタイトル Title("tikasan/goa-simple-sample") // APIの説明 Description("goaのサンプルです") // 作成者へのコンタクト情報 Contact(func() { Name("pei") Email("satak47cpc@gmail.com") URL("https://github.com/tikasan/goa-simple-sample/issues") }) // APIのライセンス License(func() { Name("MIT") URL("https://github.com/tikasan/eventory/blob/master/LICENSE") }) // APIのドキュメント Docs(func() { Description("wiki") URL("https://github.com/tikasan/goa-simple-sample/wiki") }) // ホストの設定 Host("localhost:8080") // 対応しているプロトコル定義、httpかhttpsまたはその両方 Scheme("http", "https") // 全てのエンドポイントのベースパス // /usersというエンドポイントがあったら、/api/v1/usersとなる BasePath("/api/v1") // CORSポリシーの定義 Origin("http://localhost:8080/swagger", func() { //クライアントに公開された1つ以上のヘッダー Expose("X-Time") // 1つまたは複数の許可されたHTTPメソッド Methods("GET", "POST", "PUT", "DELETE") //プリフライト要求応答をキャッシュする時間 MaxAge(600) // Access-Control-Allow-Credentialsヘッダーを設定する Credentials() }) })
MediaType(media_types.go)
レスポンスデータの形式を定義します。(詳しいことは別記事を紹介予定)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) // レスポンスデータの定義 // MediaTypeに名前をつけます var IntegerMedia = MediaType("application/vnd.integer+json", func() { // 説明 Description("example") // どのような値があるか(複数定義出来る) Attributes(func() { // idはInteger型 Attribute("id", Integer, "id", func() { // 返すレスポンスの例 Example(1) }) // レスポンスに必須な要素(基本は全て必須にした方が楽) Required("id") }) // 返すレスポンスのフォーマット(別記事で紹介予定) View("default", func() { Attribute("id") }) })
リソースへの操作(resources.go)
リソースへの操作を定義します。(詳しいことは別記事を紹介予定)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) // /actionsの定義をする var _ = Resource("actions", func() { // actionsリソースのベースパス BasePath("/actions") /* リソースに対してどういった操作を行うか定義する add リソースを追加する list リソースをリストで取得する delete リソースを削除する 上記のような感じで定義すればおkです。 */ Action("ping", func() { // アクションの説明 Description("サーバーとの導通確認") Routing( // エンドポイント -> GET http://localhost/api/v1/actions/pingになる GET("/ping"), ) // 返したいレスポンス // 200 OK + MessageTypeで定義しているMediaType Response(OK, MessageType) // 400 BadRequest + ErrorMediaというデフォルトで容易されているMediaType // 足りないパラメーターなどがあれば自動的に返される Response(BadRequest, ErrorMedia) }) Action("hello", func() { Description("挨拶する") Routing( GET("/hello"), ) // リクエストで付加出来るパラメーター Params(func() { // nameという名前でパラメーターをStringでなげれる Param("name", String, "名前", func() { // もし、空だった場合は空文字を格納する Default("") }) // 必ず設定されるべきパラメーター(デフォルト値があるので、存在しなければ空になる) Required("name") }) Response(OK, MessageType) Response(BadRequest, ErrorMedia) }) Action("ID", func() { Description("複数アクション(:id)") Routing( // エンドポイントにリソースを指定出来る // GET http://localhost:8080/api/v1/actions/1になる GET("/:id"), ) Params(func() { // :IDはIntegert型でなければならない。 Param("id", Integer, "id") // Requiredはリソースを含めたエンドポイントになるので、定義しなくても良い //Required("id") }) Response(OK, IntegerType) // 指定したリソースが無ければNotFoundを返す可能生がある Response(NotFound) Response(BadRequest, ErrorMedia) }) }) // Swaggerをローカルで実行するめの定義 var _ = Resource("swagger", func() { Origin("*", func() { Methods("GET") }) Files("/swagger.json", "swagger/swagger.json") Files("/swagger/*filepath", "public/swagger/") })
コードを生成する
インストール方法やコマンドの説明はikawahaさんの記事がわかりやすいので、紹介しておきます。
ikawaha.hateblo.jp
今回はgithub.com/tikasan/goa-stater
のrepoをgo getした想定で説明します。
goagen bootstrap -d github.com/tikasan/goa-stater/design
コマンドを実行すると以下のようなログが流れます。 実行出来ない場合は、designパッケージ(フォルダ)の指定が間違っています。
app/contexts.go app/controllers.go ..... ..... swagger swagger/swagger.json swagger/swagger.yaml
出来上がる構成は以下のような感じです。
実際に作業をするファイルは一部です。
app,client,tool,swagger周りは自動で生成されたもので、変更することはないです。
. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- actionsリソース ├── app ├── client ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go <------------ Middlewareとかそこらへんやる ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go └── tool
このままだと少しごちゃごちゃしているので、私はcontrollerというパッケージにまとめたりしています。
. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- controllerフォルダへ移動させる ├── app ├── client ├── controller ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go <------------ controllerフォルダへ移動させる └── tool
main.goを若干記述を変える必要があるので、調整だけしてください。
//go:generate goagen bootstrap -d github.com/tikasan/goa-stater/design package main import ( "github.com/goadesign/goa" "github.com/goadesign/goa/middleware" "github.com/tikasan/goa-stater/app" "github.com/tikasan/goa-stater/controller" <-------importする ) func main() { // Create service service := goa.New("goa simple sample") // Mount middleware service.Use(middleware.RequestID()) service.Use(middleware.LogRequest(true)) service.Use(middleware.ErrorHandler(service, true)) service.Use(middleware.Recover()) // Mount "actions" controller c := controller.NewActionsController(service) <-----controllerパッケージからメソッドを呼ぶ app.MountActionsController(service, c) // Start service if err := service.ListenAndServe(":8080"); err != nil { service.LogError("startup", "err", err) } }
これで準備完了しました。ここまでの作業は正直テンプレートなので、慣れればほとんど時間がかかりません。
ビジネスロジックを組んでみよう
package controller import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ActionsController implements the actions resource. type ActionsController struct { *goa.Controller } // NewActionsController creates a actions controller. func NewActionsController(service *goa.Service) *ActionsController { return &ActionsController{Controller: service.NewController("ActionsController")} } // Action ID func (c *ActionsController) ID(ctx *app.IDActionsContext) error { /* Paramsで定義したものはctx.hogeで受け取れる Params(func() { Param("ID", Integer, "ID") }) */ if ctx.ID == 0 { // Response(NotFound) // 0の場合は、404エラーを返す return ctx.NotFound() } // Response(OK, IntegerType) // MediaType IntegerTypeでレスポンス res := &app.Integer{} /* IDはInteger型で Attribute("ID", Integer, "ID", func() { Example(1) }) */ res.ID = ctx.ID return ctx.OK(res) } // Hello runs the hello action. func (c *ActionsController) Hello(ctx *app.HelloActionsContext) error { // ActionsController_Ping: start_implement // Put your logic here name := ctx.Name // ActionsController_Ping: end_implement res := &app.Message{} res.Message = "Hello " + name return ctx.OK(res) } // Ping runs the ping action. func (c *ActionsController) Ping(ctx *app.PingActionsContext) error { // ActionsController_Ping: start_implement // Put your logic here message := "pong" // ActionsController_Ping: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) }
以上でAPIのビジネスロジックの実装完了です。簡単でしょう?
では、実行してみましょう。
go run main.go
エラーが起きた場合は、importミスやポートが空いてないや何かしらのミスが考えられます。
curl -v http://localhost:8080/api/v1/actions/1 * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.integer+json < Date: Fri, 05 May 2017 11:49:59 GMT < Content-Length: 9 < {"id":1} * Connection #0 to host localhost left intact
上記のような感じでレスポンスが返ってきました。
試しに間違ったリクエストを投げてみましょう。
curl -v http://localhost:8080/api/v1/actions/hoge * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/hoge HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 400 Bad Request < Content-Type: application/vnd.goa.error < Date: Fri, 05 May 2017 12:01:54 GMT < Content-Length: 188 < {"id":"Iwzpk08B","code":"invalid_request","status":400,"detail":"invalid value \"hoge\" for parameter \"id\", must be a integer","meta":{"expected":"integer","param":"ID","value":"hoge"}} * Connection #0 to host localhost left intact
エラーのフォーマットはカスタム出来ますが、いまはデフォルトで定義されているフォーマットで返ってきます。特にこだわりがなければこれで完了します。
SwaggerUIを使ってみよう
swaggerのドキュメントも出来上がっているので、github.com/tikasan/goa-stater
を使っている場合は、以下のコマンドでAPIのドキュメントの参照とテストが出来ます!!!
open http://localhost:8080/swagger/index.html
初めの方に説明した。ビズネスロジックだけを書けば実装が完了していることが実感出来たと思います。他にもヘッダーのチェックやmodel定義が簡単に出来るgormaなど色々あるので、どんどん紹介していきたいと思います。
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com
GithubのOAuth2.0の仕様について理解する(Githubログイン)
あの楽ちんなGithubログインボタン
結構前からですが、他のSNSサービスや比較的数多くのアカウント登録数を持っているサービスが採用しているOAuth2.0ですけど、自分が関わっているプロダクトで使う必要が出てきたので、仕様と使い方にGithubの公式ドキュメントを読みながらまとめてみた。
OAuth | GitHub Developer Guide
公式ドキュメントの大体合ってる日本語訳
※実際の翻訳と異なることが多少あります。大体そんな感じという日本語訳です。
OAuth2は外部アプリケーションがユーザーのGithubアカウントが持っているプライベートな情報にパスワードなしで情報にアクセスすることが出来るプロトコルです。トークンは特定のタイプのデータに限定でき、ユーザーはそのトークンをいつでも消せるということらしい。このトークン仕組みははBasic認証よりも良いですよ?みらいなことが書いてある。
つまり、トークンとやらを発行して、そのトークンを使うことで許可された範囲で操作出来るよってことらしい。Basic認証よりイケてるからみんな使ってこうぜ!的なことが言いたいのだと思う。
全ての開発者は、これを利用する前にアプリケーションの登録をする必要があります。登録されたOAuthアプリケーションのには、固有のClient IDとClient Secretが割り当てられます。
※このClient Secretは絶対に共有しないでください!
開発者の自身で使用するための個人アクセストークンを作成したり、以下に紹介するフローを実装することで、他のユーザーがアプリケーションを許可することが出来ます。
ここで出たClient Secretは間違って、Githubとかに上げると悪用されるので、公開しないようにしてくださいという意味だと思われる。
GithubのOAuth実装は、authorization code grant typeをサポートしています。開発者は以下で説明するWebアプリケーションフローを実装して、認証コードを取得し、それをトークンに交換する必要があります。(implicit grant typeはサポートしていません)
Webアプリケーションでのフロー
大体こんな感じだと思う。
1.ユーザーはリダイレクトでGithubへアクセスする
GET https://github.com/login/oauth/authorize
設定できるパラメーター
*は必須という意味です。
Name | Type | 説明 |
---|---|---|
client_id * | string | 登録時にい発行されたクライアントID |
redirect_uri | string | 承認後にユーザーのリダイレクト先。(アプリケーションURL) |
scope | string | scopeスペースで区切られたスコープのリスト。scopeは空の場合は、アプリケーションのscopeを許可していないユーザーの空リストにデフォルトで設定されます。アプリケーションのscopeを承認したユーザーの場合は、scopeのリストを含むOAuth認証ページは表示されません。その代わり、このフローでは、ユーザーのアプリケーションに対しての承認を自動的に処理する。 |
state | string | サイト間のやり取りをする際のCSRF対策に使うランダムな文字列。これが漏れると攻撃されるので、取扱には気をつけたい。 |
allow_signup | string | 認証されていないユーザーに、OAuthフロー中にGithubにサインアップするオプションを提供するかどうかを設定する。つまり、認証フローに入る前からGithubにログイン済みのユーザーしか許可しないかどうかを決定するようです。もし、許可する場合はデフォルトで設定されているtrue で、許可したくない場合はfalse とするようです。 |
補足
redirect_uri
リダイレクトのURLには以下のようなルールがあります。
OAuth | GitHub Developer Guide
CALLBACK: http://example.com/path GOOD: http://example.com/path GOOD: http://example.com/path/subdir/other BAD: http://example.com/bar BAD: http://example.com/ BAD: http://example.com:8080/path BAD: http://oauth.example.com:8080/path BAD: http://example.org
示されている例が少し分かりにくかったので、ありそうな例を書きました。
http://localhost/login ◯http://localhost/login/callback ×http://localhost/callback
scope
OAuth | GitHub Developer Guide
このscopeというのは、アプリケーションから取得または操作を許可する範囲のことを指している。
例えば、何も設定をしないno scope
の場合は、公開されている情報に読み取り専用でアクセスを許可されたり、user:email
などにすると、ユーザーがGithubに登録しているメールアドレスへアクセスが出来るなど。様々なレベルのscopeがあるので、必要に応じて設定すると良いらしい。
2.Githubからアプリケーションにリダイレクトする
POST https://github.com/login/oauth/access_token
ユーザーがリクエストを受け入れると、Githubはcode
パラメーターの一時コードと1の手順で指定したstate
をパラメーターに指定したアプリケーションにリダイレクトされます。この時にstateが違った場合は攻撃の可能生が高いため、OAuthのフローを中止するべきです。
設定できるパラメーター
*は必須という意味です。
Name | Type | 説明 |
---|---|---|
client_id* | string | アプリケーション登録時に発行されたclient_id |
client_secret* | string | アプリケーション登録時に発行されたclient_secret |
code* | string | 1の手順の応答で受け取る。tokenとの引き換え券のようなもの。 |
redirect_uri | string | 承認後にユーザーがリダイレクトされるアプリケーションのURL。リダイレクトのルールは先程紹介したものと同じです。 |
POST https://github.com/login/oauth/access_token
へのレスポンス
access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer
Accept ヘッダーに応じて、様々な形式で受け取ることが出来ます。
Accept: application/json {"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"} Accept: application/xml <OAuth> <token_type>bearer</token_type> <scope>repo,gist</scope> <access_token>e72e16c7e42f292c6912e7710c838347ae178b4a</access_token> </OAuth>
3. tokenを使ってAPIへリクエストする
ここまでのフローで受け取ったaccess_tokenでAPIへリクエストが出来ます。
tokenは以下のようなパラメーターで渡すパターン。
GET https://api.github.com/user?access_token=...
HeaderのAuthorizationを使って渡すことも出来ます。
Authorization: token OAUTH-TOKEN
curlコマンドでは以下のようになります。
curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com/user
なんとなく分かった気がする。
GoでAPIから取得したJSONを5分でパースする
微妙に面倒なアレ
GoはAPIの用途で、私は結構使うのですが、そのAPIを構築する上で、外部のAPIを使ってデータを集めたりすることもよくあります。そして避けて通れないのが、JSON解析です。自力でやると地味に面倒です。
今回はその作業は5分で終わらせる方法を紹介します。
はよソース
便利なツール達
https://mholt.github.io/json-to-go/JSON-to-Go: Convert JSON to Go instantly
JSONを食わせると、解析するstructを生成してくれる。JSON Pretty Linter - JSONの整形と構文チェック
JSONを整形してくれる。
Let’s try!!!
目的のJSONを取得する
connpass.com 今回はConnpassのAPIから返ってくるJSONを解析します。
{ "results_returned": 1, "events": [ { "event_url": "https://kikaigakushuu.connpass.com/event/56040/", "event_type": "participation", "owner_nickname": "keiichirou_miyamoto", "series": { "url": "https://kikaigakushuu.connpass.com/", "id": 2589, "title": "人工知能勉強会" }, "updated_at": "2017-04-26T02:47:01+09:00", "lat": "35.696575200000", "started_at": "2017-05-09T20:00:00+09:00", "hash_tag": "geek", "title": "強くなるロボティック・ゲームプレイヤーの作り方勉強会「4章 強化学習 前半」", "event_id": 56040, "lon": "139.771830100000", "waiting": 0, "limit": 30, "owner_id": 10866, "owner_display_name": "keiichirou_miyamoto", "description": "説明", "address": "東京都千代田区神田須田町2丁目19−23 (野村第3ビル4階)", "catch": "#人工知能,#機械学習,#深層学習,#ニューラルネット,#ディープラーニング,#初心者", "accepted": 17, "ended_at": "2017-05-09T22:00:00+09:00", "place": "コワーキングスペース秋葉原 Weeyble(ウィーブル)" } ], "results_start": 1, "results_available": 1744 }
https://connpass.com/api/v1/event/?keyword=python&count=1
リクエストをブラウザで実行します。
JSON Pretty Linter - JSONの整形と構文チェック コピーして、JSON整形します。 整形したJSONをコピーして、goのソースに貼り付けます。
jsonStrという変数に以下のような感じで代入してください。
var jsonStr = `{ "results_returned": 1, "events": [ { "event_url": "https://kikaigakushuu.connpass.com/event/56040/", "event_type": "participation", "owner_nickname": "keiichirou_miyamoto", "series": { "url": "https://kikaigakushuu.connpass.com/", "id": 2589, "title": "人工知能勉強会" }, "updated_at": "2017-04-26T02:47:01+09:00", "lat": "35.696575200000", "started_at": "2017-05-09T20:00:00+09:00", "hash_tag": "geek", "title": "強くなるロボティック・ゲームプレイヤーの作り方勉強会「4章 強化学習 前半」", "event_id": 56040, "lon": "139.771830100000", "waiting": 0, "limit": 30, "owner_id": 10866, "owner_display_name": "keiichirou_miyamoto", "description": "説明", "address": "東京都千代田区神田須田町2丁目19−23 (野村第3ビル4階)", "catch": "#人工知能,#機械学習,#深層学習,#ニューラルネット,#ディープラーニング,#初心者", "accepted": 17, "ended_at": "2017-05-09T22:00:00+09:00", "place": "コワーキングスペース秋葉原 Weeyble(ウィーブル)" } ], "results_start": 1, "results_available": 1744 }`
JSONからstructを作成する
https://mholt.github.io/json-to-go/JSON-to-Go: Convert JSON to Go instantly
jsonを食わせて、strcutを生成する。
右に出てるstrcutをコピーしてgoのソースに貼り付ける
type AutoGenerated struct { ResultsReturned int `json:"results_returned"` Events []struct { EventURL string `json:"event_url"` EventType string `json:"event_type"` OwnerNickname string `json:"owner_nickname"` Series struct { URL string `json:"url"` ID int `json:"id"` Title string `json:"title"` } `json:"series"` UpdatedAt time.Time `json:"updated_at"` Lat string `json:"lat"` StartedAt time.Time `json:"started_at"` HashTag string `json:"hash_tag"` Title string `json:"title"` EventID int `json:"event_id"` Lon string `json:"lon"` Waiting int `json:"waiting"` Limit int `json:"limit"` OwnerID int `json:"owner_id"` OwnerDisplayName string `json:"owner_display_name"` Description string `json:"description"` Address string `json:"address"` Catch string `json:"catch"` Accepted int `json:"accepted"` EndedAt time.Time `json:"ended_at"` Place string `json:"place"` } `json:"events"` ResultsStart int `json:"results_start"` ResultsAvailable int `json:"results_available"` }
UmarshalでJSONをパースする
jsonパッケージのUnmarshalでパースして終了。
jsonBytes := ([]byte)(jsonStr) data := new(AutoGenerated) if err := json.Unmarshal(jsonBytes, data); err != nil { fmt.Println("JSON Unmarshal error:", err) return } fmt.Println(data.Events[0])
$ go run main.go
以上で、完了です!!!簡単すぎる!!!!
全ソース
package main import ( "encoding/json" "fmt" "time" ) type AutoGenerated struct { ResultsReturned int `json:"results_returned"` Events []struct { EventURL string `json:"event_url"` EventType string `json:"event_type"` OwnerNickname string `json:"owner_nickname"` Series struct { URL string `json:"url"` ID int `json:"id"` Title string `json:"title"` } `json:"series"` UpdatedAt time.Time `json:"updated_at"` Lat string `json:"lat"` StartedAt time.Time `json:"started_at"` HashTag string `json:"hash_tag"` Title string `json:"title"` EventID int `json:"event_id"` Lon string `json:"lon"` Waiting int `json:"waiting"` Limit int `json:"limit"` OwnerID int `json:"owner_id"` OwnerDisplayName string `json:"owner_display_name"` Description string `json:"description"` Address string `json:"address"` Catch string `json:"catch"` Accepted int `json:"accepted"` EndedAt time.Time `json:"ended_at"` Place string `json:"place"` } `json:"events"` ResultsStart int `json:"results_start"` ResultsAvailable int `json:"results_available"` } func main() { var jsonStr = `{ "results_returned": 1, "events": [ { "event_url": "https://kikaigakushuu.connpass.com/event/56040/", "event_type": "participation", "owner_nickname": "keiichirou_miyamoto", "series": { "url": "https://kikaigakushuu.connpass.com/", "id": 2589, "title": "人工知能勉強会" }, "updated_at": "2017-04-26T02:47:01+09:00", "lat": "35.696575200000", "started_at": "2017-05-09T20:00:00+09:00", "hash_tag": "geek", "title": "強くなるロボティック・ゲームプレイヤーの作り方勉強会「4章 強化学習 前半」", "event_id": 56040, "lon": "139.771830100000", "waiting": 0, "limit": 30, "owner_id": 10866, "owner_display_name": "keiichirou_miyamoto", "description": "説明", "address": "東京都千代田区神田須田町2丁目19−23 (野村第3ビル4階)", "catch": "#人工知能,#機械学習,#深層学習,#ニューラルネット,#ディープラーニング,#初心者", "accepted": 17, "ended_at": "2017-05-09T22:00:00+09:00", "place": "コワーキングスペース秋葉原 Weeyble(ウィーブル)" } ], "results_start": 1, "results_available": 1744 }` jsonBytes := ([]byte)(jsonStr) data := new(AutoGenerated) if err := json.Unmarshal(jsonBytes, data); err != nil { fmt.Println("JSON Unmarshal error:", err) return } fmt.Println(data.Events[0]) }
実際にAPIにリクエストする
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "time" ) type AutoGenerated struct { ResultsReturned int `json:"results_returned"` Events []struct { EventURL string `json:"event_url"` EventType string `json:"event_type"` OwnerNickname string `json:"owner_nickname"` Series struct { URL string `json:"url"` ID int `json:"id"` Title string `json:"title"` } `json:"series"` UpdatedAt time.Time `json:"updated_at"` Lat string `json:"lat"` StartedAt time.Time `json:"started_at"` HashTag string `json:"hash_tag"` Title string `json:"title"` EventID int `json:"event_id"` Lon string `json:"lon"` Waiting int `json:"waiting"` Limit int `json:"limit"` OwnerID int `json:"owner_id"` OwnerDisplayName string `json:"owner_display_name"` Description string `json:"description"` Address string `json:"address"` Catch string `json:"catch"` Accepted int `json:"accepted"` EndedAt time.Time `json:"ended_at"` Place string `json:"place"` } `json:"events"` ResultsStart int `json:"results_start"` ResultsAvailable int `json:"results_available"` } func main() { url := "https://connpass.com/api/v1/event/?keyword=python&count=1" resp, _ := http.Get(url) defer resp.Body.Close() byteArray, _ := ioutil.ReadAll(resp.Body) jsonBytes := ([]byte)(byteArray) data := new(AutoGenerated) if err := json.Unmarshal(jsonBytes, data); err != nil { fmt.Println("JSON Unmarshal error:", err) return } fmt.Println(data.Events[0]) }
以上で終了です。Go最高っすね。