golang.org/x/tools/go/analysis を使ってコードを自動修正する

この記事は Go3 Advent Calendar 2019 の 3 日目の記事です。

今年 2 月にリリースされた Go 1.12 において、 公式の静的解析ツールである go vetgolang.org/x/tools/go/analysis を使う形に書き直されました

Analyzer

golang.org/x/tools/go/analysisAnalyzer (静的解析モジュール) を作るためのパッケージで、 go vet が行う様々な処理は個別の Analyzer として分離されこのパッケージ配下に格納されています。これらは外部からも参照できるようになっており、サードパーティAnalyzer から参照することもできます。

Diagnostic

Analyzer は、解析して見つかった問題を Diagnostic (診断) として報告します。 Diagnostic には Pos (コードの開始位置) と Message (メッセージ) が含まれます。

type Diagnostic struct {
    Pos      token.Pos
    Message  string
    ...
}

例えば Printf() の書式識別子と引数の型が合っていないと go vet は診断を出します。

$ cat main.go
package main

import "fmt"

func main() {
    fmt.Printf("%s", 1)
}
$ go vet .
./main.go:6:2: Printf format %s has arg 1 of wrong type int

このように問題があったコードの位置 (./main.go:6:2) とメッセージ (Printf format %s has arg 1 of wrong type int) を出すことでユーザーに修正を促す仕組みになっています。

Analyzer は checker というパッケージを通して実行可能ファイルにすることができます。 checker には singlecheckermultichecker 、そして unitchecker の 3 種類があり、 go vetunitchecker を使って作られています。

SuggestedFix

今年の 6 月、この golang.org/x/tools/go/analysisSuggestedFix (修正案) という機能が入りました。これにより、今までのように解析して見つかった問題を報告するだけでなく、その修正案を提示できるようになりました。 SuggestedFix には Message (メッセージ) と複数の TextEdit (テキスト編集) が含まれます。

type SuggestedFix struct {
    Message   string
    TextEdits []TextEdit
}

TextEditPos (コードの開始位置) と End (コードの終了位置) 、そして NewText (新しいテキスト) から構成されます。

type TextEdit struct {
    Pos     token.Pos
    End     token.Pos
    NewText []byte
}

そして SuggestedFixDiagnostic に複数紐づけることができます。つまり 1 つの診断に対して複数の修正案を提示できるということですね。

type Diagnostic struct {
    Pos      token.Pos
    Message  string
    SuggestedFixes []SuggestedFix
    ...
}

SuggestedFix に複数の TextEdit を含めているのは修正案に柔軟性を持たせるためでしょう。例えば「関数に不要な引数が含まれる」という診断に対する修正案には、関数そのものに対する編集だけでなくその呼び出し元に対する編集も含めることができます。

func add(a, b, c int) int {
    //       ^^^ ここを編集する (1 つ目の TextEdit)
    return a + b
}

func main() {
    fmt.Println(add(1, 2, 0))
    //                  ^^^ ここも編集する (2 つ目の TextEdit)
}

SuggestedFix のサンプルとして、引数が 1 つの fmt.Printf()fmt.Print() に修正する Analyzer を作ってみました。

github.com

-fix を付けて実行することで SuggestedFix を適用し、コードを自動修正することができます。

$ cat main.go
package main

import "fmt"

func main() {
    fmt.Printf("Hello, World")
}
$ fmtprintf -fix .
./main.go:6:2: fmt.Printf call which have one argument can be replaced with fmt.Print
$ cat main.go
package main

import "fmt"

func main() {
    fmt.Print("Hello, World")
}

それから今年リリースされた Goa v3 へ移行するため v1 から v3 へのアップグレードツールを作っているのですが、こちらも SuggestedFix を使う Analyzer として実装しています。

github.com

SuggestedFix の適用

SuggestedFix の適用方法は現状 2 通り用意されているようです。

1 つは前述したサンプルの例で出た -fix を使う方法で、 singlecheckermultichecker はこのフラグが渡されると SuggestedFix を自動で適用する仕組みになっています。 go vet に使われている unitchecker はこのフラグに対応しておらず、現状だと SuggestedFix の適用はできないようでした。

もう 1 つは gopls を使う方法です。 SuggestedFix は既に gopls に統合されているようですが、私が使っている vim-go はまだ該当のメソッド (textDocument/codeAction) に対応していないようで今回は試せませんでした。

gopls 自体もまだ開発途中のようですが、近い将来に Analyzer が提示する修正案を様々なエディタから適用できるようになるかもしれません。

参考

Goa v1 と v3 の DSL の比較

前回の記事ではプリミティブ型の比較を行いましたが、今回は DSL の比較を行っていきます。各 DSL の詳細については随時追記していきます。

