Plan 9とGo言語のブログ

主にPlan 9や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が同一である限り後方互換性を維持しましょう

golang.org/x/sync/semaphoreを使ってゴルーチンの同時実行数を制御する

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

特定の処理をゴルーチンで並列実行したいけれど、サーバの負荷等を考慮して、同時実行数の上限を設定したい話です。元ネタの記事では、チャネルやsync.Poolを使って実現していて、すでに十分シンプルなのですが、x/sync/semaphoreを使う方法も便利だったので紹介します。

見た目はほぼ、チャネルを使った実装と同じですが、s.Acquire(ctx, n)nの値で重みをつけることができます。なので、Aという処理が動いているときは他の処理を行わない、けれどBなら3個まで同時に動いても良い、といった対応をチャネルで行うと面倒ですが、semaphore.Weightedなら重みを変更するだけで実現できるので便利だと思いました。

元ネタ

実装例

以下の例は、同時実行数が3つに制限された状態でdoSomething(u)を並列実行します。サンプルコード自体はmattnさんのものをほぼそのまま流用しました。

全ての処理完了を待つためにsync.WaitGroupを使っていますが、semaphore.Weightedには全く関係ありません。

2024-04-10追記 sync.WaitGroup を使わなくても、最後にs.Acquire(Limit)すると完了を待てるので、そっちのほうがシンプルですね。

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

func doSomething(u string) {
    fmt.Println(u)
    time.Sleep(2 * time.Second)
}

const (
    Limit  = 3 // 同時実行数の上限
    Weight = 1 // 1処理あたりの実行コスト
)

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.net/foo",
        "http://www.example.net/bar",
        "http://www.example.net/baz",
    }
    s := semaphore.NewWeighted(Limit)
    var w sync.WaitGroup
    for _, u := range urls {
        w.Add(1)
        s.Acquire(context.Background(), Weight)
        go func(u string) {
            doSomething(u)
            s.Release(Weight)
            w.Done()
        }(u)
    }
    w.Wait()
}

説明

まずは、semaphore.NewWeighted(lim)lim個のリソースをもつsemaphore.Weightedを作成します。s.Acquire(ctx, n)は、全体のリソース(lim)からn個消費しますが、Weightedのリソースが足りない場合は、s.Acquire(ctx, n)の呼び出しは他のゴルーチンからs.Release(n)されるまでブロックします。そのため、同時にlim個以上の処理が動くことはありません。

処理が終わった後s.Release(n)を使うと、n個のリソースをWeightedへ戻します。ブロックしていたs.Acquire(ctx, n)があれば、ブロックが解除されて続きの処理を行います。

また、s.TryAcquire(n)というメソッドも用意されていて、こちらはブロックしません。代わりに、リソースが取得できたらtrueを返します。

Jenkinsfileの書き方

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

ほとんど情報がなかったので調べながら書いたメモ。基本的に、公式情報が最もまとまっている。

また、古い情報らしいけど、現状こちらにしか書かれていない項目もある。

Declarative Pipelineの1.2.8から、ジェネレータが追加されたようです。

基本

現状のJenkinsfileは2通り書き方があって、pipelineがルートになっている場合はDeclarative Pipelineという。この場合は、Groovyスクリプトを直接書くことができず、Groovyを書きたければscriptディレクティブを使う必要がある。この記事では主にこちらを扱う。

pipelineから始まらない場合、Scripted Pipelineといって、この場合は直接Groovyスクリプトも書けるし、node()stage()などの、Pipeline Stepsメソッドも書くことができる。便利そうにみえるけど自由度が高すぎて職人コードになりがち。

Declarative Pipelineの書き方

Jenkinsfileは特定の条件ごとにディレクティブを書いて実装する。ディレクティブの出現順は以下の順番になっている。例えばstagesのすぐ下にagentを書くことはできない。

