YAMAGUCHI::weblog

海水パンツとゴーグルで、巨万の富を築きました。カリブの怪物、フリーアルバイター瞳です。

golang.org/x/text/messageでI18N

はじめに

こんにちは、Stackdriver担当者です。この記事は Go Advent Calendar 2018 *1の最終日のエントリです。昨日は @yasuo-ozuさんの「Go言語は沼」 でした。

ところで今日はクリスマスですね。自分宛も含めてまだプレゼントを送っていない方はこの本を送るのがおすすめです。

Go言語による並行処理

Go言語による並行処理

年末年始休暇に読んでもらってGo言語による並行処理への理解を深めてもらいましょう!

さて、今日は準標準パッケージの "golang.org/x/text/message" の紹介です。本文に出てくる雑なサンプルのリンクを貼っておきます。

"golang.org/x/text/message" とは

godoc.org

Go準標準パッケージ内にある、国際化のためのパッケージです。履歴を見ればおわかりの通り、非常に地味に更新が続いているパッケージです。

Log - master - text - Git at Google

このパッケージは大きく分けて2つの使い方があって

  • フォーマットのローカライゼーション
  • メッセージの翻訳

の2種類があります。どちらもメッセージの出力をする際に fmt パッケージでなく、 message.Printer を使うようにするところがポイントです。

フォーマットのローカライゼーション

これはドキュメントのサンプルにあるとおりで、プリセット(CLDR, Common Locale Data Repositoryに準拠)で用意されているフォーマットを利用して、数字の3桁区切りやその区切り文字、通貨記号の取扱も可能です。*2

これらはパッケージの目的として golang.org/x/text 以下にあるデータフォーマットにはすべて対応するという目標があるようです。(まだやってない)

import (
    "golang.org/x/text/currency"
    "golang.org/x/text/language"
    "golang.org/x/text/message"
)

func localization() {
    jp := language.Japanese
    p := message.NewPrinter(jp)
    cur, _ := currency.FromTag(jp)
    // クリスマスには¥ 120000のiPadを買いました。(フォーマット引数が%dなのに通貨記号が入っているのがミソ。しかしカンマ区切りが反映されない)
    p.Printf("クリスマスには%dのiPadを買いました。\n", currency.NarrowSymbol(cur.Amount(120000.0)))
    // お年玉は10,000円あげるつもりです。(カンマ区切りが反映されている)
    p.Printf("お年玉は%d円あげるつもりです。\n", 10000)
}

メッセージの翻訳

正直ローカライゼーションのほうはまだまだ改善の余地ありという感じですが、一番良く使われるのはこちらのメッセージの翻訳機能の方でしょう。こちらは使い方が単純です。

ベースとなるフォーマット文字列をキーとして、各言語ごとに翻訳版のフォーマット文字列を指定するという形です。(message.Setmessage.SetString を使う) 指定した文字列は golang.org/x/text/message/catalog#Catalog に追加されていくだけので、アプリケーションなどで使う場合には独自のカタログを作っておくと良いでしょう。(例: エラーログメッセージなど)

import (
    "golang.org/x/text/language"
    "golang.org/x/text/message"
)

func init() {
    message.SetString(language.Japanese, "%d days to the new year day.\n",
        "新年まであと%d日\n")
    message.SetString(language.Japanese, "%s, I wish you a happy new year.\n",
        "%s、良いお年を\n")
    // フォーマット引数の順番を入れ替える場合には"[]"を使って指定する
    message.SetString(language.Japanese, "%s, %s\n", "%[2]s%[1]s\n")
}

func translation() {
    p := message.NewPrinter(language.Japanese)
    local, _ := time.LoadLocation("Local")
    nyd := time.Date(2019, 1, 1, 0, 0, 0, 0, local)
    days := nyd.Sub(time.Now()) / (time.Hour * 24)
    // 新年まであと6日
    p.Printf("%d days to the new year day.\n", days)
    // みなさん、良いお年を
    p.Printf("%s, I wish you a happy new year.\n", "みなさん")
    // 世界、こんにちは
    p.Printf("%s, %s\n", "こんにちは", "世界")
}

