Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

http.RoundTripperでHTTPクライアントを拡張する

この記事はQiitaで公開されていました

GoでHTTPリクエストを行いたい場合、一般的には net/httphttp.Gethttp.Postを使うことになると思います。もしくは少し複雑なリクエストする場合、http.NewRequestを使うかもしれません。

上記どの方法を使うにしても、これらのリクエストはhttp.Client.Transportを通して、最終的にhttp.RoundTripper.RoundTripが呼び出されることになりますが、http.Client.Transporthttp.RoundTripper インターフェイスを満たしていれば良いので、ユーザが自由に置き換えることが可能です。http.RoundTripper の実装については、過去や今年のアドベントカレンダーに記事があるのでそちらを参照ください。

http.RoundTripper の具体的な話としては、例えば golang.org/x/oauth2 パッケージでは、アクセストークンの有効期限が切れた場合はリフレッシュトークンを使って自動更新する http.RoundTripper が組み込まれた http.Client を使います。こうした仕組みにより、ユーザ側のコードでOAuth2のヘッダを追加したりトークンを更新したりといった面倒な処理を書かなくてもうまく動くようになっています。

これと同様に、HTTPリクエストの事前処理または事後処理があるのなら、自作の http.RoundTripper を実装してみるといいんじゃないかなと思います。ただし自作する場合にとても重要なリソースの問題があって、http.Transportは内部でHTTP Keep-Aliveなどを行っているため、特に理由が無い場合は http.DefaultTransport を使い回すことが推奨されます。それでも、どうしても http.Transport を作らなければならない理由があるなら、全てのリクエストが終わった後で必ずhttp.Transport.CloseIdleConnectionsを呼びましょう。これを呼ばないとコネクションやゴルーチンなど色々なものがリークします。

リソース管理についてはGoのnet/httpクライアントで大量のリクエストを高速に行う方法にも少し書きました。

作ってみて便利だったもの

ここからは、今まで作った http.RoundTripper の実装で、個人的に便利だったものの紹介です。

リクエストとレスポンスを出力するもの

通過するリクエストとレスポンスのヘッダとボディを出力する http.RoundTripper です。デバッグする時だけ差し込めばいいのでとても楽でした。

type DumpTransport struct {
    Transport   http.RoundTripper
    Output      io.Writer
}

func (t *DumpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    w := t.Output
    if w == nil {
        w = os.Stdout
    }

    b, err := httputil.DumpRequest(req, true)
    if err != nil {
        return nil, err
    }
    if _, err := w.Write(b); err != nil {
        return nil, err
    }
    resp, err := t.Transport.RoundTrip(req)
    if err != nil {
        return nil, err
    }
    b, err = httputil.DumpResponse(resp, true)
    if err != nil {
        return nil, err
    }
    if _, err := w.Write(b); err != nil {
        return nil, err
    }
    return resp, nil
}

指数バックオフでリトライするもの

context で期限が来るまで指数バックオフする http.RoundTripper です。単純に増加するだけならそれほどでもないんですが、Retry-After ヘッダがあればそこまで待たなければならないなど地味に面倒だったりします。

type RetriableTransport struct {
    Transport http.RoundTripper
}

var retriableStatuses = map[int]struct{}{
    http.StatusTooManyRequests:     struct{}{},
    http.StatusInternalServerError: struct{}{},
    http.StatusServiceUnavailable:  struct{}{},
    http.StatusGatewayTimeout:      struct{}{},
}

func (p *RetriableTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    ctx := req.Context()
    var w backoff.Backoff
    for {
        resp, err := p.Transport.RoundTrip(req)
        if err != nil {
            if !isTemporary(err) {
                return nil, err
            }
            w.Wait(ctx)
            continue
        }
        if _, ok := retriableStatuses[resp.StatusCode]; !ok {
            return resp, nil
        }

        if d := ParseRetryAfter(resp, time.Now()); d > 0 {
            w.SetNext(d)
        }
        io.Copy(ioutil.Discard, resp.Body)
        resp.Body.Close()
        resp = nil
        if err := w.Wait(ctx); err != nil {
            return nil, err
        }
    }
}

type temporaryer interface {
    Temporary() bool
}

func isTemporary(err error) bool {
    e, ok := err.(temporaryer)
    return ok && e.Temporary()
}

一定期間内のリクエスト数を制限するもの

一定時間のリクエスト数、例えば1秒で最大100回までに制限する http.RoundTripper です。

type RateLimitTransport struct {
    Transport http.RoundTripper
    Interval  time.Duration
    Limit     int

    l    *rate.Limiter
    once sync.Once
}

func (t *RateLimitTransport) interval() time.Duration {
    return t.Interval / time.Duration(t.Limit)
}

func (t *RateLimitTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    t.once.Do(func() {
        n := rate.Every(t.interval())
        t.l = rate.NewLimiter(n, t.Limit)
    })
    ctx := req.Context()
    if err := t.l.Wait(ctx); err != nil {
        return nil, err
    }
    return t.Transport.RoundTrip(req)
}

rategolang.org/x/time/rate を使いました。

全てのリクエストが終わったことを保証するもの

通過したリクエスト全てが終わるまで待つ http.RoundTripper です。上で紹介したリトライやレートリミットと組み合わせると、待ち時間も含めて終わるまで待ちます。