pipeline {
    // 1行コメント
    /*
    * 複数行コメント
    */
    agent { ... }
    environment { ... }
    options {
        buildDiscarder(...)
        checkoutToSubdirectory(...)
        disableConcurrentBuilds(...)
        disableResume()
        newContainerPerStage(...)
        overrideIndexTriggers(...)
        preserveStashes(...)
        quietPeriod(...)
        retry(...)
        skipDefaultCheckout(...)
        skipStagesAfterUnstable(...)
        timeout(...)
        timestamps()
        parallelsAlwaysFailFast(...)
    }
    parameters {
        string(...)
        booleanParam(...)
        choice(...)
        file(...)
        text(...)
        password(...)
        run(...) // ??
    }
    tools {
        maven '...'
        jdk '...'
        gradle '...'
    }
    triggers {
        cron(...)
        pollSCM(...)
    }
    stages {
        stage {
            agent { ... }
            environment { ... }
            tools { ... }
            options {
                skipDefaultCheckout(...)
                timeout(...)
                retry(...)
                timestamps()
            }
            when { ... }
            steps {
                //Pipeline Steps Reference参照
                echo 'help'
                sh 'ls'
                script {
                    // 任意のGroovyスクリプト
                }
            }
        }
    }
    steps {
        //Pipeline Steps Reference参照
    }
    post {
        always { ... }
        success { ... }
        failure { ... }
        ...
    }
}

stepsだけの場合は、直接stepsの中に書くものを記述できる。

echo 'help'
sh 'ls'

node()のようにstepsディレクティブの中で子ブロックが現れる場合、そのブロックの中はstepsディレクティブと同じものが書ける。

steps {
    node('slave1'){
        sh 'ls'
        dir('output'){
            sh 'ls'
        }
    }
}

個別の事例

stageの書き方

stage()を使うと、進捗を分けて表示することができるが、インターネットでは2種類の書き方がある。stage単体で書かれているものは古く、推奨されない。

stage 'first'
sh 'ls'

stage 'second'
sh 'pwd'

今はブロックを取るようになっていて、こちらが推奨される。

stage('first'){
    sh 'ls'
}
stage('second'){
    sh 'pwd'
}

ビルドするノードを制限する

agentディレクティブでラベルを指定する。

agent {
    label 'slave1'
}

または、stepsディレクティブでnodeブロックを使う。

steps {
    node('slave1'){
    }
}

環境変数をセットする

environmentディレクティブが使える場合はその中で書く。ここでは、定数またはcredentials()しか使えない。また、credentials()SSHユーザ名と秘密鍵を扱えない。

environment {
    GOPATH = '/home/jenkins/go'

    // Jenkinsの資格情報に登録されている値を取り出す
    TOKEN = credentials('credential_id')
}

credentials()で取り出したものは、シークレットテキストの場合はそのまま使える。ユーザ名とパスワードの場合、TOKENはユーザ名とパスワードを:で区切った文字列になっている。個別に使いたい場合は、TOKEN_USRTOKEN_PSWのようにサフィックスを付けて扱うと良い。

または、stepsディレクティブの中でwithEnvブロックやwithCredentialsブロックを使う。withEnvの中でだけ環境変数が有効になる。

steps {
    withEnv(["GOPATH=${env.WORKSPACE}"]) {
    }
    withCredentials(bindings: [
        // シークレットテキスト
        string(credentialsId: 'credential_id', variable: 'TOKEN'),

        // ユーザ名とパスワード
        usernamePassword(credentialsId: 'credential_id',
         passwordVariable:'PASS',
         usernameVariable:'USER'),

        // ファイル
        file(credentialsId: 'credential_id', variable: 'FILE'),

        // SSH秘密鍵: passphraseVariable, usernameVariableは省略可
        sshUserPrivateKey(credentialsId: 'credential_id',
         keyFileVariable: 'KEY_FILE',
         passphraseVariable: '',
         usernameVariable: '')
    ]){
    }
}

withCredentials()の詳細はCredentials Binding Pluginを読むと良い。

Groovyを使ってもっと細かく制御したい場合。stepsディレクティブに直接Groovyは書けないのでscriptディレクティブを使う。

steps {
    script {
        // ここからGroovy
        env.GOPATH = env.WORKSPACE
    }
}

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

Jenkinsの管理メニューにGlobal Tools Configuration があり、そこで事前にMavenやGoコンパイラなどのバージョンと名前を定義しておくと、toolsディレクティブで、自動でインストールすることができる。

pipeline {
    tools {
        go 'go1.10'
    }
}

Pipeline Syntaxで自動生成したコードはtool name: 'go1.9', type: 'go'のように出力されるが、このままDeclarative Pipelineとして記述するとエラーになる。$type '$name'のように置き換えて書く必要がある。

パラメータ付きビルドにする

parametersに何か書くと、パラメータ付きビルドとして扱われる。params.nameで参照する。