ただこちらもドキュメントにフォーマット引数の順番に関する記述がなかったり、plural.Selectf の動作がいまいち怪しかったりと、まだまだ改善の余地ありな状況なので、もし本番に使うなら message.SetString に限るなど限定的な使用方法が良いかもしれません。逆にContributeチャンスでもありますね。

おわりに

個人的な感想として golang.org/x/text 以下は需要の割に作りが甘い感じがしていて、今回も調べるにあたってつまずく部分がいくつかありました。このあたりのパッケージは技術的な困難さという部分よりも、テストケースをいかに作るかという部分が大きいと思いますので、これを機会にこのパッケージを使ってGo製アプリケーションの日本語化をどんどん行って、どんどんIssue登録がされると良いなと思いました。

今年も1年お疲れ様でした。Gopherのみなさん、良いお年を。

*1:ところで、オーバーフローしたGoのアドベントカレンダーが ”Go 2〜" "Go 3〜" となってるのに違和感を感じたのは僕だけでしょうか。"Go〜 その2” とかならわかるけど、最初 "Go 2〜" を見たときに、Go 2 に関するアドベントカレンダーかと思いました。

*2:ただ、このエントリを書いていてところどころ挙動が怪しいところを見つけてしまったので、これはcontributeチャンスですね。

「Go言語による並行処理」という本が出版されました #cingo

はじめに

こんにちは、Stackdriver担当者です。このたび私の印刷書籍としては2冊めの翻訳本「Go言語による並行処理」がオライリー・ジャパン社より出版されました。本日より書店ならびに各社オンラインストアでご購入いただけます。

Go言語による並行処理

Go言語による並行処理

電子書籍版についてはオライリー・ジャパンのサイトよりePub、mobi、PDFの各種フォーマットにてご購入いただけます。 www.oreilly.co.jp

誤字脱字等を見つけた場合にはこちらのレポジトリまでご報告ください。 github.com

「Go言語による並行処理」はどのような本か

本書はKatherine Cox-Budayが2017年に執筆した "Concurrency in Go" の日本語訳書籍です。

Concurrency in Go: Tools and Techniques for Developers

Concurrency in Go: Tools and Techniques for Developers

この本はこれまで多くの場所で議論されたり語られたりされてはいたものの、書籍のようなまとまったボリュームで記述されることが少なかったGoでの並行処理についてのあれこれを1冊にまとめた本です。 Goの特徴といえば、並行処理を前提とした言語仕様および標準ライブラリでのサポートが挙げられますが、本書ではそれらの前提にある理論の説明と、実際にそれらを使った並行処理のパターンを記述方法についての説明、さらにはランタイムの内部動作に至るまでを触れています。

ともすると並行処理に関する理論などはそれだけでも非常に多くの説明を要し、このサイズの本には収まりきらない内容になってしまうところですが、本書では説明に必要な最低限の部分だけをうまく切り出し、それらを抑えた上でGoでの実装に多くの分量を費やしています。最低限とはいいつつも、必要な内容に関してはうまく導入しているので、理論的な内容を知りたい方も本文や脚注で触れられている参考文献を参照することで、より深い内容を理解するきっかけにもなることと思います。

また本書ではGoでの並行処理で陥りやすい失敗はなにか、紹介するテクニックがなぜ必要になるのか、といった内容も失敗例も含めて記述されているので、実際にコードを書いてはまったことがある人にはより実感を持ってご理解いただけるのではないでしょうか。

すでに @mattn_jp さんや @lestrrat さんより書評をいただいていますので、そちらもあわせてご覧いただけると、どのような本かよりイメージしやすいかと思います。

mattn.kaoriya.net

medium.com

出版に至るまでの話