type ClosableTransport struct {
    Transport http.RoundTripper

    wg     sync.WaitGroup
    closed int32
}

func (p *ClosableTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    p.wg.Add(1)
    defer p.wg.Done()
    if v := atomic.LoadInt32(&p.closed); v != 0 {
        return nil, errors.New("closed")
    }
    return p.Transport.RoundTrip(req)
}

func (p *ClosableTransport) Close() error {
    atomic.StoreInt32(&p.closed, 1)
    p.wg.Wait()
    return nil
}

おわりに

いかがだったでしょうか。ひとつひとつは簡単な実装ですが、例えば

  • 1分あたりのリクエスト数を300に制限する
  • リトライが必要なら指数バックオフでリトライする
  • リトライ分も含めて全て終わってからリソースを解放する
  • デバッグオプション付きの場合だけリクエストとレスポンスを出力する

という処理を愚直に実装すると、リトライやレートリミット解除待ちのリクエストをどう扱うのかなど地味に面倒なんですが、組み合わせで対応できるので個人的には便利だなと思っています。

ここで紹介したもの以外にもgithub.com/lufia/httpclientutilで公開しているので、興味があれば眺めてみてください。

また、似たようなパッケージもいくつかあります。サーバサイドのMiddlewareと区別して、http.RoundTripper実装のことをTripperwareと呼んでいるものもありますね。

http.RoundTripper 実装のヒントになれば幸いです。

GitHubへHTTPSアクセスする場合はトークンを使った方が良さそう

GitHubGitHub Enterpriseで、HTTPSを経由したgit push git pullをするとログインIDとパスワードの入力を促されますが、二段階認証を有効にしていると、正しいIDとパスワードでも認証がエラーになります。この場合、repo スコープを有効にしたトークンを用意して、パスワードの代わりにトークンを入力すれば良いです。

トークン入力を省略するため ~/.netrc の利用を案内する記事がありますけれども、さすがにトークンとはいえプレーンな形で保存するのは良くないので、最低でも暗号化しておきましょう。

macOSの場合は、いちど入力すると以降はKeychain Access.appに保存されるので~/.netrc は不要です。Keychain Access.appに保存されない場合は ~/.ssh/configUseKeychain yes を設定すると保存できるようになります。

そんなことを同僚氏に話したところ、なんか普通に2段階認証を設定してなくてもトークンを使うのが良いらしいということを聞いたので、今後はHTTPSトークンでアクセスしようと思いました。

HTTPSを使いたい理由

トークンを使わなくても、SSHでアクセスすれば問題なかったんですが、この時は珍しくサブモジュールを使いました。

サブモジュールを登録したリポジトリをJenkinsでビルドする場合、

checkout([
    $class: 'GitSCM',
    branches: scm.branches,
    extensions: scm.extensions + [
        [ $class: 'SubmoduleOption',
          disableSubmodules: false,
          parentCredentials: true,
          recursiveSubmodules: true,
          reference: '',
          trackingSubmodules: false
        ],
    ],
    userRemoteConfigs: scm.userRemoteConfigs
])

このように書けばサブモジュールも拾ってきてくれますが、JenkinsのGitHub Branch Source pluginGitHubAPIを利用してgit pullするので、parentCredentials: trueでメインリポジトリで使った資格情報を参照するとHTTPSトークンでアクセスできますが、SSHの場合はそれとは別に鍵の設定が必要になります。あまりCI環境への依存を増やしたくなかったので、サブモジュールのremoteHTTPSにしておく必要があってSSHが使えませんでした。

Go 2のgenerics/contract簡易まとめ

この記事はQiitaで公開されていました

2020年8月追記

この記事は2018年頃に出てきたGoのジェネリクスに関するドラフトを読んで、個人的に理解した内容をまとめたものです。2020年時点ですでに、contractの代わりにinterfaceが使われるなど、十分に内容が古くなってしまっているので最新仕様が知りたい人は新しい情報を探してください。

当時の内容

以下の内容は、ドラフトを読みながら書いているので、おそらく抜けている部分や理解不足なところはあると思います。特に、間違いがあれば修正しますので、この記事のコメント等で教えてください。原書は以下のリンクから。

導入

Goはこれまで、ジェネリクスも持っていないし継承もありませんでした。そのため、コレクションを扱う汎用的な方法は、interface{}などを使って多少ぎこちないけれどもうまくやるしかありませんでした。

例えばGo 1.11現在、標準の container/list はこんな感じです。

// 定義
package list

type Element struct {
    Value interface{}
}
func (e *Element) Next() *Element
func (e *Element) Prev() *Element

// 使い方
package main

l := list.New()
l.PushFront("test")
for e := l.Front(); e != nil; e = e.Next() {
    s := e.Value.(string)
    if s == "test" {
        ...
    }
}

元の型に戻すためには、扱う側で型アサーション(type assertion)しないといけません。リストとして動作はしていますが、もっと型の恩恵を受けたくなりますね。

また、標準の sort パッケージは、

func SearchFloat64s(a []float64, x float64) int
func SearchInts(a []int, x int) int
func SearchStrings(a []string, x string) int
type Float64Slice
type IntSlice
type StringSlice
func Float64s(a []float64)
func Ints(a []int)
func Strings(a []string)