pipeline {
    parameters {
        string(name: 'TARGET', defaultValue: 'Tests', description: '説明')
        choice(name: 'STAGE', choices: 'staging\nproduction', description: '説明')
    }
    stages {
        stage('build'){
            sh "./make.bash ${params.TARGET}"
        }
    }
}

実行条件を設定する

stage()ブロックでwhenディレクティブを使う。以下の例は、ブランチ名がrelease-*にマッチした場合のみstageを実行する。

stage('stage'){
    when {
        branch 'release-*'
    }
}

他にも、環境変数の値を条件としたり、全て一致またはどれかが一致などの条件を作ることができる。

複数行のシェルスクリプトを実行

'''または"""で囲むと複数行のテキストを記述できる。

steps {
    sh '''
  date
  sleep 10
  date
  '''
}

'''の場合は変数展開などをせずにそのままシェルへ渡す。"""なら${env.PATH}のような展開を行う。

GitHub Branch Source Plugin環境でスレーブを使ってビルドする

基本的には何もしなくてもプラグインがcloneするが、デフォルトでは(おそらく)Jenkinsマスターのワークスペースへcloneされる。このため、ビルドノードがマスター以外の場合、ビルドするソースコードがスレーブのワークスペースにない状態となる。

stepsディレクティブでcheckoutを使うとslaveのワークスペースへcloneが行われる。

steps {
    checkout scm
}

または最初から、agentディレクティブでラベルを指定しておくと、対象となったスレーブでcloneされる。

pipeline {
    agent {
        label 'slave1'
    }
    steps {
        sh 'ls'
    }
}

特定のディレクトリへcloneする

optionsディレクティブで指定する。

options {
    checkoutToSubdirectory 'src/v2'
}

または、dir()を使うとカレントディレクトリを変更できるので、移動した後でcheckout scmを実行すればいい。移動するディレクトリが存在しない場合は自動的に作成される。

stage('chekout'){
    steps {
        dir("src/v2"){
            checkout scm
        }
    }
}

GitHub Branch Source Pluginでcheckoutの動作を変更する

How to Customize Checkout for Pipeline Multibranchによると、checkout scmは、

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions,
 userRemoteConfigs: scm.userRemoteConfigs
])

の省略形らしい。Shallow cloneとかcloneするディレクトリを変更する場合は、scm.extensionsに配列でオプションを追加する。

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions + [
        [ $class: 'CloneOption', noTags: false ],
        [ $class: 'RelativeTargetDirectory',
       relativeTargetDir: "src/v2"
        ],
    ],
 userRemoteConfigs: scm.userRemoteConfigs
])

Gitサブモジュールを使う

checkout scmSubmoduleOptionをセットします。

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

ただし、GitHub Branch Source Pluginを使う場合、parentCredentialsGitHub APIトークンが使われているため、サブモジュールの参照もHTTPで行う必要があります。

checkoutの前にワークスペースをクリアする

checkout scmの前にdeleteDirを使う。

stage('clean'){
    steps {
        deleteDir()
    }
}
stage('checkout'){
    steps {
        checkout scm
    }
}

Scripts not permitted to use methodエラーで動作しない

Jenkinsfileのコードを実行した時、

RejectedAccessException: Scripts not permitted to use method (メソッド名)

というエラーで停止する場合がある。これは、外部のコードを無条件に実行すると危険なので、Script Security Pluginによってsandbox実行されているため、らしい。

外部のコードを制限するための機能なので、Jenkinsfileで回避できるものではない。エラーが発生した後であれば、Jenkinsの管理画面にエラーとなったメソッドの許可を求めるログが出ているので、そこでApprovalボタンを押せば次からはエラーを回避できる。このファイルは$JENKINS_HOME/scriptApproval.xmlに置かれているので、これをコピーしてもいい。

成果物を保存する

postセクションでArchive Artifact Pluginを使えばよい。

pipeline {
    post {
        success {
            archiveArtifacts artifacts: bin/*, fingerprint: true
       }
   }
}

成果物の保存数を制限する

いろいろ書き方はあるが、おそらくoptionsディレクティブを使うのが簡単。

pipeline {
    options {
        buildDiscarder(logRotator(numToKeepStr: '5', daysToKeepStr: '7', artifactNumToKeepStr: '5'))
    }
}

並列ビルドする

stepsparallelを使う。

steps {
    parallel(
     linux: {
            sh './make.bash -t linux_amd64'
        },
     windows: {
            sh './make.bash -t windows_amd64'
        }
    )
}

この例では、./make.bash -t linux_amd64./make.bash -t windows_amd64が並列実行される。

他のジョブをビルドする

buildを使う。

引数のパラメータはJenkinsを置いているフォルダまでのパスを渡す。相対パスの場合は呼び出し元ジョブのディレクトリがカレントになり、絶対パスの場合は$JENKINS_HOME/jobsがルートになる。

steps {
    build '../jobName1'
}

マルチブランチ構成のプロジェクトを呼び出す場合は、内部の階層がブランチごとに切られているので、ブランチ名も必要。

steps {
    build '../jobName1/master'
}

GitHub Branch Sourceが管理しているジョブはマルチブランチに近くて、Organizationの下にジョブが作られるので、次のようになる。

steps {
    build '/organizationName/jobs/jobName/master'
}

Jenkinsが管理しているCredentialでssh接続したい

ssh-agentプラグインをインストールするとsshagentブロックが利用可能になる。このブロックの中に記述したコマンドは、Jenkinsが管理している秘密鍵が追加されたssh-agentが動作してる状態で実行される。なのでJenkinsのアカウントでgit pushしたい場合は、以下のように書く。

steps {
    // jenkins_credential_id_strという名前のCredentialを読み込む
    sshagent(['jenkins_credential_id_str']){
        sh 'git push'
    }
}

bashのあまり知られていないけど便利な話

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

~*を展開する場合はクオートしない

クオートされた文字列の中にある~はそのまま文字として扱われる。扱いは*と同じようにみえる。

$ echo ~/bin
/Users/kadota/bin
$ echo "~/bin"
~/bin

$ echo *.txt
hello.txt
$ echo "*.txt"
*.txt

$*$@の違い

基本的には同じ動きをするが、クオートの中で使うと文字区切りの扱いが変わる。

# star.sh
for i in $*; do echo "'$i'"; done

# star-quote.sh
for i in "$*"; do echo "'$i'"; done

# atom.sh
for i in $@; do echo "'$i'"; done

# atom-quote.sh
for i in "$@"; do echo "'$i'"; done

スペース($IFS)を含む場合の例。

$ bash star.sh 1 "2 3" 4
'1'
'2'
'3'
'4'
$ bash atom.sh 1 "2 3" 4
'1'
'2'
'3'
'4'
$ bash star-quote.sh 1 "2 3" 4
'1 2 3 4'
$ bash atom-quote.sh 1 "2 3" 4
'1'
'2 3'
'4'

間接参照

rcでは変数の値を変数名として使うと便利だけど

% a=foo
% foo=1
% echo $$a
1

bashはそのまま実行すると、$$が先に解決されてPIDになってしまう。

$ a=foo
$ foo=1
$ echo $$a
1136a

正しくはこちら。

$ echo ${!a}

[[[test

シェルで分岐をする場合によく使うこれ。[testと同じコマンドなのでどちらを使ってもいい。

# if test $a -gt 1でも同じ

if [ $a -gt 1 ]
then
    echo $a greater than 1
else
    echo $a less than or equal to 1
fi

だけどこの2つはシェルとは別のコマンドなので、空白文字や空文字の場合に注意しないといけなくてめんどくさい。

$ a=""
$ [ $a = "" ] # unary operater expectedエラー
$ [ "$a" = "" ] # ok
$ a="1 2"
$ [ $a = "" ] # too many argumentsエラー
$ [ "$a" = "" ] # ok

bashを使うのであれば、今は常に[[を使うのが良いと思う。

$ a=""
$ [[ $a = "" ]]
$ a="1 2"
$ [[ $a = "" ]]

正規表現でマッチ

[[=~演算子を使うと正規表現が使える。

text='a b c'
if [[ $text =~ ^a.*$ ]]
then
    echo match
fi

ここでは、安易に正規表現をクオートしてはいけない。クオートすると、正規表現で特別な扱いされる文字が全てバックスラッシュでエスケープされる。シングルクオートとダブルクオートでどちらも同じ。

[[ $text =~ '^a.*$' ]] # \^a\.\*\$と同等

正規表現の途中でスペース等を含ませたい場合は、その部分だけクオートするとよい。

[[ $text =~ ^'a b'.*$ ]]

実際にどのようなエスケープがされるのかは、set -xしておくとエスケープされた後の正規表現を見ることができる。

if&&||

ifの代わりに、&&||が使える。エラー処理はこちらを使った方が読みやすい。

$ test -f file || echo 'file not exist' >&2 && exit 1

配列

簡単な使い方。

# 初期化
a=()

# 末尾に追加
a=("${a[@]}" test)

# 先頭に追加
a=(test "${a[@]}")

# 内容を列挙
echo "${a[@]}"

単純にecho $aすると、先頭の要素しか対象にならない。

配列に特定の要素が存在するか

専用の機能は無いが、[[コマンドの=~演算子が使える。

a=(cafe beef)
if [[ " ${a[@]} " =~ " cafe " ]]
then
    echo found
fi

=~正規表現でマッチさせる演算子なので、スペースで要素の区切りを表現してあげなければならない。

計算する

$((10 + 1))のように書くと外部コマンドなく計算ができる。

$ N=10
$ echo $((10 + $N))
20
$ echo $((1 * 3 - 1))
2

数字の前に基数を書ける。

$ echo $((16#0F + 8#10))
23

((expr))だけの場合は評価をするだけなので、条件式に使うと便利。

$ ((10 > 8#10)) && echo ok
ok

trap

trap は、SIGINT, SIGHUP などの他に以下の擬似シグナルが存在し、それらにハンドラを割り当てられる。

擬似シグナル 意味
ERR コマンド実行結果がエラーなら都度実行される
EXIT コマンドを終了したときに実行される
DEBUG コマンド実行のたびに都度実行される

一時ファイルを必ず削除したい場合はEXITに登録しておくと良い。利用可能なシグナルはtrap -lで表示できる。

変数展開

bashは通常のshと比べてとても多くの展開式が使える。

変数置換

古くからある展開式。

書き方 意味
${v:-text} vが未定義ならtextに置換
${v:=text} vが未定義ならtextをセットして置換
${v:?text} vが未定義ならtextを表示して終了
${v:+text} vが 定義されていたら textに置換

変数の少し変わった扱い方。

書き方 意味
${#v} 文字列の長さ、または配列の要素数に展開
${!v} vの値を変数名として展開

文字列置換

書き方 意味
${v%glob} vの末尾から最短マッチした部分を削除
${v%%glob} vの末尾から最長マッチした部分を削除
${v#glob} vの先頭から最短マッチした部分を削除
${v##glob} vの先頭から最長マッチした部分を削除
${v/s1/s2} vに含まれるs1を1つだけs2に置換
${v//s1/s2} vに含まれるs1を全てs2に置換

bash 4以降から利用可能な式

便利な式だけど、macOS High Sierraはまだ3系なので利用箇所には注意。

書き方 意味
${v^} 先頭の文字を大文字にして展開
${v^^} 全ての文字を大文字にして展開
${v,} 先頭の文字を小文字にして展開
${v,,} 全ての文字を小文字にして展開
${v~} 先頭文字の大文字小文字を反転
${v~~} 全ての文字で大文字小文字を反転

オプションを扱う

色々方法はあるけどgetoptsがいちばん面倒がなくて良いと思う。以下のコードでは、-fに引数がない・不明なオプションが与えられた時に、getoptsがエラーを出してくれる。

while getopts xf: OPT
do
    case $OPT in
    f)    filename="$OPTARG" ;;
    x)    flagx=1 ;;
    esac
done
shift $((OPTIND - 1))

だけど不明なオプションでもエラーとしたくない場合は少しめんどくさい。getopts最初の引数先頭に:をつけるとエラーにならない代わりに、特殊なオプション文字として:または?のどちらかが入っている。

文字 意味
? 不明なオプション文字が使われた
: 引数が必要なオプションだけど無かった

このように使う。

while getopts :xf: OPT
do
    case $OPT in
    :)  echo usage: $(basename $0) [-x] [-f file] >&2
        exit 1 ;;
    x)  flagx=1 ;;
    \?) x=$((OPTIND - 1))
        echo unknown option: ${!x} >&2
        exit 1 ;;
    esac
done
shift $((OPTIND - 1))

不明なオプションだった場合、具体的にどの文字が使われたのか調べる方法は特に用意されてなさそうだったので、上記のように自分で計算する。

先頭のアレ

シバン(shebang)というらしい。

#!/usr/bin/env bash

bashを使う場合、環境によってパスが違うかもしれないのでenvでラップするのをよく見かける。