私がGoを知ったのは、2010年2月ごろで、まだGoに go tools がなく、Makefileでビルドし、クロスビルドはできていたものの、対象アーキテクチャごとに 6g8g などの専用コンパイラを使う、まるでPlan 9そのままといった様子でした。

その後私がGoogleに転職し日常的にGoに触れるようになり、Goを本格的に使いだしたのはGo 1.0がリリースされる前後でした。その後もGoは今日に至るまで発展を続け、Makefileアーキテクチャごとのコンパイラをいちいち叩きながら使っていたビルド環境も徐々に go tools として一つにまとめ上げられ、またかつては数少なかったサードパーティーパッケージも、いまや実用に耐えうるものが潤沢になりました。そしてGoを本番環境システムの開発用言語として採用する企業もいまや当たり前となりました。はじめてのGoConであるGoCon 2013 spring を開催したときには、まだ採用している企業が日本ではほとんどなかったことを考えると、言語自身だけでなくコミュニティもものすごい勢いで拡大してきたことがおわかりでしょう。

そんなGoですが、オープンソースとしてリリースされた当初から変わらず、それでいてもっともGoをGoたらしめているもの、それが goroutinechannelselect といった並行処理に関するプリミティブです。 Goがこれほどまでに急速な発展を遂げたのも、Goの並行処理のプリミティブが非常に強力であったことと、マルチコアCPU時代かつ分散処理が当たり前になりつつあった時代の要請とが見事に噛み合ったことが大きな要因の一つでしょう。*1

そうしたプリミティブは使い始めるには非常に簡単ではあったのですが、どんな道具でもそうであるように「使いこなす」ためには経験が必要となります。他言語での経験があった開発者も、スレッドプールなどを用いた並行処理のパターンをゴルーチンに当てはめて使うことは容易だったとは思いますが、それでもGo独特の並行処理の記述方法やパターンなどは標準パッケージ内での記述や、 golang-nuts のような開発者メーリングリストでシェアされたことで広まったものも少なくありません。

私自身もそうしたところから学んでいった一方で、2年ほど前に、こうした一箇所にまとまっていない情報が整理された書籍があればいいなと考えていたところでした。そんな折に原著である Concurrency in Go の出版の話を知り、以前よりお世話になっていたオライリー・ジャパンの瀧澤さんにその翻訳ができないか聞いてみました。最初のお返事では訳者と編集のご担当の方がすでにいらっしゃるということでしたので、私もそれを楽しみにしていたのですが、その後紆余曲折あり6年ぶりにオライリージャパンでの翻訳を瀧澤さんとご一緒させてもらえることとなりました。

かねてより瀧澤さんのお仕事は間近に見ていたので、今回も翻訳開始前から非常に頼もしく、実際編集に関しても非常にやりやすい環境を提供していただけました。初校、レビュアーによるレビュー、二校、三校と各段階で間に原稿が進まない時期もあったのですが、瀧澤さんにスケジュールをご調整いただき無事に年内に発刊に至りました。

謝辞

訳者序文でも謝辞として執筆させていただきましたが、本書では多くの方々よりレビューをいただきました。 あらためて本書の出版にあたり、忙しい業務や私生活の合間を縫ってレビューに参加してくださった皆様に感謝いたします。

レビュアーの皆様(五十音順)

  • 伊藤友気さん (@mururururu)
  • 上田拓也さん (@tenntenn)
  • 上西康太さん (@kuenishi)
  • 小泉守義さん (@moriyoshit)
  • 渋川よしきさん (@shibu_jp)
  • 知久翼さん (@_achiku)
  • 中島大一さん (@deeeet)
  • 松木雅幸さん (@Songmu)

オライリー・ジャパン

  • 瀧澤昭広さん (@turky)

特に、上西さん、知久さんには英語の細かなニュアンスの差異などを指摘していただき、大いに参考になりました。また小泉さん、渋川さんには訳注として追加すべき情報や技術的な考慮点などを共有いただいたことで、本書の内容がより充実したものとなりました。@mattn_jp さんには書評をいただいただけでなく、本書の告知にも多分にご協力いただきました。あらためて、ありがとうございました。