など、よく使うプリミティブ型ごとに同じようなメソッドを提供してくれていますが、この辺りも、今後メンテナンスすることを考えるとつらいだろうなと思いますし、使う側も、例えば[]int32をソートしようと思った場合、

// これはできない
a := []int32{0, 1, 2}
sort.Ints(a) // cannot use s (type []int32) as type []int in argument to sort.Ints

// 詰め替えてあげる必要がある
a1 := make([]int, len(a))
for i := range a {
    a1[i] = int(a[i])
}
sort.Ints(a1)

のように、適切な配列へ詰め替えるか、sort.Interfaceを満たす型を自分で書くか、のどちらかが必要です。

ジェネリクスへの要望はずっと前からあったにも関わらず、言語の複雑さや実行速度などの影響から導入されてきませんでしたが、記事の最初で紹介したように、Go 2 Draft Designsジェネリクスのドラフトが上がってきました。Go 2と呼称していますが、実際はGo 1.15または1.16あたりを指しているようです。全く別の言語になるわけではありませんし、互換性も今まで通り維持されます。

追加される文法

ドラフトによると、ジェネリクスのために、型パラメータ型引数コントラクトが追加されます。

型パラメータと型引数

ジェネリックな型や関数は、名前の直後にカッコとtypeを使って実装します。型名の部分を型パラメータ (type parameter)と呼びます。

type List(type T) []T

func (l *List(T)) PushBack(x T)

type IntList = List(int) // typealias

func Keys(type K, V)(m map[K]V) []K

ジェネリックな型や関数を使う側は型名を渡します。型パラメータに渡す型は、型引数 (type argument)と呼びます。

var l List(string)
l.PushBack("hello")

// 引数から型引数がわかる場合は省略可能
keys := Keys(map[string]int{"A": 1, "B": 2})

他の言語、例えばJavaC++を経験した人は、なんで<T>[T]じゃないの、と思いますが、以下のようなケースで言語パーサが複雑になるからだそうです。

v := F<T>
n := f(a<b, c>d)

これらジェネリックな関数は、型パラメータの部分だけを適用することも可能です。上で挙げたsortパッケージの例をGo 2のジェネリクスで再定義すると、こんな感じでしょうか。

func Sort(type T)(a []T)
var Ints = Sort(int)
var Strings = Sort(string)

コントラクトの追加

型パラメータだけでは、例えばSort(type T)(a []T)の実装はできません。a[i]の値がa[j]と比べて大小どちらなのかを比較する方法が必要です。または実装によっては、sort.InterfaceのようにLess()メソッドを要求するかもしれません。しかし型パラメータだけでは必要な条件を表明することができません。

このため、ドラフトでは、contractを使って必要な条件を表明します。コントラクトにはinterfaceと同じように、メソッドシグネチャを書けますし、型名を書くこともできます。コントラクトの型パラメータは複数書くことができるので、区別のためメソッド名の前に型パラメータが必要です。

memo: ,で区切るとOR条件、別に分けるとAND条件

contract equaler(T) {
    T int, Equal(T) bool
}
contract comparer(T) {
    equaler(T)
    T Less(T) bool
}

contractの名前はよく小文字で表記されますが、型チェックで使うものなので、他のパッケージへエクスポートしている必要は 例を眺めた限りではおそらく ありません。(大文字開始の名前にすればエクスポート可能ですが)

こんな面倒なもの導入しなくても、ジェネリック関数の本文から必要な条件を抽出することもできるんじゃないのと思ってしまいますが、それだと内部実装を少し変更するだけでコントラクトも変わってしまうしエラーメッセージも不恰好になるため、コントラクトとして明記するように設計したようです。

型パラメータにコントラクトを追加

contractを参照する側は、型パラメータの後に続けてコントラクトを書きます。

func Sort(type T comparer(T))(a []T)

// contractのTは省略しても良い
func Sort(type T comparer)(a []T)

// 型パラメータが複数ある場合(この2つは同等)
func F(type T1, T2 comparer)(t1 T1, t2 T2)
func F(type T1, T2 comparer(T1, T2))(t1 T1, t2 T2)

コントラクト付きの型パラメータに、型引数を与えたコードをコンパイルすると、型引数がコントラクトを満たしているかのチェックがコンパイラで行われます。

interfaceとの違い

とりあえず気づいたところを2つ。他にもあるかもしれません。

  • interfaceは常にポインタと型を持つけどジェネリクスは値型のまま使える
  • interfaceは実行時にメソッド解決だけどジェネリクスコンパイル時に解決する?
  • 型の混在したコンテナのようなものはinterfaceでなければできない?

感想と告知

他の言語と様子が全然違いますが、Goのジェネリクスについて雰囲気はつかめたでしょうか。最初のバージョンと比べてだいぶ理解しやすくなったと思いますし、インターフェイスとの使い分けも分かりやすくなったんじゃないかなと感じました。

GoDocドキュメントの書き方

この記事はQiitaで公開されていました

Goでドキュメントを書くとき、一般的にはGoDocを使うと思います。GoDocはシンプルにみえて、実際は色々な書き方をサポートしていますし、ブラウザで単純に表示する以外の読み方もあるので、一通りの方法をまとめてみました。