v1 v2
API API
APIKeySecurity APIKeySecurity
(APIKey)
(APIKeyField)
AccessCodeFlow (廃止) -
Action Method
ApplicationFlow (廃止) -
ArrayOf ArrayOf
Attribute Attribute
Attributes Attributes
BasePath Path
BasicAuthSecurity BasicAuthSecurity
(Password)
(PasswordField)
(Username)
(UsernameField)
CONNECT CONNECT
CanonicalActionName CanonicalMethod
CollectionOf CollectionOf
Consumes Consumes
Contact Contact
ContentType ContentType
Credentials (goa.design/plugins/cors に移動) -
DELETE DELETE
Default Default
DefaultMedia (廃止) -
Description Description
Docs Docs
Email Email
Enum Enum
Example Example
(Value)
Expose (goa.design/plugins/cors に移動) -
Files Files
Format Format
Function (廃止) -
GET GET
HEAD HEAD
HashOf MapOf
Header Header
Headers Headers
Host Host
Server
URI
Variable
ImplicitFlow ImplicitFlow
JWTSecurity JWTSecurity
(Token)
(TokenField)
License License
Link (廃止) -
Links (廃止) -
MaxAge (goa.design/plugins/cors に移動) -
MaxLength MaxLength
Maximum Maximum
Media Result
MediaType ResultType
Member (廃止) -
Metadata Meta
Methods (goa.design/plugins/cors に移動) -
MinLength MinLength
Minimum Minimum
MultipartForm MultipartRequest
Name Name
NoExample (廃止) -
NoSecurity NoSecurity
OAuth2Security OAuth2Security
(AccessToken)
(AccessTokenField)
OPTIONS OPTIONS
OptionalPayload (廃止) -
Origin (goa.design/plugins/cors に移動) -
PATCH PATCH
POST POST
PUT PUT
Package (廃止) -
Param Param
Params Params
Parent Parent
PasswordFlow PasswordFlow
Pattern Pattern
Payload Payload
Produces Produces
Query (廃止) -
ReadOnly (廃止) -
Reference Reference
Required Required
Resource Service
Response Response
ResponseTemplate (廃止) -
Routing (廃止) -
Scheme (廃止) -
Scope Scope
Security Security
Status Code
TRACE TRACE
TermsOfService TermsOfService
Title Title
TokenURL (廃止) -
Trait (廃止) -
Type Type
TypeName TypeName
URL URL
UseTrait (廃止) -
Version Version
View View
- AuthorizationCodeFlow
- Body
- ClientCredentialsFlow
- ConvertTo
- CreateFrom
- Elem
- Error
- Extend
- Fault
- Field
- GRPC
- HTTP
- Key
- MapParams
- Message
- Metadata
- Services
- StreamingPayload
- StreamingResult
- Tag
- Temporary
- Timeout
- Trailers

BasePath

  • Path に変更されました。
  • HTTP 内に記述します。
// v1
BasePath("/users")
// v2
HTTP(func() {
    Path("/users")
})

Consumes, Produces

  • HTTP 内に記述します。
  • これらの DSL 内で使用できた FunctionPackage は廃止されました。
// v1
Consumes("application/xml")
Consumes("application/json")
Produces("application/xml")
Produces("application/json")
// v2
HTTP(func() {
    Consumes("application/xml")
    Consumes("application/json")
    Produces("application/xml")
    Produces("application/json")
})

Format

  • v1 では引数の型が string でしたが、 v2 では expr.ValidationFormat になりました。
  • 使用可能な expr.ValidationFormatgoa.design/goa/exprconst として定義されています。

GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH

  • v1 ではパスパラメータをコロン (:) で記述しましたが、 v2 では中括弧 ({, }) を使います。
// v1
GET("/users/:id")
// v2
GET("/users/{id}")

HashOf

  • MapOf に変更されました。
  • key のためのオプション DSLKey で記述します。
  • value のためのオプション DSLElem で記述します。
// v1
HashOf(String, Integer,
    func() {
        MinLength(1)
        MaxLength(16)
    },
    func() {
        Minimum(1)
        Maximum(5)
    },
)

// v2
MapOf(String, Int, func() {
    Key(func() {
        MinLength(1)
        MaxLength(16)
    })
    Elem(func () {
        Minimum(1)
        Maximum(5)
    })
})

Headers

リクエストヘッダ

  • Payload 内に Attribute を、 HTTP 内に Headers を記述します。
// v1
Action("show", func() {
    Headers(func() {
        Header("Authorization", String)
    })
})
// v2
Method("show", func() {
    Payload(func() {
        Attribute("Authorization", String)
    })
    HTTP(func() {
        Headers(func() {
            Header("Authorization", String)
        })
    })
})

Host, Scheme

  • v1 では HostScheme (と BasePath) で記述しましたが、 v2 では ServerHostURI を使います。
// v1
Host("localhost:8080")
Scheme("http")
BasePath("/cellar")
// v2
Server("app", func() {
    Host("development", func() {
        URI("http://localhost:8080/cellar")
    })
})

NoExample

  • 廃止されました。
  • Meta で同等の定義ができます。
Meta("swagger:example", "false")

Params

パスパラメータ

  • Payload 内に Attribute を、 HTTP 内に Params を記述します。
  • Params は省略できます。省略した場合は暗黙的に定義されます。
// v1
Action("show", func() {
    Params(func() {
        Param("id", Integer)
    })
})
// v2
Method("show", func() {
    Payload(func() {
        Attribute("id", Int)
    })
    HTTP(func() {
        Params(func() {       // 省略可
            Param("id", Int)  //
        })                    //
    })
})

