ぺい

渋谷系アドテクエンジニアの落書き

GolangのgoaでAPIをデザインしよう(エンドポイント編)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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&reg=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&reg=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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください

goでAPIを作成する場合は、必ずといっていいくらいgoaでやっている私ですが、日本ではあまり使っている人が居ません。恐らくこの理由は日本語の情報があまり無いことやシンプルなサンプル集がないからかもしれない・・・と思いまして、軽量なサンプルを定期的に紹介していくことにしました。

API設計フェーズ

goaは最初に設計を行ってから、その設計書を元に実装を行っていきます。設計はDSLと呼ばれるもので、最初はこの定義に慣れないですが、読み方さえ分かれば容易にAPIを設計出来ます。

何が嬉しいの?

例えば、GET /users/:IDというエンドポイントがあるとします。どのようなロジックを組む必要があるでしょう?

curl http://localhost/users/1でリクエストする

  1. /users/1の1の部分を取得する
  2. :IDから取得したものが文字なのか数字なのかチェックする
  3. 取得した:IDを使って、データベースなりにSELECT句などで検索する
  4. 該当するものがなかったら、404ステータスを返す
  5. 正しく取得できたら、200ステータスとレコードを返す。

goaだとどうなるか?

  1. /users/1の1の部分を取得する
  2. :IDから取得したものが文字なのか数字なのかチェックする
  3. 取得した:IDを使って、データベースなりにSELECT句などで検索する
  4. 該当するものがなかったら、404ステータスを返す
  5. 正しく取得できたら、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

f:id:tikasan0804:20170505214106p:plain

初めの方に説明した。ビズネスロジックだけを書けば実装が完了していることが実感出来たと思います。他にもヘッダーのチェックや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アプリケーションでのフロー

f:id:tikasan0804:20170502231453p:plain 大体こんな感じだと思う。

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

ユーザーがリクエストを受け入れると、Githubcodeパラメーターの一時コードと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分で終わらせる方法を紹介します。

はよソース

github.com

便利なツール達

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を生成する。

f:id:tikasan0804:20170426103310p:plain 右に出てる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最高っすね。