ドキュメントの書き方

GoDocではソースコードの中に、ある決まった形でコメントを書くと、そのコメントをドキュメントとして扱うことができます。具体的には、パッケージ、型、定数、構造体フィールド、関数・メソッドそれぞれの直前に、空行を入れずコメントを書きます。これらの前に改行を入れてしまうと、ただのコメントになってしまいます。

装飾について

GoDocは、大きなドキュメントのために、ヘッダと整形されたテキストの2通り装飾ができます。ただし、リストやテーブルなどは対応していません。

ヘッダ

以下の条件を全て満たせば、ヘッダとして認識されます。

  • 大文字で開始される(日本語文字は適用外)
  • 1行だけで構成されている
  • 句読点を含まない
  • 直前の要素がヘッダではない
  • 最初の要素ではない

最後の2つは、

/*
Paragraph 1

Paragraph 2

Paragraph 3
*/
package qiita

というコメントブロックがあった場合、Paragraph 1はコメントブロック中の最初にある要素なので、ヘッダにはなりません。また、Paragraph 3は直前の要素がヘッダなので、通常の段落になります。

整形済みテキスト

他の要素よりもインデントを増やせば整形されたテキストになります。

リンク

URLを書けばそのままリンクに変換されます。任意のテキストにリンクを付けることはできません。

具体例

以下で、それぞれのコメントがどのように表示されるかを紹介します。

パッケージドキュメント

パッケージに対するコメントは、ドキュメントの一番上にOverviewとして表示されます。ここには、パッケージのざっくりとした説明を書いたり、パッケージ全体を通して使われる仕様などの情報を記載するために記述します。以下はパッケージコメントの例です。

// Copyright xxx

// +build darwin

/*
Godoc はGoのパッケージドキュメント情報をQiitaで紹介するためのパッケージです。そのままサンプルとして使えます。
リポジトリは https://github.com/lufia/godoc-sample です。

How to read a document

ドキュメントを読むためにはgo docコマンドまたはgodocコマンドが使えます。

How to write a document

パッケージドキュメントはpackage句の直前に書く必要がありますが、
Build constraintsはpackageよりも前に書かなければなりません。
そのため、記述する順番としては、Build constraintsが先になります。

空行を入れると、別の段落として区切ることができます。

   インデントすると、ソースコードのような
   整形されたテキストも書けます

Heading

アルファベットの大文字で始まり、句読点を含まない1行だけの段落があれば、
それはヘッダとして装飾されます。ただし、ヘッダを2つ以上続けることはできません。
次の行はヘッダになりますが、その次は同じルールにも関わらず普通の段落です。

This is a header

This is not a header

*/
package qiita

このコメントをGoDocで読むと、以下のように表示されます。

f:id:lufiabb:20200328212429p:plain
Overview

パッケージ一覧での表示

パッケージドキュメントの最初に現れる文は、パッケージ一覧ページでも使われます。パッケージドキュメントが1文以上書かれていても、最初の文だけがパッケージ一覧ページに概要として使われ、残りは無視されます。例えば日本語の場合、読点(。)で文が終わります。英語の場合はピリオドで終わりますが、この場合は後ろにスペースがなければ文の終わりとしては認識しません。

f:id:lufiabb:20200328212508p:plain
Package概要

型とフィールド

型宣言の直前にコメントを書くと、それは型に対するドキュメントになります。もし型が構造体だったなら、フィールドのコメントも一緒にドキュメントとして表示されます。フィールドの上に書いてもいいし、行末に書いても構いません。例えば以下のコードは、

// Article は1つの記事を表します。
type Article struct {
    // 記事のタイトル
    Title string
    
    // 記事本文
    Body string
    
    // 状態
    Status PostStatus // Draft or Publish
}

次のように表示されます。

f:id:lufiabb:20200328212539p:plain
型とフィールドの表示例

変数・定数

変数や定数も同じで、直前のコメントがドキュメントになります。

// PostStatus は記事の投稿状態を表現します。
type PostStatus int

// 記事の投稿状態。
const (
    StatusDraft   PostStatus = iota // 下書き
    StatusPublish                   // 公開済み
)

このコードは、次のように表示されます。

f:id:lufiabb:20200328212608p:plain
定数の表示例

constの直前と定数それぞれにドキュメントが付きます。これはvarも同様です。

メソッド・関数

メソッドや関数へのコメントもドキュメントになります。

// NewArticle はタイトルをtitleに設定した新しい記事を作成します。
func NewArticle(title string) *Article {
    return &Article{Title: title}
}

// Save は、記事aの状態をデータベースに保存します。
func (a *Article) Save() error {
    // BUG(lufia): 保存機能は未実装です。
    // TODO(lufia): 実装する
    return nil
}

これをドキュメントにすると、以下のように表示されます。

f:id:lufiabb:20200328212638p:plain
関数とメソッドの表示例

GoDocは、型に関連するメソッドや関数を、なるべく近くに表示するように並び替えます。

コード例を書く

GoDocでは、パッケージを使ったコードサンプルをExampleとして掲載できます。コード例はコメントではなく、テストコードとしてExampleで始まる名前のメソッドを実装すると、名前に対応した場所にドキュメントとして表示されるものです。対応する場所とは以下の通りです。