クエリパラメータ

  • Payload 内に Attribute を、 HTTP 内に Params を記述します。
  • Params は省略できません。
//v1
Action("list", func() {
    Params(func() {
        Param("name", String)
    })
})
// v2
Method("list", func() {
    Payload(func() {
        Attribute("name", String)
    })
    HTTP(func() {
        Params(func() {            // 省略不可
            Param("name", String)  //
        })                         //
    })
})

Routing

  • 廃止されました。
  • HTTP 内に HTTP リクエストメソッドと同名の DSL を記述します。
// v1
Action("show", func() {
    Routing(
        GET("/:id"),
    )
})
// v2
Method("show", func() {
    HTTP(func() {
        GET("/{id}")
    })
})

Goa v1 と v3 のプリミティブ型の比較

Goa v3 では design に使うプリミティブ型が v1 から変更になっています。この記事では Swagger データ型も交えながら v1 と v3 のプリミティブ型を比較してみます。

v1 のプリミティブ型

出典: https://godoc.org/gopkg.in/goadesign/goa.v1/design#pkg-constants

const (
    // Boolean is the type for a JSON boolean.
    Boolean = Primitive(BooleanKind)

    // Integer is the type for a JSON number without a fraction or exponent part.
    Integer = Primitive(IntegerKind)

    // Number is the type for any JSON number, including integers.
    Number = Primitive(NumberKind)

    // String is the type for a JSON string.
    String = Primitive(StringKind)

    // DateTime is the type for a JSON string parsed as a Go time.Time
    // DateTime expects an RFC3339 formatted value.
    DateTime = Primitive(DateTimeKind)

    // UUID is the type for a JSON string parsed as a Go uuid.UUID
    // UUID expects an RFC4122 formatted value.
    UUID = Primitive(UUIDKind)

    // Any is the type for an arbitrary JSON value (interface{} in Go).
    Any = Primitive(AnyKind)

    // File is the type for a file. This type can only be used in a multipart definition.
    File = Primitive(FileKind)
)

v3 のプリミティブ型

出典: https://godoc.org/goa.design/goa/expr#pkg-constants

const (
    // Boolean is the type for a JSON boolean.
    Boolean = Primitive(BooleanKind)

    // Int is the type for a signed integer.
    Int = Primitive(IntKind)

    // Int32 is the type for a signed 32-bit integer.
    Int32 = Primitive(Int32Kind)

    // Int64 is the type for a signed 64-bit integer.
    Int64 = Primitive(Int64Kind)

    // UInt is the type for an unsigned integer.
    UInt = Primitive(UIntKind)

    // UInt32 is the type for an unsigned 32-bit integer.
    UInt32 = Primitive(UInt32Kind)

    // UInt64 is the type for an unsigned 64-bit integer.
    UInt64 = Primitive(UInt64Kind)

    // Float32 is the type for a 32-bit floating number.
    Float32 = Primitive(Float32Kind)

    // Float64 is the type for a 64-bit floating number.
    Float64 = Primitive(Float64Kind)

    // String is the type for a JSON string.
    String = Primitive(StringKind)

    // Bytes is the type for binary data.
    Bytes = Primitive(BytesKind)

    // Any is the type for an arbitrary JSON value (interface{} in Go).
    Any = Primitive(AnyKind)
)

Swagger データ型

出典: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types

Common Name type format Comments
integer integer int32 signed 32 bits
long integer int64 signed 64 bits
float number float
double number double
string string
byte string byte base64 encoded characters
binary string binary any sequence of octets
boolean boolean
date string date As defined by full-date - RFC3339
dateTime string date-time As defined by date-time - RFC3339
password string password Used to hint UIs the input needs to be obscured.

比較

v3 は gRPC 対応のため v1 より多くの型を用意しています。 DateTimeUUID は廃止されましたが Format DSL を使用して同等の定義を記述できますし、より多くのフォーマットも用意されています。

v1 v3 Swagger Common Name Swagger type Swagger format
Boolean Boolean boolean boolean
Integer Int long integer int64
Int32 integer integer int32
Int64 long integer int64
UInt long integer int64
UInt32 integer integer int32
UInt64 long integer int64
Number Float32 float number float
Float64 double number double
String String string string
DateTime String + Format(FormatDateTime) dateTime string date-time
UUID String + Format(FormatUUID) string uuid
String + Format(FormatDate) date string date
String + Format(FormatEmail) string email
String + Format(FormatHostname) string hostname
String + Format(FormatIPv4) string ipv4
String + Format(FormatIPv6) string ipv6
String + Format(FormatIP) string ip
String + Format(FormatURI) string uri
String + Format(FormatMAC) string mac
String + Format(FormatCIDR) string cidr
String + Format(FormatRegexp) string regexp
String + Format(FormatJSON) string json
String + Format(FormatRFC1123) string rfc1123
Bytes byte string byte
Any Any any
File file

2018 年振り返り

仕事

サーバサイドでは引き続き Go を書いていました。フロントエンドでは React を書く機会があったのが良かったです。

OSS

今年もコツコツ goa に機能追加したりしていました。

tchssk.hatenablog.com