参照

*1:他にも多くの開発者が慣れている手続き型指向の言語であったこと、実行速度が速いこと、標準のツールが充実していること、シングルバイナリにビルドされること、メモリフットプリントが小さいこと、ランタイムの起動が速いこと、ビルドが継続開発に耐えうる時間で完了すること、など理由は様々だと思います。

make関数でチャネルを作成する際にキャパシティを指定しない場合の内部動作

はじめに

こんにちは、Stackdriver担当です。Goの make 関数はあらゆるビルトイン型の作成を行う役割を担っているわけですが、諸用でチャネルを作成する際にキャパシティを設定しなかった場合にデフォルト値の0を設定する処理はどこでやってるのかなと思って見てみました。

Go言語による並行処理

Go言語による並行処理

TL;DR

メモ書き

Goのbuiltinは src/builtin/builtin.go にあると見せかけて、それはgodocのために書いてあるだけのダミー。 実際は src/rumtime 以下と src/cmd/compile/internal/gc 以下に隠されている。

src/runtime/chan.go には func makechan(t *chantype, size int) *hchan があり、これが唯一のチャネルを作る関数。したがって、これを呼び出すときにはintの引数が必要になる。

makechan 関数は src/cmd/compile/internal/gc/walk.go 内の func walkexpr(n *Node, init *Nodes) *Node構文解析した結果のノードのオペレーターが OMAKECHAN という識別子になっていた場合に呼ばれている。

OMAKECHAN はどこで呼ばれているかと言うと、src/cmd/compile/internal/gc/typecheck.gofunc typecheck1(n *Node, top int) *Node 内でやはりオペレーターが OMAKE だったときに呼ばれていて、かつその引数が TCHAN (チャネルの識別子)だった場合に呼ばれている。

go/typecheck.go at ae9c822f78d5048aa4290b06a5a38f67aaf23dbe · golang/go · GitHub

case TCHAN 内で i という値は引数の数として i = 1 (=チャネル型を指定しているところ)がまず最低限の数として定義されていて、もしこの他に引数がある(=キャパシティ)の設定がされている場合にはその値を設定している。

で、知りたかった、キャパシティの引数が設定されていない場合は、 n.Left = nodintconst(0) が設定される。 詳細は省くが nodintconst(0) は名前の通り 0 というintの値を持ったノードを作って返すので、ここでデフォルト値のキャパシティの0が設定されている、ということでした。

GoでStackdriver Logging向けのログをお手軽に出力する設定

はじめに

こんにちは、Stackdriverで遊んでいる人です。Stackdriver Loggingは標準出力に出されたJSON形式のログをFluentdベースのエージェント経由でいい感じに表示してくれます。

一方でGoに限らず通常のロギングライブラリは標準エラーにログを吐くという感じになるのですが、Stackdriver Loggingの場合デフォルト設定だと標準エラーに吐かれたログはすべてエラー扱いになりますので(まあ当たり前だよな)、そのあたりのすり合わせ調整が必要。

標準の log パッケージの場合

標準パッケージだけ使うのであれば、とりあえずこれだけやっておけばOK

  • 標準出力に出す(GKEの場合)
  • JSON形式でいくつかの決められたフィールド名(message, time, severity)を満たす

ということなのでこういう log.Loggerインスタンスを作ってあげる感じになりそう。

func sdLog(l *log.Logger, severity, msg string) {
    now := time.Now().Format(time.RFC3339Nano)
    entry := map[string]string{
        "time":     now,
        "severity": severity,
        "message":  msg,
    }
    b, err := json.Marshal(entry)
    if err != nil {
        log.Fatal(err)
    }
    l.Print(string(b))
}

logger := log.New(os.Stdout, "", 0)
sdLog(logger, "info", "hello")

logrus を使う場合