関数名 場所
Example() パッケージ全体
ExampleFunc() Func関数
ExampleType_Func() Type型のFuncメソッド

また、それぞれのExample関数名の終わりに、最初が小文字で始まる名前を付けると、別のパターンとして複数のコード例を書くことができます。一通り包括した例を挙げます。

package qiita_test

import (
    "fmt"
    "log"

    "github.com/lufia/godoc-sample"
)

func Example() {
    a := qiita.NewArticle("テスト")
    fmt.Println(a.Title)
    // Output: テスト
}

func Example_other() {
    a := qiita.NewArticle("テスト")
    a.Body = "サンプル"
    fmt.Println(a.Body)
    // Output: サンプル
}

func ExampleNewArticle() {
    a := qiita.NewArticle("テスト")
    fmt.Println(a.Status)
    // Output: 0
}

func ExampleNewArticle_otherStatus() {
    a := qiita.NewArticle("テスト")
    a.Status = qiita.StatusPublish
    fmt.Println(a.Status)
    // Output: 1
}

func ExampleArticle_Save() {
    a := qiita.NewArticle("テスト")
    a.Save()
}

func ExampleArticle_Save_errorHandling() {
    a := qiita.NewArticle("エラー")
    if err := a.Save(); err != nil {
        log.Fatalln(err)
    }
}