tchssk.hatenablog.com

multipart/form-data でファイルアップロードする機能は仕事でも使ったのですが便利ですね。

あとは goagen をデフォルト GOPATH や、 Go 1.11 Modules で動くようにしたりしました。

github.com

github.com

v2 についてはひたすらテストコードを書くということをやっていて、数えてみたら Pull request を 36 本出してました。

github.com

これでもテストがない箇所はまだまだあるので根気よく続けていきたいと思っています。

買い物

Amazon Kindle Paperwhite

以前から欲しいなーと思いつつ購入していなかったのですが、サイバーマンデーで結構安くなっていたので書いました。文庫本サイズですが技術書もスマートフォンより読み易くていいですね。

The Last of Us Remastered

今年はたくさんゲームをやったのですがとにかく The Last of Us が最高でした。続編が製作中ということなのでとても期待しています。

iRobot Roomba

アプリや Wi-Fi に対応していない一番廉価なモデルですが掃除自体は問題なくこなしてくれます。障害物にぶつかる時は一応バンパーでショックを軽減する仕組みにはなっているのですが、家具に傷が付くと嫌なのでショックノンテープを貼っています。

Dyson Cyclone V10 Fluffy

こちらは細かい所の掃除用に。サイクロン式はゴミ捨てが面倒な場合が多いと聞きますが、これはワンタッチでゴミが取れるようになっているので便利です。

SHARP ドラム式洗濯乾燥機

乾燥機はヒートポンプ式が良いと聞いたのでデザインが気に入ったこのモデルを買いました。以前は乾燥機の付いていない洗濯機を使っていたので手で干していたのですが、一度乾燥機付きを体験してしまうと戻れませんね。

Panasonic 6ドア冷蔵庫

最初はミラータイプのものを検討していたのですが、結果的にはインテリアに良く馴染んでくれたのでブラウンを選んで正解でした。

Sony 55V型 有機EL テレビ

画面自体が振動して音が出る、という謎技術のテレビなのですが結構音も良いしベゼルもスリムで大変気に入っています。

I-O DATA HDMI キャプチャーボード

スプラトゥーンで自分のプレイを録画して見返す用に購入しました。比較的安価ですが 1080/60p で録画できて良いです。

一生もの。

f:id:tchssk:20181231213941j:plain

総括

新居が最高過ぎて下半期はほとんど家にいたような気がします。来年はもう少し運動しなければ ...

Go 1.11 Modules でパッケージ管理を行う

この記事は Go4 Advent Calendar 2018 の 22 日目の記事です。

今年の 8 月に Go 1.11 がリリースされました。このバージョンでは Modules と呼ばれる新しいコンセプトが導入されています。これはバージョニングとパッケージ配布の仕組みで、 GOPATH 配下以外でのプロジェクト管理を可能にするものです。

これは Go 1.11 の時点では実験的な機能として提供されていて、環境変数 GO111MODULE の設定やプロジェクトが GOPATH 配下にあるかどうかに応じて有効化されるようになっています。このバージョンでフィードバックを取り込みながら Go 1.12 で正式な機能としてリリースされる予定ということです。今回は現時点での Go Modules を試してみたいと思います。

Go 1.11 Modules で依存解決を行う

私は Web API の作成に goa を良く使っているので、その example である goa-cellar に Go 1.11 Modules を適用できるか試してみたいと思います。

1. GOPATH 外にプロジェクトを用意してそのルートに移動する

GOPATH~/go なので、その配下ではない ~/goa-cellargit clone してプロジェクトルートに移動します。

$ git clone git@github.com:goadesign/goa-cellar.git ~/goa-cellar
$ cd ~/goa-cellar

2. Module 定義を初期化して go.mod ファイルを作成する

go コマンドには既にサブコマンドとして mod が組み込まれているのでそれを使って go.mod を作成します。

$ go mod init
go: creating new go.mod: module github.com/goadesign/goa-cellar

3. Module をビルドする

Module をビルドします。 Go Modules ではビルドやテストのタイミングで必要な依存解決が行われます。

$ go build -o cellar
go: finding github.com/goadesign/goa v1.4.0
go: finding github.com/satori/go.uuid v1.2.0
go: finding github.com/go-kit/kit v0.8.0
go: finding github.com/dimfeld/httptreemux v5.0.1+incompatible
go: finding github.com/hashicorp/go-immutable-radix v1.0.0
go: finding github.com/go-logfmt/logfmt v0.4.0
go: finding github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: finding golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: finding github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding github.com/hashicorp/go-uuid v1.0.0
go: finding github.com/hashicorp/golang-lru v0.5.0
go: downloading github.com/goadesign/goa v1.4.0
go: downloading github.com/go-kit/kit v0.8.0
go: downloading golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/dimfeld/httptreemux v5.0.1+incompatible
go: downloading github.com/satori/go.uuid v1.2.0
go: downloading github.com/go-logfmt/logfmt v0.4.0
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/hashicorp/golang-lru v0.5.0
# github.com/goadesign/goa/uuid
../go/pkg/mod/github.com/goadesign/goa@v1.4.0/uuid/uuid.go:18:23: not enough arguments in call to uuid.Must
        have (uuid.UUID)
        want (uuid.UUID, error)

