rsc.io/quoteライブラリをコードリーディングしてみた[Go学習]

2023/10/29に公開

コードリーディング、この記事の目的

  • Goを学習するにあたって、初学者の自分が手探りでコードを書いて学ぶよりも、経験豊富な先人の書いたコードを読むほうが学べることが多そうだから
  • またアウトプットすることによる知識の定着を狙っているから

rsc.io/quote

こちらのライブラリのコードリーディングを行いたいと思います。
たまたま見つけたライブラリですが、制作者がRuss Coxという方でGoの共同創設者の1人なのでコードリーディングの対象として間違いないでしょう。

https://pkg.go.dev/rsc.io/quote@v1.5.2

Readmeには以下の書かれており、実用的なライブラリではないようです。

このパッケージには、格言が集められている。
Goにおけるパッケージのバージョン管理のデモンストレーションの一部です。

ディレクトリ構造

主要なものだけ記載します。

- quote
  - go.mod
  - go.sum
  - quote.go
  - quote_test.go

ライブラリ内の4つの関数

このquoteライブラリには文字列を返す4つの関数が定義されています。
それぞれの挙動を見てみましょう。
Goプロジェクトを作成し以下のコードを実行します。

main.go
package main

import "rsc.io/quote"

func main() {
    println("quote.Go()   : ", quote.Go())
    println("quote.Opt()  : ", quote.Opt())
    println("quote.glass(): ", quote.Glass())
    println("quote.Hello(): ", quote.Hello())
}

結果はこちら

ターミナル
$ go run .
quote.Go()   :  Don't communicate by sharing memory, share memory by communicating.
quote.Opt()  :  If a program is too slow, it must have a loop.
quote.glass():  I can eat glass and it doesn't hurt me.
quote.Hello():  こんにちは世界。

ご覧の通り、それぞれの関数が文字列を返しています。

4つのうち始めの3つ、Go(), Opt(), Glass()はベタ書きされた文字列を単純に返しているだけです。
例えばGo()のソースコードはこちら。

quote/quote.go
func Go() string {
	return "Don't communicate by sharing memory, share memory by communicating."
}

しかし最後の1つHello()は違います。
返ってきた文字列は"こんにちは世界。"であり、何の変哲もないhello, worldの日本語版に見えますが、この関数の中で文字列がベタ書きされているわけではありません。
別のパッケージの関数が呼び出されています。

Hello()のコードを紹介して、ここからはHello()の内部で起こっていることを集中的に見ていきます。

quote/quote.go
import "rsc.io/sampler"

func Hello() string {
	return sampler.Hello() // samplerパッケージのHello関数が呼ばれている
}

sampler.Hello()について

さて、先に明かしてしまいますが、このsamplerパッケージのHello関数は"Hello, world."を利用者の言語に翻訳して返しています。

英語利用者には"Hello, world."、
日本語利用者には"こんにちは世界。"、
ネパール語利用者には"नमस्कार संसार।"となります。

やっていることを具体的に言うと

  1. 利用者の言語設定を取得する(環境変数であるlocale変数をos.Getenvで取得)
  2. samplerパッケージ内にある、各言語の"Hello, world."の一覧表(ソースコードはこちら)から利用者の言語の"Hello, world."文字列を取得しそれを返す

となります。

実際にコードを見ていきます。今回のケースではreturn sampler.Hello()と引数無しで利用されていたため、DefaultUserPrefs()が作動することになります。

samplerパッケージ内のHello()DefaultUserPrefs()を示します。なおsampler.goではlanguageパッケージがimportされていますがこれは後ほど触れます。

sampler.sampler.go
import {
()
"golang.org/x/text/language"
}

// Hello returns a localized greeting.
// If no prefs are given, Hello uses DefaultUserPrefs.
func Hello(prefs ...language.Tag) string {
	if len(prefs) == 0 {
		prefs = DefaultUserPrefs()
	}
	return hello.find(prefs)
}

// DefaultUserPrefs returns the default user language preferences.
// It consults the $LC_ALL, $LC_MESSAGES, and $LANG environment
// variables, in that order.
func DefaultUserPrefs() []language.Tag {
	var prefs []language.Tag
	for _, k := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} {
		if env := os.Getenv(k); env != "" {
			prefs = append(prefs, language.Make(env))
		}
	}
	return prefs
}

DefaultUserPrefs()では1の「利用者の言語設定を取得する」が行われています。
その結果をHello()内でhello.find(prefs)として利用し、最終的にHello, world.にあたる文字列が返されます。

ではhello.find(prefs)内の要素である変数hellofind()のコードを見てみます。2の「各言語の"Hello, world."の一覧表から利用者の言語の"Hello, world."文字列を取得しそれを返す」にあたる部分です。

// sampler/hello.go

var hello = newText(`
English: en: Hello, world.
(略)
Japanese: ja: こんにちは世界。
(略)
Nepali: ne: नमस्कार संसार।
`) // 約100の言語が定義されている

// newText creates a new localized text, given a list of translations.
func newText(s string) *text {
()
}

// sampler/sampler.go
type text struct {
	byTag   map[string]string
	matcher language.Matcher
}

func (t *text) find(prefs []language.Tag) string {
	tag, _, _ := t.matcher.Match(prefs...)
	s := t.byTag[tag.String()]
	if strings.HasPrefix(s, "RTL ") {
		s = "\u200F" + strings.TrimPrefix(s, "RTL ") + "\u200E"
	}
	return s
}

変数helloはtext型であり、byTagフィールドには言語タグとその言語の"Hello, world."にあたる文字列のmapが格納されています。

text型のメソッドfind()で、そのmapからDefaultUserPrefs()で取得した言語タグに見合った"Hello, world."にあたる文字列を取得し返しています。

(ちなみにif strings.HasPrefix(s, "RTL ") {... の部分は、アラビア語やヘブライ語などの右から左に書く言語の場合の処理を行っています アラビア語の例->Arabic: ar: RTL مرحبا بالعالم.

languageパッケージについて

languageパッケージの型やメソッドが複数箇所で使われていますが、これはgolang.org/x/text/language として提供されており、主に言語と地域(ロケール)に関連する操作をサポートします。

具体的な内容としては言語タグの解析やマッチングを可能にするパッケージとなります。

ハンズオン:"こんにちは世界。"以外を出力させてみる

ターミナル
// 初期状態確認
$ locale // LC_ALL= (LC_ALLのみ表示)

// 英語にする
$ export LC_ALL=en_US.UTF-8
$ locale // LC_ALL="en_US.UTF-8"
$ go run . // quote.Hello():  Hello, world.

// アラビア語にする
$ export LC_ALL=ar
$ locale # LC_ALL="C"
$ go run . // quote.Hello():  مرحبا بالعالم.

// 変更したロケールをもとに戻す
$ export LC_ALL=

Discussion