上記のコードで、パッケージ名をqiita_testとしてパッケージ本体と分けているのは、そうした方が、コード例にパッケージ名を記述することができて親切だからです。Goのパッケージは通常、異なるパッケージを同じディレクトリに入れることはできませんが、_testの場合は外部テストパッケージ(external test といって特別らしいです。

この例を表示させると、以下のようになります。

f:id:lufiabb:20200328212714p:plain
Exampleリスト

NewArticle関数の例は次のように表示されます。他も同じです。

f:id:lufiabb:20200328212741p:plain
Exampleの表示例

また、コード例のなかには、1つの関数で表現できない大きな例も必要になるかもしれません。その場合、1つの*_test.goファイルにExample関数を1つだけ書いて、さらに、Exampleの外に定義したなにかを1つ以上参照しましょう。パッケージグローバルに他の宣言が行われていると、GoDocはファイル全体をひとつの大きなコード例として扱います。以下はその例です。

package qiita_test

import (
    "fmt"

    "github.com/lufia/godoc-sample"
)

// グローバルに1つ以上、Example以外の何かが必要
const defaultTitle = "untitled"

// 1つだけExampleの実装がされていること
func Example_wholeFileExample() {
    a := qiita.NewArticle(defaultTitle)
    fmt.Println(a.Title)
    // Output: untitled
}

通常のExampleは、関数の中しか例として表示しませんが、このExampleはファイル全体を表示します。

f:id:lufiabb:20200328212823p:plain
Example全体例

ノート

コードの途中で、// BUG(who): xxxのようにコメントを書くと、それもドキュメントの末尾に表示してくれます。例えば、上記のSave()メソッドを次のように変更してみましょう。

// Save は、記事aの状態をデータベースに保存します。
func (a *Article) Save() error {
    // BUG(lufia): 保存機能は未実装です。
    // TODO(lufia): 実装する。
    return nil
}

これを、-notes=(正規表現)オプションを指定したGoDocで表示すると、次のように脚注として表示できます。

f:id:lufiabb:20200328212906p:plain
ノート例

GoDocのデフォルトで表示されるノートはBUG(who)だけですが、godocのオプションでgodoc -notes='.*'のようにラベルを正規表現で指定すると、BUG(who)以外のラベルも一緒に表示します。

他にも、公式ドキュメントを眺めると、

// Deprecated: xx

// See: https://xxx

などが使われていますが、これらはノートではなく普通のテキストとしてドキュメントに表示されます。

参考情報

ドキュメントを読む方法

godoc.org

オンラインのGoDocは、URLのパス部分にimport pathを与えると、パッケージのドキュメントを表示してくれるサービスです。例えばこの記事で書いたサンプルリポジトリの場合、以下のURLで参照できます。

このサービスはGOOS=linuxで動作しているようで、他のOSをターゲットとしているファイルは対象となりません。クエリパラメータとしてGOOSGOARCHがサポートされているので、例えばPlan 9/386のドキュメントを表示したければ以下のURLにアクセスすると読めます。

godocコマンド

godocコマンドは標準パッケージに含まれませんので、go getでインストールして使います。

$ go get golang.org/x/tools/cmd/godoc

このコマンドは、一般的にはgodoc -http=:6060のようにHTTPサーバとして動作させて、ブラウザで参照することが多いと思われます。ブラウザで閲覧すると、Goの公式サイトと似たようなページが表示されますが、公式と異なり、godocのパッケージ一覧ページには、ローカルの$GOPATH以下にある全パッケージがリストされています。

Go 1.11から、型や関数の右側に対応したバージョンが表記されるようになりました。このデータは $GOROOT/api/go*.txt から読み込んでいるようです。

静的解析

公式やオンラインのgodoc.orgと比べて特に便利だと思うのは、-analysis=オプションの存在です。-analysis=オプションは以下の2つの値をとります。

  • type
  • pointer

-analysis=typeとすると、型がどのインターフェイスを実装しているかを調べることができます。以下の画像はbufio.Readerの例です。io.Readerなどのインターフェイスを実装していることが確認できます。

f:id:lufiabb:20200328213011p:plain
analysis=typeの例

また、ソースコードを表示した場合には、普段より詳細な解析が行われています。こちらもbufio.Readerの例です。カーソルをリンクに乗せると、定義がその場で表示されますし、定義された場所までのリンクも追加されています。

f:id:lufiabb:20200328213031p:plain
ツールチップ

また、-analysis=pointerとすると、上記に加えて静的コールグラフ等の情報が追加されます。以下はbufio.Reader.ReadByte()が呼び出す関数のグラフです。

f:id:lufiabb:20200328213053p:plain
呼び出す関数グラフ

ソースコードを表示して、funcの部分に追加されたリンクを選択すると、その関数を呼び出しているソースコードがリストされます。以下の例はbufio.Reader.ReadByte()を呼び出している場所です。

f:id:lufiabb:20200328213106p:plain
呼び出しグラフ

コマンドラインモード

コマンドラインモードはGo 1.11が最終バージョンです。以後サポートされなくなります。

godocは、ブラウザで使う以外にも、コマンドラインでドキュメントを読むためにも使うことができます。コマンドラインモードの使い方は

$ godoc [options] full/path/to/pkg [name ...]

のように引数としてパッケージと名前(省略可能)を受け取ります。godocは、nameを省略すると、パッケージに含まれる全てのドキュメントをコンソールに出力します。nameを与えると、その部分だけ抽出します。注意すべき点として、パッケージのimport pathは必ず完全なimport pathで渡す必要があります。

コマンドラインモードのgodocは、-srcオプションを与えると、対象のソースコードを標準出力に書き出します。他にも-qオプションを使うと、検索することも可能です。

$ godoc -src sync WaitGroup # WaitGroupのソースを表示

$ godoc -q 'Buffer' # BufferをGoDocから検索

参考情報

go docコマンド

goコマンドのサブコマンドです。godocコマンドラインモードと利用する場面は似ています。godocよりも機能はシンプルで、コマンドラインでのドキュメント出力しかできませんが、引数の渡し方が少し便利になっています。例えばgodocはパッケージをフルパスで渡す必要がありましたが、go doc

$ go doc appengine.Context

のように、パッケージが一意に特定できるならimport pathの一部だけを渡せば検索してくれます。また、特定メソッドだけを調べる場合、以下のように.でつなげて引数で渡すと、メソッドのドキュメントだけを絞り込んで表示します。

$ go doc oauth2.Config.AuthCodeURL

参考情報

その他

この記事ではパッケージのドキュメント例を紹介しましたが、他にも、presentコマンドを使うと、Go関連のスライドでよく使われる形式のスライドが作れたり、記事が作成できたりします。

golang.org/x/oauth2で色々な認可フローや方言に対応する

この記事はQiitaで公開されていました

OAuth 2.0クライアントの実装

golang.org/x/oauth2を使ってトークンを発行する際に、複数の認可フローや方言があるけれど、どうやって実装するんだろう?と思ったことを調べました。OAuth 2.0自体については、OAuth 2.0の仕組みと認証方法が、どのようなレスポンスが返却されるのか記載されているので理解しやすいと思います。

アクセストークンの発行方法

OAuth 2.0には、リダイレクトを伴うものだけではなく、いくつかの認可フローが存在します。各フローの説明は、色々な OAuth のフローと doorkeeper gem での実装がわかりやすいと思います。

  • Authorization Code
  • Client Credentials
  • Resource Owner Password Credentials
  • Implicit Grant

以下で、golang.org/x/oauth2を使うと、それぞれの認可フローではどのようにhttp.Clientを取得するのか、をサンプルとして紹介します。

Authorization Code

ブラウザでリダイレクトを行うタイプのトークン取得方法です。3-legged OAuth 2.0と呼ばれます。この認可フローでは、grant_typeauthorization_codeです。この場合、さらにオンラインとオフラインアクセスで微妙に異なります。以下はオフラインアクセスの例です。

conf := oauth2.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://provider.com/o/oauth2/auth",
        TokenURL: "https://provider.com/o/oauth2/token",
    },
}
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)

// ...urlをブラウザで開いて認可コードを取得...

authCode := "AUTH_CODE"
token, err := conf.Exchange(context.Background(), authCode)
if err != nil {
    log.Fatal(err)
}
client := conf.Client(context.Background(), token)

余談ですが、ユーザにコピペさせずにoauth2.Config.RedirectURLで工夫してアクセストークンを取得する方法もあるそうです。

Client Credentials

少し簡略化したトークン取得方法です。2-legged OAuth 2.0と呼ばれます。このためには、golang.org/x/oauth2/clientcredentialsという別のパッケージが必要です。この認可フローでは、grant_typeclient_credentialsです。

conf := clientcredentials.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    TokenURL:     "https://provider.com/o/oauth2/token",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
}
client := conf.Client(context.Background())

上記の例では直接http.Clientを取得していますが、clientcredentials.Config.Token(ctx)を使うとトークンも取得できます。

Resource Owner Password Credentials

ユーザ名とパスワードを使ったトークンの取得方法です。この認可フローでは、grant_typepasswordです。