おっと、エラーが出てしまいました。コードを見てみます。

https://github.com/goadesign/goa/blob/v1.4.0/uuid/uuid.go#L18

import "github.com/satori/go.uuid"

// FromString Wrapper around the real FromString
func FromString(input string) (UUID, error) {
    u, err := uuid.FromString(input)
    return UUID(u), err
}

// NewV4 Wrapper over the real NewV4 method
func NewV4() UUID {
    return UUID(uuid.Must(uuid.NewV4()))
}

これは https://github.com/satori/go.uuid の問題ですね。当初 uuid.Must()uuid.UUID だけを返す関数だったのですが、あるとき破壊的変更が入って error も返す形にシグネチャが変更されました。しかしその変更のあとに新たなバージョンのリリースは行われておらず master ブランチだけに変更が入っている状態です。

goa はこの master ブランチのコードを参照しているのですが、 Go Modules はデフォルトだとタグ付けされた最新のリリースバージョンを選択するため master ではなく v1.2.0 が使われることになります。これがエラーの原因でした。

$ cat go.mod | grep github.com/satori/go.uuid
        github.com/satori/go.uuid v1.2.0 // indirect

このデフォルトのバージョン選択は @version 接尾辞を用いた go get で変更することが出来るので、明示的に master を参照するよう依存を変更します。

$ go get github.com/satori/go.uuid@master
go: finding github.com/satori/go.uuid master
go: downloading github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

再度ビルドしてみます。

$ go build -o cellar
go: finding github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

今度は成功です。ではサーバを起動してリクエストを送信してみます。