上の条件を満たすだけなんだけども、 logrus の場合はJSONFormatterがあるし、logrus.Loggerに各種ログレベルにあわせたメソッド(InfofWarnf など)があるので楽に設定できる。

log := logrus.New()
log.Level = logrus.DebugLevel
log.Formatter = &logrus.JSONFormatter{
    FieldMap: logrus.FieldMap{
        logrus.FieldKeyTime:  "timestamp",
        logrus.FieldKeyLevel: "severity",
        logrus.FieldKeyMsg:   "message",
    },
    TImestampFormat: time.RFC3339Nano,
}
log.Out = os.Stdout

Stackdriver LoggingのGo用のクライアントライブラリを使う

これを使っていいならはじめから使ったほうが良い。ただ個人的には logrus のほうがseverityに応じたヘルパーメソッドがあるので使い勝手が良いように感じる。

godoc.org

client, _ := logging.NewClient(ctx, projectID)
logger := client.Logger("my-log")
// Text Payload
logger.Log(logging.Entry{Payload: "Hello, world!")

// JSON Payload
type MyEntry struct {
    Name  string
    Count int
}
logger.Log(logging.Entry{
        Payload: MyEntry{Name: "Bob", Count: 3},
        Severity: logging.Critical,
})

or-done-channelでコードの可読性を上げる

はじめに

こんにちは、キーボード自作おじさんです。このエントリはGo Advent Calendar 2017の4日目の記事です。 今年のエントリーは大作が並ぶアドベントカレンダーの休憩用のエントリーと思っていただければ幸いです。

Goの並列処理のパターン

Goが公開されてからもう8年になり、Goが得意とする並列処理にもGo特有のパターンなどがコミュニティ内で蓄積されてきました。 その中でもよく聞くものとしては

  • for-select loop
  • or-channel
  • or-done-channel
  • tee-channel
  • fan-in, fan-out

などがあります。今日はその中でも or-done-channel について書こうと思います。

どういうときに or-done-channel を使うか

上限の数が決まっているような処理を行う場合に

  • データのソースからの入力が終わってしまった場合(channelのcloseに対応)
  • 上限の数に達してしまった場合(doneに対応)

のいずれかに当てはまる場合に便利なパターンです。

rangeを使う場合

まずchannelがcloseするまでの処理を行う場合は、range キーワード使って forループを回すのが便利で可読性も高いです。

for v := range ch {
    something(v)
}

しかし、この場合は処理の上限に達してしまった場合が対応できません。何らかの形で ch と done の両方を受けてあげる必要があります。

for-select loopだけを使う場合

そうなるとGoで複数のchannelを使った並行処理を書こうと思った場合には for-select loop が一番良く出てくるので、それを書きたくなります。

LOOP:
for {
    select {
    case v, ok := <-ch:
        if !ok {
            break LOOP
        }
        something(v)
    case <-done:
        break LOOP
    }
}

これでとりあえず希望の処理は書けるのですが、どうも見た目が大きくなってしまいます。そこでこの処理を別途まとめて、rangeの書き方に持ち込むのが or-done-pattern です。

or-done-channel

deferの性質を使って done が来ても ch からのデータ取得が終わった場合にも、かならず stream を close しています。これで得られる stream が閉じられたどうかだけ意識すればよくなるため、普通に range のパターンに持ち込めるわけです。

func orDone(done <-chan bool, ch <-chan Data) <-chan Data {
    stream := make(chan Data)
    go func() {
        defer close(stream)
        for {
            select {
            case <-done:
                return
            case v, ok := <-ch:
                if !ok {
                    return
                }
                stream <- v
            }
        }
    }()
    return stream
}

for v := range orDone(done, ch) {
    something(v)
}

Goはシンプルな文法ながら、 goroutinechannelfordefer などの機能をうまく組み合わせると非常に柔軟な書き方ができるので、こういった書き方との出会いがたくさんあって日々勉強になっています。皆さんも面白いパターンを知ったらぜひ教えてください。

明日は @cia_rana さんです。