conf := oauth2.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
    Endpoint: oauth2.Endpoint{
        TokenURL: "https://provider.com/o/oauth2/token",
    },
}
token, err := conf.PasswordCredentialsToken(ctx, "USER", "PASSWORD")
if err != nil {
    log.Fatal(err)
}
client := conf.Client(ctx, token)

Authorization Codeと異なりoauth2.AuthCodeURL()を呼び出さないため、EndpointAuthURLは不要です。

Implicit Grant

これはよくわかりません。

任意のヘッダを追加したい

認可サーバの仕様によっては、HTTPヘッダが必須になる場合があります。golang.org/x/oauth2にはカスタムしたHTTPリクエストを認証フローで使わせる方法があるので、これを使って行います。具体的には、context.Contextの値に、oauth2.HTTPClientというキーでカスタムしたhttp.Clientをセットしておくという形になります。

type oauthTransport struct{}

func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
    r.Header.Set("User-Agent", "MyApp/1.0.0")
    return http.DefaultTransport.RoundTrip(r)
}

func main() {
    c := &http.Client{Transport: &oauthTransport{}}
    ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
    conf := &oauth2.Config{...}
    token, err := config.PasswordCredentialsToken(ctx, "USER", "PASSWORD")

任意のパラメータを追加したい

試していませんが、認可コード取得時ならoauth2.SetAuthURLParam()が使えると思います。

Authorizationヘッダではなくペイロードを使う

OAuth 2.0でクライアントシークレットを認可サーバへ渡す方法は2通り存在します。一つはBasic認証のヘッダで渡す方法。もう一つはリクエストのペイロードで渡す方法です。golang.org/x/oauth2は、通常はBasic認証の方法を使いますが、一部のペイロードを要求する既知のサービスはペイロードとして渡すようになっています。

独自または無名の認可サーバは、Basic認証で動作してしまうため、これが不都合な場合はoauth2.RegisterBrokenAuthHeaderProvider(tokenURL)ペイロードとして渡されるようにURLを登録する必要があります。これは前方一致でマッチします。

func init() {
    oauth2.RegisterBrokenAuthHeaderProvider("https://provider.com/")
}

JSONの形式が微妙に異なる場合に対応する

golang.org/x/oauth2パッケージは、トークン取得時のレスポンスに含まれるペイロードがフォームエンコードまたは以下のような形式のJSONであることを要求します。

{
  "access_token": "xxx",
  "token_type": "bearer",
  "refresh_token": "xxx",
  "expires_in": 3600
}

そのため、以下のようなJSONは、そのままでは扱うことができません。

{
  "auth_param": {
    "access_token": "xxx",
    "token_type": "bearer",
    "refresh_token": "xxx",
    "expires_in": 3600
  }
}

この場合、上で書いたヘッダを追加する方法と同じように、http.RoundTripperで加工したものを返してあげる必要があります。

type oauthTransport struct{}

type tokenJSON struct {
    Auth json.RawMessage `json:"auth_param"`
}

func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
    resp, err := http.DefaultTransport.RoundTrip(r)
    if err != nil {
        return resp, err
    }
    var v tokenJSON
    decoder := json.NewDecoder(resp.Body)
    if err := decoder.Decode(&v); err != nil {
        return resp, err
    }
    resp.Body.Close()
    resp.Body = ioutil.NopCloser(bytes.NewReader(v.Auth))
    resp.ContentLength = int64(len(v.Auth))
    return resp, err
}

json.RawMessageは値を[]byteのまま扱う型です。JSONの一部だけをそのまま扱いたい場合にとても便利です。

macOSでNixpkgsのアップグレード

この記事はQiitaで公開されていました

TL;DR

NixpkgsのmacOSインストーラはマルチユーザインストーラなので、rootユーザにインストールされているnixパッケージのアップデートが必要。

$ sudo -i nix-env -q
nix-2.0
nss-cacert-3.35
$ nix-env --version
nix-env (Nix) 2.0

モチベーション

最近nix-channel --updateした環境では、nix-env -qaするとエラーになってしまう。

$ nix-env -qa ct
error: undefined variable ‘placeholder’ at /nix/store/f81z(snip)ba37/nixpkgs/pkgs/development/libraries/pipewire/default.nix:37:46

これは、Nix 2.0でplaceholder変数が導入されたけれど、Nix 1.11以前には存在していないため発生するエラーなので、先にNix自体をアップグレードする必要がある。しかしbrew upgradeのようなそれ自身をアップグレードするコマンドは用意されていない。

現在、macOS版のNixpkgsはマルチユーザ用にインストールされているので、Nixpkgs自体をアップグレードするには、rootnixパッケージを更新する必要がある。

$ sudo -i su -
root# nix-channel --update
root# nix-env -iA nixpkgs.nix
root# launchctl stop org.nixos.nix-daemon
root# launchctl start org.nixos.nix-daemon

sudo -i nix-channel --updateでも良さそうに思えるが、

error: unable to download ‘https://d3g5gsiof5omrk.cloudfront.net/nixpkgs/nixpkgs-18.09pre133932.ee28e35ba37/nixexprs.tar.xz’: Problem with the SSL CA cert (path? access rights?) (77)