$ ./cellar &
[1] 27478
lvl=info msg=mount ctrl=Account action=Create route="POST /cellar/accounts"
lvl=info msg=mount ctrl=Account action=Delete route="DELETE /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Account action=List route="GET /cellar/accounts"
lvl=info msg=mount ctrl=Account action=Show route="GET /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Account action=Update route="PUT /cellar/accounts/:accountID"
lvl=info msg=mount ctrl=Bottle action=Create route="POST /cellar/accounts/:accountID/bottles"
lvl=info msg=mount ctrl=Bottle action=Delete route="DELETE /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=List route="GET /cellar/accounts/:accountID/bottles"
lvl=info msg=mount ctrl=Bottle action=Rate route="PUT /cellar/accounts/:accountID/bottles/:bottleID/actions/rate"
lvl=info msg=mount ctrl=Bottle action=Show route="GET /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=Update route="PATCH /cellar/accounts/:accountID/bottles/:bottleID"
lvl=info msg=mount ctrl=Bottle action=Watch route="GET /cellar/accounts/:accountID/bottles/:bottleID/watch"
lvl=info msg=mount ctrl=Public files=public/html/index.html route="GET /ui"
lvl=info msg=mount ctrl=Js files=public/js route="GET /js/*filepath"
lvl=info msg=mount ctrl=Js files=public/js/index.html route="GET /js/"
lvl=info msg=mount ctrl=Swagger files=public/swagger/swagger.json route="GET /swagger.json"
lvl=info msg=listen transport=http addr=:8081
$ http :8081/cellar/accounts
req_id=pTlK94BRv4-1 lvl=info msg=started GET=/cellar/accounts from=::1 ctrl=Account action=list
req_id=pTlK94BRv4-1 lvl=info msg=headers Accept=*/* Accept-Encoding="gzip, deflate" Connection=keep-alive User-Agent=HTTPie/1.0.2                                                                                                      
req_id=pTlK94BRv4-1 lvl=info msg=completed status=200 bytes=114 time=203.016µs ctrl=Account action=list
HTTP/1.1 200 OK
Content-Length: 114
Content-Type: application/vnd.account+json; type=collection
Date: Tue, 18 Dec 2018 13:58:15 GMT

[
    {
        "href": "/cellar/accounts/1",
        "id": 1,
        "name": "account 1"
    },
    {
        "href": "/cellar/accounts/2",
        "id": 2,
        "name": "account 2"
    }
]

$ kill 27478
[1]+  Terminated: 15          ./cellar

問題なさそうです。

依存としてのツール

goa はプロジェクトを構成するコードの大部分を自動生成する仕組みになっており、そのために goagen という専用のツールを用意しています。 Go Modules は、そういったプロジェクトに必要となるツールの管理も出来るようになっています。 Go modules by example に詳しい手順が載っているので、これを参考に goagen を管理してみます。

GOBIN にツールのインストール先を設定する

Go はデフォルトだと go install で作られたバイナリを $GOPATH/bin に配置しますが、 $GOBIN を設定することで対象のパスを変更することが出来ます。今回はツールもプロジェクトのディレクトリ配下に置くようにするため $PWD/bin に設定します。

$ export GOBIN=$PWD/bin
$ export PATH=$GOBIN:$PATH

ツールを import するためのファイルを作成する

ツールの package を import するためのファイルを作成します。先頭行のコメントは Build Constraints と呼ばれるもので、デフォルトのビルド対象から除外することが出来ます。

$ cat tools.go
// +build tools

package tools

import (
   _ "github.com/goadesign/goa/goagen"
)

ツールをインストールする

ではツールをインストールしてみます。

$ go get github.com/goadesign/goa/goagen@master
go: finding github.com/goadesign/goa/goagen master
go: finding github.com/goadesign/goa master
go: finding github.com/manveru/faker latest
go: finding github.com/zach-klippenstein/goregen latest
go: finding github.com/dimfeld/httppath latest
go: finding github.com/spf13/pflag v1.0.3
go: finding github.com/spf13/cobra v0.0.3
go: finding golang.org/x/tools/go/ast/astutil latest
go: downloading github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: downloading github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: finding gopkg.in/yaml.v2 v2.2.2
go: downloading github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: downloading github.com/spf13/pflag v1.0.3
go: downloading github.com/spf13/cobra v0.0.3
go: finding golang.org/x/tools/go/ast latest
go: finding golang.org/x/tools/go latest
go: finding golang.org/x/tools latest
go: downloading golang.org/x/tools v0.0.0-20181218204010-d4971274fe38
go: downloading gopkg.in/yaml.v2 v2.2.2
go: finding gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
$ tree ./bin/
./bin/
└── goagen

0 directories, 1 file

$PWD/bin/goagen が作成されました。

インストールしたツールを使う

このプロジェクトには Makefile が含まれており、 make することで以下のタスクが実行されるようになっています。

  • 関連する package の go get
  • goagen によって前回生成されたコードの削除
  • goagen によるコードの再生成
  • go build

go get 対象の goa のパッケージに @master を付与しておきます。

diff --git a/Makefile b/Makefile
index 9790e3a..80a82d7 100644
--- a/Makefile
+++ b/Makefile
@@ -14,9 +14,9 @@
 # - all is the default target, it runs all the targets in the order above.
 #
 DEPEND=    bitbucket.org/pkg/inflect \
-  github.com/goadesign/goa \
-  github.com/goadesign/goa/goagen \
-  github.com/goadesign/goa/logging/logrus \
+   github.com/goadesign/goa@master \
+   github.com/goadesign/goa/goagen@master \
+   github.com/goadesign/goa/logging/logrus@master \
    github.com/sirupsen/logrus \
    gopkg.in/yaml.v2 \
    golang.org/x/tools/cmd/goimports

make します。

$ make
go: finding github.com/goadesign/goa/goagen master
go: finding github.com/goadesign/goa/logging/logrus master
go: finding github.com/goadesign/goa master
go: finding github.com/goadesign/goa/logging master
go: finding bitbucket.org/pkg/inflect latest
go: finding golang.org/x/tools/cmd/goimports latest
go: finding golang.org/x/tools/cmd latest
go: finding golang.org/x/tools latest
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/account_testing.go
app/test/bottle_testing.go
app/test/health_testing.go
app/test/js_testing.go
app/test/public_testing.go
app/test/swagger_testing.go
public/swagger
public/swagger/swagger.json
public/swagger/swagger.yaml
public/schema
public/schema/schema.json
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/account.go
client/bottle.go
client/health.go
client/js.go
client/public.go
client/swagger.go
client/user_types.go
client/media_types.go
public/js
public/js/client.js
public/js/axios.min.js
public/js/index.html
public/js/example.go

無事にビルドできました。

余談ですが、 goagen は今まで GOPATH を前提とした実装になっていたので Go Modules では動きませんでした。実は今回の記事を書くにあたって Go Modules をサポートする Pull request を出したので goa をお使いの方は試してみてもらえると嬉しいです。 (記事の公開前にマージされて良かった😌)

github.com

依存解決でダウンロードされたパッケージ

実は今回の作業を始める前に $GOPATH/src を空にしておいたので、念のため中身を確認してみます。

$ tree ~/go/src/
/Users/tchssk/go/src/

0 directories, 0 files

何もないですね。

Go Modules は依存解決でダウンロードしたパッケージを $GOPATH/pkg/mod/ の配下にキャッシュするようです。

$ tree -L 2 ~/go/pkg/mod/
/Users/tchssk/go/pkg/mod/
├── bitbucket.org
│   └── pkg
├── cache
│   ├── download
│   └── vcs
├── github.com
│   ├── armon
│   ├── dimfeld
│   ├── go-kit
│   ├── go-logfmt
│   ├── goadesign
│   ├── hashicorp
│   ├── manveru
│   ├── satori
│   ├── sirupsen
│   ├── spf13
│   └── zach-klippenstein
├── golang.org
│   └── x
└── gopkg.in
    └── yaml.v2@v2.2.2

21 directories, 0 files

このキャッシュは go clean -modcache で削除することが出来ます。

$ go clean -modcache
$ tree -L 2 ~/go/pkg/mod/
/Users/tchssk/go/pkg/mod/ [error opening dir]

0 directories, 0 files

また、依存パッケージは go mod vendor でプロジェクト内にコピーすることもできます。

$ go mod vendor                
go: finding github.com/hashicorp/go-immutable-radix v1.0.0
go: finding github.com/spf13/pflag v1.0.3          
go: finding github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: finding github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: finding github.com/goadesign/goa v1.0.1-0.20181216194621-14b7b9950e87
go: finding github.com/sirupsen/logrus v1.2.0      
go: finding github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: finding github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
go: finding golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52
go: finding golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: finding github.com/go-logfmt/logfmt v0.4.0        
go: finding github.com/pmezard/go-difflib v1.0.0      
go: finding github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: finding golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
go: finding github.com/davecgh/go-spew v1.1.1         
go: finding github.com/stretchr/testify v1.2.2       
go: finding github.com/hashicorp/go-uuid v1.0.0     
go: finding github.com/konsorten/go-windows-terminal-sequences v1.0.1
go: finding github.com/dimfeld/httptreemux v5.0.1+incompatible
go: finding github.com/stretchr/objx v0.1.1
go: finding github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding github.com/go-kit/kit v0.8.0                  
go: finding github.com/spf13/cobra v0.0.3                 
go: finding github.com/hashicorp/golang-lru v0.5.0               
go: finding golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
go: finding bitbucket.org/pkg/inflect v0.0.0-20130829110746-8961c3750a47
go: finding gopkg.in/yaml.v2 v2.2.2
go: finding gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
go: downloading github.com/goadesign/goa v1.0.1-0.20181216194621-14b7b9950e87
go: downloading golang.org/x/net v0.0.0-20181217023233-e147a9138326
go: downloading github.com/spf13/cobra v0.0.3                          
go: downloading github.com/go-kit/kit v0.8.0                  
go: downloading github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
go: downloading gopkg.in/yaml.v2 v2.2.2
go: downloading github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
go: downloading github.com/spf13/pflag v1.0.3
go: downloading github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/dimfeld/httptreemux v5.0.1+incompatible
go: downloading github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
go: downloading golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/hashicorp/golang-lru v0.5.0
go: finding github.com/inconshreveable/mousetrap v1.0.0       
go: downloading github.com/go-logfmt/logfmt v0.4.0       
go: downloading github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
go: finding google.golang.org/appengine v1.3.0               
go: downloading github.com/inconshreveable/mousetrap v1.0.0   
go: downloading google.golang.org/appengine v1.3.0
go: finding golang.org/x/net v0.0.0-20180724234803-3673e40ba225  
go: finding github.com/golang/protobuf v1.2.0
go: finding golang.org/x/text v0.3.0    
go: downloading github.com/golang/protobuf v1.2.0
$ tree -L 2 ./vendor/
./vendor/
├── github.com
│   ├── armon
│   ├── dimfeld
│   ├── go-kit
│   ├── go-logfmt
│   ├── goadesign
│   ├── golang
│   ├── hashicorp
│   ├── inconshreveable
│   ├── kr
│   ├── manveru
│   ├── satori
│   ├── spf13
│   └── zach-klippenstein
├── golang.org
│   └── x
├── google.golang.org
│   └── appengine
├── gopkg.in
│   └── yaml.v2
└── modules.txt

20 directories, 1 file

Go Modules の今後

この記事を書いていたらちょうど The Go Blog で Go Modules の記事が公開されていました。

Go Modules in 2019 - The Go Blog

Go 1.12, scheduled for February 2019, will refine module support but still leave it in auto mode by default.

Our aim is for Go 1.13, scheduled for August 2019, to enable module mode by default (that is, to change the default from auto to on) and deprecate GOPATH mode.

2019 年 2 月予定の Go 1.12 は現在と同じようにデフォルトで auto モードで動作するようですね。 Module モードをデフォルトにするのは 2019 年 8 月予定の Go 1.13 からの予定で、そのときには GOPATH モードを非推奨にするということです。楽しみですね。

goa が返すエラーレスポンスの Content-Type を変更する

Web API がエラーレスポンスを返す際にはレスポンスボディでエラーの詳細を示すのが一般的です。 goa には goa.ErrorResponse という型が組み込まれており、 controller に到達する前の処理でエラーが発生した際のレスポンスに使われています。また controller の中で goa.ErrorResponse を使うこともできます。

controller 到達前のエラーレスポンス

例えば /foo というエンドポイントが存在しないのに GET /foo というリクエストが来たときには以下の形のレスポンスが返ります。

{
  "id": "XXXXXXXX",
  "code": "not_found",
  "status" :404,
  "detail": "/foo"
}

この際、レスポンスヘッダの Content-Typeapplication/vnd.goa.error となります。この値は goa.ErrorMediaIdentifier として定義されているため変更することが可能です。例えば Content-Typeapplication/vnd.myservice.error に変える場合は以下のコードを main パッケージに追加します。

package main

import (
    "github.com/goadesign/goa"
)

func init() {
    // Change the media type identifier used for error responses.
    goa.ErrorMediaIdentifier = "application/vnd.myservice.error"
}

controller が返すエラーレスポンス

goa.ErrorResponse には、対応する design 定義として design.ErrorMedia が用意されています。これを design の中で使うことで、あるレスポンスが goa.ErrorResponse のフォーマットであることを明示できます。以下の例では BadRequestErrorMedia を指定することで、不正なリクエストが来たときのエラーレスポンスが goa.ErrorResponse のフォーマットであることを表しています。

Response(BadRequest, ErrorMedia)

controller の実装では ctx.BadRequest() を呼び出すことで Bad Request を返却することができます。その際、引数に *goa.ErrorResponse を渡すとそれがレスポンスボディに設定されます。

ctx.BadRequest(&goa.ErrorResponse{
    Detail: "this is a error message",
})

こちらの Content-Typeapplication/vnd.goa.error ですが、controller 到達前のエラーレスポンスとは違い design.ErrorMedia.Identifier が参照されています。このフィールドには design.ErrorMediaIdentifier が設定されているため、まず design.ErrorMediaIdentifier を変更した上で design.ErrorMedia.Identifier に再設定しておくことで実際の Content-Type を変更することができます。

package design

import (
    "github.com/goadesign/goa/design"
)

func init() {
    // Change the media type identifier used for error responses.
    design.ErrorMediaIdentifier = "application/vnd.myservice.error"
    design.ErrorMedia.Identifier = design.ErrorMediaIdentifier
}

Swagger UI

goa は Swagger 定義を出力できるので Swagger UI と組み合わせて使われることも多いと思います。しかし application/vnd.goa.error のようにサフィックスが付いていない形式だと Try it out ボタンでリクエストを試すときにエラーレスポンスが Unknown response type となり表示されません。

f:id:tchssk:20180922113030p:plain

これを表示させるには、プロジェクトに以下のコードを追加して Content-Typeサフィックス付きの application/vnd.goa.error+json に変更します。

package main

import (
    "github.com/goadesign/goa"
)

func init() {
    // Change the media type identifier used for error responses.
    goa.ErrorMediaIdentifier = goa.ErrorMediaIdentifier + "+json"
}
package design

import (
    "github.com/goadesign/goa/design"
)

func init() {
    // Change the media type identifier used for error responses.
    design.ErrorMediaIdentifier = design.ErrorMediaIdentifier + "+json"
    design.ErrorMedia.Identifier = design.ErrorMediaIdentifier
}

これで Swagger UI でもレスポンスが表示されるようになります。

f:id:tchssk:20180922113527p:plain

goa で multipart/form-data を扱う (ファイルアップロード編)

前回 goa で multipart/form-data を扱う方法についてご紹介しました。

tchssk.hatenablog.com

multipart/form-data に関連してよく取り上げられるのがファイルアップロードですね。 HTML フォームを介したサーバへのファイル送信は多くのウェブサービスで利用されています。しかし前回の記事ではその話については言及しませんでした。理由は「ファイル」を表すデータ型がまだ goa に存在しなかったからです。

goa では Type DSL を使ってユーザ型を定義します。また、ユーザ型の各要素を定義するには Attribute DSL を使用します。

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Required("name", "birthday")
})

Attribute の 2 番目の引数がデータ型です。上記の StringDateTime を含む、以下 7 種類が用意されています

  • Boolean
  • Integer
  • Number
  • String
  • DateTime
  • UUID
  • Any

そして今回、新たに 8 種類目の File 型が追加されました。

github.com

では先ほどの design に File 型の要素を追加してみましょう。

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Attribute("icon", File, "Icon")
    Required("name", "birthday", "icon")
})

ここから生成される Go の構造体は以下の形になります。

// ProfilePayload user type.
type ProfilePayload struct {
    // Birthday
    Birthday time.Time `form:"birthday" json:"birthday" xml:"birthday"`
    // Icon
    Icon *multipart.FileHeader `form:"icon,omitempty" json:"icon,omitempty" xml:"icon,omitempty"`
    // Name
    Name string `form:"name" json:"name" xml:"name"`
}

Icon の型が *multipart.FileHeader になっていますね。これは標準パッケージの mime/multipart で提供されている構造体で、ファイル名、 MIME ヘッダ、ファイルサイズを含んでいます。

type FileHeader struct {
        Filename string
        Header   textproto.MIMEHeader
        Size     int64
}

また、この構造体には Open() というメソッドが用意されており、ファイルを multipart.File として取得することが可能です。 multipart.Fileio.Reader などを実装しているので通常のファイルと同じようにように操作することができます。

以上を踏まえて、アップロードされたファイルをサーバのローカルディレクトリに保存する処理を controller に実装してみましょう。

func (c *ProfilesController) Submit(ctx *app.SubmitProfilesContext) error {
    res := &app.ResultMedia{
        Name:     ctx.Payload.Name,
        Birthday: ctx.Payload.Birthday,
    }
    file, err := ctx.Payload.Icon.Open()
    if err != nil {
        return err
    }
    defer file.Close()
    f, err := os.OpenFile("./icons/"+ctx.Payload.Icon.Filename, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        return fmt.Errorf("failed to save file: %s", err) // causes a 500 response
    }
    defer f.Close()
    io.Copy(f, file)
    return ctx.OK(res)
}

前述の通り ctx.Payload.Icon*multipart.FileHeader となっているので Open()multipart.File を取得しています。あとはアップロードされたものと同じ名前のファイルを os.OpenFile() で作成し io.Copy() で書き込むだけです。

この例は既に公式サンプルにも収録されているので以下からご覧ください。

github.com

ファイルアップロードにも対応した goa が益々便利ですね!