というエラーでうまく動作しなかった。

Go & Versioning(vgo)を読んで大きな変更が入ったなと思った

この記事はQiitaで公開されていました

このQiita記事は、Go & Versioningで掲載された一連の記事を読んで、自分なりのまとめと感想です。私の周りはあまり騒いでないけど、これ感覚的なものが大きく変わるなあ、と思ったので、主に表面上に現れる変更をまとめました。

これは、Go 1.11で試験的な導入、Go 1.12で正式サポートとなる予定の機能に関する話です。

@nekketsuuuさんが原文の和訳をされています。

何が変わるのか

バージョン管理機能の導入

goコマンドにバージョン管理の機能が追加されます。バージョンは常にセマンティックバージョニングで表します。今もdepコマンドが(goとは別に)存在しますが、バージョン管理機能が追加されたgoコマンド(以下vgoと表記)は、depglideなどの依存管理ツールとは別のアプローチでバージョンを管理します。とはいえ、目的はどちらもバージョン管理なので、vgoを使う場合はdepを使いません。また、vgovendorディレクトリを使わないためvendorも不要です。

モジュールという単位の追加

バージョン管理機能に伴って、Goのパッケージにモジュールという単位が追加されます。モジュールは複数のパッケージをまとめたもので、go.modというファイルで管理します。Go & Versioningでは以下の例が記されていました。

// My hello, world

module "rsc.io/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
)

モジュールは、vgoでバージョン管理を行う基本的な単位となります。1つのリポジトリが1つのモジュールに該当し、タグ(git tag)を使ってモジュールにバージョンを与えます。従って、バージョン管理を行う単位はモジュールです。例えば、上のgo.modでリストされているgolang.org/x/textrsc.io/quoteもモジュールです。今まではパッケージという扱いでしたが、モジュールに変わります。

モジュールのアップデートはvgo get

モジュールはgo.modファイルでバージョンを指定しますが、これを人が維持するのは大変です。vgo getで必要なモジュールを追加したり、vgo get -uでアップデートを行ったりするようです。

たくさんのExampleが書かれているのでA Tour of Versioned Goを眺めてみてください。これだけで雰囲気はつかめると思います。

破壊的変更を加える場合はimport pathを変更する

vgoで扱うパッケージは、全て後方互換性を持たなければなりません。例えばAというパッケージの作者は、メジャーバージョンが同じ間は後方互換性を維持する必要があります。この制約によって、v1.1.0を参照しているプログラムは、v1.2.1が使われた場合でも同じように動作することが保証されます。もしマイナーバージョンのアップデートでビルドが壊れた場合、Av1.2.2で過去の互換性を取り戻すべきです。例えば、関数の動作を変更する場合は、仕様変更ではなく名前を変えて新しい関数として追加しましょう。

とはいえ、どうしても破壊的な変更が避けられないケースは存在します。その場合はimport pathを変更することで別のパッケージとして作成してください。例えば、lufia.org/pkg/xに破壊的変更を加える場合、新しいバージョンはlufia.org/pkg/x/v2のようにメジャーバージョンを含むimport pathにしましょう。そうするとv1v2で重複する部分がソースコードの二重管理になりがちですが、それはtypealias等を使って頑張ってください。

最初からimport pathをlufia.org/pkg/x/v1のようにするべきかについては、最終的にどうなるかは分かりませんが、個人的には「最初はバージョンを含めない」でいいと思います。

$GOPATHが不要になる

これまで$GOPATHはGoワークスペースのルートとして必要でしたが、vgoではgo.modによってモジュールのURLやバージョンが明確に特定できるため、$GOPATHがなくてもソースコードの取得やビルドの再現性には困りません。参照するモジュールのマイナーバージョンは上がるかもしれませんが、マイナーバージョンの変更はモジュール作者の努力によって互換性が維持されるため、最終的な動作は変わらないことが保証できます。

これによって、任意のディレクトリでGoのコードを書くことが可能となります。今までのように、$GOPATH配下にワークスペースを構築する必要は無くなります。

ただこれ疑問なのは、今までの$GOPATH直下には

$ ls $GOPATH
bin pkg src

が存在していて、まあsrcpkgは無くてもいいかなと思うのですが、go getでインストールする場所としての$GOPATHはどうなるんだろう?と思いました。

何が変わらないのか

今までのコードはそのままビルド可能

vgoだからといって今までのコードが壊れることはありません。$GOPATHvendorも、不要になるだけで、そのまま使えます。go.modがないリポジトリもそのままビルドできるはずです。

depvgoが普及するまでは残る

長期的にみればdepは無くなるのかもしれませんが、少なくともvgoがリリースされて、十分に普及するまでは継続することが書かれていました。今すぐ何かが変わることはないので、そのまま使い続けても問題ないと思います。

今後の話

vgoは、この記事の最初にも書きましたけれど、Go 1.11で試験的な導入、Go 1.12で正式サポートとなる予定です。今はまだGo 1.10がリリースされたばかりなので何もしなくても良いと思いますが、半年後にGo 1.11がリリースされたら、特にパッケージ作者は以下のことに注意しましょう。

  • go.modを作成しましょう
  • セマンティックバージョニングでタグを打ちましょう
  • import pathが同一である限り後方互換性を維持しましょう