- 投稿日:2021-03-06T23:16:05+09:00
AtCoder Beginner Contest 194のメモ
前置き
Atcoderをやってみたので、自分用のメモです。
あとから加筆・修正する予定です。問題
A
Q_A.gopackage main import ( "fmt" ) func main() { var A, B int fmt.Scanf("%d %d", &A, &B) C := A + B if (C >= 15) && (B >= 8) { fmt.Printf("1\n") } else if (C >= 10) && (B >= 3) { fmt.Printf("2\n") } else if (C >= 3) { fmt.Printf("3\n") } else { fmt.Printf("4\n") } }B
Q_B.gopackage main import ( "fmt" ) func main() { var N int fmt.Scanf("%d",&N) var A_0 int = 100000 var A_1 int = 100000 var B_0 int = 100000 var B_1 int = 100000 var a_i, b_i int var A, B int for i:=0; i<N; i++{ fmt.Scanf("%d %d",&A, &B) if A_0 >= A { A_1 = A_0 A_0 = A a_i = i } if B_0 >= B { B_1 = B_0 B_0 = B b_i = i } } if a_i == b_i{ C := A_0 + B_0 if (C <= A_1) && (C <= B_1) { fmt.Printf("%d\n", C) } else { if A_1 <= B_1{ if A_1 <= B_0{ fmt.Printf("%d\n", B_0) } else { fmt.Printf("%d\n", A_1) } } else { if A_0 <= B_1{ fmt.Printf("%d\n", B_1) } else { fmt.Printf("%d\n", A_0) } } } } else { if A_0 < B_0{ fmt.Printf("%d\n", B_0) } else { fmt.Printf("%d\n", A_0) } } }C
覚えてたら後で書きます。
D
覚えてたら後で書きます。
E
覚えてたら後で書きます。
F
覚えてたら後で書きます。
- 投稿日:2021-03-06T22:24:03+09:00
Testify の assert を使ってテストを書く
概要
- testify
- golang でテストをより記述しやすくするためのパッケージ
- その中で
assert
パッケージは、結果値の妥当性をチェックする関数を提供する本記事で使用したバージョン
- go: 1.16
- testify 1.7.0
テストの書き方と結果出力
import
import ( // ... 他の必要なパッケージ "github.com/stretchr/testify/assert" )引数の指定
- 1番目の引数は、
testing.T
- そのあと、テスト関数ごとに指定の引数を追加する
- 2つの値を比較するとき、大抵は
期待される値
実際の値
の順だが、たまに逆の関数もある- 指定の順番を間違えると、テスト失敗時の出力で逆に表示されてしまう
- 引数の末尾に任意の文字列を追加することができる
type Member ( Name string Grade int ) func TestEqual(t *testing.T) { expected := &Member{Name: "Niko", Grade: 3} assert.Equal(t, expected, &Member{Name: "Niko", Grade: 2}, "%s is 3rd grade", expected.Name) }結果表示
- テストに失敗すると、以下のような内容が出力される
=== RUN TestEqual assert_test.go:27: Error Trace: Error: Not equal: expected: &assertion_test.Member{Name:"Niko", Grade:2} actual : &assertion_test.Member{Name:"Niko", Grade:3} Diff: --- Expected +++ Actual @@ -2,3 +2,3 @@ Name: (string) (len=4) "Niko", - Grade: (int) 2 + Grade: (int) 3 }) Test: TestEqual Messages: Niko is 3rd grade --- FAIL: TestEqual (0.00s)関数
以下の関数で、カッコ内でプレフィクス
Not
またはNo
がついているものは、元のテストの逆の結果になることのテスト関数。等価性チェック
Equal(NotEqual)
Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
- 実際値が期待値と一致していることをテストする
- 値はスカラー値だけでなく、構造体のインスタンス等でも比較可能
- 唯一、function 同士の比較はできない
type Member struct { Name string Grade int } func TestEqual(t *testing.T) { num := 111 str := "Honoka" arr := []string{"Kotori", "Umi", "Hanayo"} niko := &Member{ Name: "Niko", Grade: 3, } assert.Equal(t, 111, num) assert.NotEqual(t, 222, num) assert.Equal(t, "Honoka", str) assert.Equal(t, []string{"Kotori", "Umi", "Hanayo"}, arr) assert.Equal(t, &Member{Name: "Niko", Grade: 3}, niko, "%s is 3rd grade", niko.Name) }EqualValues(NotEqualValues)
EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
- 2つの値が同一かどうかをテストする
Equal
との違いは、型が同一でなくても、型変換により同一とみなせる場合はテストが成功する点func TestEqualValues(t *testing.T) { assert.EqualValues(t, uint(123), int(123)) }Same(NotSame)
Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
- 2つのポインタ値が同一オブジェクトを指していることをテストする
Same
であるためには以下の両方の条件を満たす必要がある
- ポインタが同一オブジェクトを指している(ポインタ値が同一)
- 同一の型である
func TestSame(t *testing.T) { eri := &Member{ Name: "Eri", Grade: 3, } ptrEri := eri type Student *Member sEri := Student(eri) assert.Same(t, eri, ptrEri) assert.NotSame(t, &Member{Name: "Eri", Grade: 3}, ptrEri) // assert.Same(t, eri, sEri, "not same types") // fail }
- 一番下のテストは、ポインタは同一(
0xc0000b61f8
)だが、型が異なるため失敗するassert_test.go:42: Error Trace: Error: Not same: expected: 0xc0000b61f8 &assertion_test.Member{Name:"Eri", Grade:3} actual : 0xc0000b61f8 &assertion_test.Member{Name:"Eri", Grade:3} Test: TestSame Messages: not same types: *assertion_test.Member, assertion_test.Student --- FAIL: TestSame (0.00s)Exactly
- 2つのオブジェクトが同一かどうかをテストする
- TODO: すみません
Equal
との違いが不明です特定の条件を満たす値かどうか
Nil(NotNil)
Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
- オブジェクトが
nil
であるかをテストするEmpty(NotEmpty)
Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
- オブジェクトの内容が空であることをテストする
- 次項の「
Empty
とZero
の違い」も参照Zero(NotZero)
Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool
- 値がその型のzero valueであることをテストする
Empty
とZero
の違い
Zero
は、値そのものが zero value であることをチェックする
- array, slice, map, channel, ポインタの場合
- 値が
nil
かどうかEmpty
は、その値が指し示すものが「空」とみなせるかどうかをチェックする
- array, slice, map, channel の場合
- 長さが 0 であるかどうか
- ポインタの場合
- それが指し占めす値が空であれば、空とみなされる
func TestEmptyZero(t *testing.T) { i := 0 ip := &i assert.Empty(t, i) assert.Empty(t, ip) // ip が指し示す i は zero value assert.Zero(t, i) assert.NotZero(t, ip) // ip 自体は値を持つ var se := []string{} // []string{} var sn []string // []string(nil) assert.Empty(t, se) assert.Empty(t, sn) assert.NotZero(t, se) // 空の slice を持つ assert.Zero(t, sn) // slice 自体がない }True/False
True(t TestingT, value bool, msgAndArgs ...interface{}) bool
- オブジェクトの値が
true
またはfalse
と判定されるかどうかをテストするLen
Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool
- オブジェクトの長さが期待通りであることをテストする
- built-in 関数の
len()
を実行可能なオブジェクトに対してのみ適用できる
- 適用できない場合はテストに失敗する
func TestLen(t *testing.T) { assert.Len(t, "Rin", 3) }Regexp(NotRegexp)
Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool
- 実際に得られた文字列が、指定の正規表現に合致することをテストする
- 期待される正規表現(
rx
)は、*regexp.Regexp
型、もしくはstring
で表現された正規表現のどちらでもよいfunc TestRegexp(t *testing.T) { str := "nikko nikko nii" assert.Regexp(t, regexp.MustCompile("^(nikko )+nii$"), str) assert.Regexp(t, "^(nikko )+nii$", str) assert.NotRegexp(t, "^(nikko ){3}nii$", str) }リスト
Contains/NotContains
Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool
- オブジェクトが特定の値を含むことをテストする
- 適用可能なのは、
string
、リスト(array, slice 等len()
の適用が可能なもの)、map
に限られる
- それ以外のオブジェクトに適用した場合はテストに失敗する
string
の場合、部分文字列として含まれるかどうかをテストする- リストの場合、特定の要素がリスト内に存在するかどうかをテストする
map
の場合、特定の key が map 内に含まれるかどうかをテストする
- map の value は比較の対象外
func TestContains(t *testing.T) { assert.Contains(t, "Maki Nishikino", "Maki") assert.Contains(t, []string{"Maki", "Hanayo", "Rin"}, "Maki") assert.Contains(t, map[string]int{"Maki": 1, "Honoka": 2, "Niko": 3}, "Maki") // map の value は対象外 assert.NotContains(t, map[string]int{"Maki": 1, "Honoka": 2, "Niko": 3}, 3) }Subset(NotSubset)
Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool)
- リスト型のオブジェクトに対して、期待されたサブセットが含まれるかどうかをテストする
func TestSubset(t *testing.T) { members := []string{"Kotori", "Kayo", "Eri"} assert.Subset(t, members, []string{"Kotori", "Eri"}) assert.NotSubset(t, members, []string{"Kotori", "Yukiho"}) }ElementsMatch
ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool)
- リスト型のオブジェクトに対して、すべての要素が含まれていることをテストする
- 要素の順番は問わない。ただし個数は一致する必要がある
func TestElementsMatch(t *testing.T) { members := []string{"Kotori", "Kayo", "Eri", "Kayo"} assert.ElementsMatch(t, members, []string{"Eri", "Kayo", "Kayo", "Kotori"}) //assert.ElementsMatch(t, members, []string{"Eri", "Kotori", "Kayo", "Kotori"}) //fail }誤差の判定
WithinDuration
WithinDuration( t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool
time.Time
で表される時刻と、期待する時刻との誤差が一定以下であることをテストするfunc TestWithinDuration(t *testing.T) { now := time.Now() expected := now.Add(2 * time.Second) assert.WithinDuration(t, expected, now, time.Duration(5*time.Second)) //assert.WithinDuration(t, expected, now, time.Duration(1*time.Second)) // fail }InDelta
InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool
- 両者の誤差が一定値(
delta
)以下であることをテストする。func TestInDelta(t *testing.T) { val := 10.0 / 2.998 assert.InDelta(t, 10.0, val*2.99, 0.1) }InDeltaSlice
InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool
InDelta
のスライス版- 2つのスライスに含まれる同一位置の数値同士を比較し、その誤差が一定値(
delta
)以下であることをテストする- テストされる側(
actual
)のすべての要素に対してテストが通った場合に、InDeltaSlice
のテストも通るfunc TestInDeltaSlice(t *testing.T) { expect := []float32{1.0, 2.0, 3.0} actual := []float32{1.03, 1.98, 3.01} assert.InDeltaSlice(t, expect, actual, 0.03) }InDeltaMapValues
InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool
- 比較対象は map 型
- すべての key に対応する値を期待値、実際値で比較し、その誤差が範囲内に収まっていることをテストする
- key が完全に一致しない場合は FAIL となる
func TestInDeltaMapValues(t *testing.T) { expected := map[string]float64{"width": 2.5, "height": 7.5} actual := map[string]float64{"width": 2.47, "height": 7.53} assert.InDeltaMapValues(t, expected, actual, 0.1) }InEpsilon
InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool
- 期待値対する実測値の誤差が一定割合以内に収まっていることをテストする
InDelta
と似ているが、InEpsilon
では誤差の「割合」をチェックするfunc TestInEpsilon(t *testing.T) { assert.InEpsilon(t, 10000, 10090, 0.01) assert.InEpsilon(t, 100, 100.9, 0.01) // assert.InEpsilon(t, 1, 0.11, 0.01) <-- fail }InEpsilonSlice
InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool
InEpsilon
のスライス版ファイル
FileExists(NoFileExists)
FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool
- 指定したファイルが存在することをテストする
- 指定したパスがディレクトリである場合はテスト失敗となる
func TestFileExists(t *testing.T) { // exists.txt はすでに存在し、missing.txt はないものとする assert.FileExists(t, "./exists.txt") assert.NoFileExists(t, "./missing.txt") // existsdir は存在するディレクトリ assert.NoFileExists(t, "./existsdir") }DirExists(NoDirExists)
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool {
- 指定したディレクトリが存在することをテストする
- 指定したパスがファイルであった場合はテストに失敗する
func TestDirExists(t *testing.T) { // existsdir は存在するディレクトリ、missingdir はないものとする assert.DirExists(t, "./existsdir") assert.NoDirExists(t, "./missingdir") // exists.txt はファイル assert.NoDirExists(t, "./exists.txt") }JSON, YAML
JSONEq
JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool
- 実際値のJSON文字列が、期待されるJSON文字列と内容的に同一であることをテストする
- 文字列内での順番が異なってもかまわない
func TestJSONEq(t *testing.T) { expected := `{"name": "Nozomi", "age": 17, "blood": "O"}` actual := `{"age": 17, "blood": "O", "name": "Nozomi"}` assert.JSONEq(t, expected, actual) }YAMLEq
YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool
- 実際値のYAML文字列が、期待されるYAML文字列と内容的に同一であることをテストする
- 文字列内での順番が異なってもかまわない
func TestYAMLEq(t *testing.T) { expected := `--- name: Eri age: 17 blood: B` actual := `--- blood: B age: 17 name: Eri` assert.YAMLEq(t, expected, actual) }状態変化
Eventually
Eventually( t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool
- 一定時間内(
waitFor
)に条件が満たされた状態になることをテストする- 条件が満たされているかをチェックするための関数(
condition
)を引数で渡す
- 条件が満たされたら
true
、そうでなければfalse
を返す- この関数は
tick
で指定した時間おきに呼び出されるfunc TestEventually(t *testing.T) { var content []byte go func() { res, err := http.Get("https://example.com") if err != nil { return } defer res.Body.Close() content, _ = io.ReadAll(res.Body) }() // コンテンツが読み込まれたかをチェックする関数 checkCompleted := func() bool { if len(content) > 0 { return true } return false } assert.Eventually(t, checkCompleted, time.Second*5, time.Second*1) }Never
Never( t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool
Eventually
の逆。一定時間内に条件が満たされないことをテストする。エラーおよび Panic
Error(NoError)
Error(t TestingT, err error, msgAndArgs ...interface{}) bool
- error が存在する(
nil
でない)ことをテストするfunc TestError(t *testing.T) { divideFunc := func(a, b int) (int, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } _, err := divideFunc(5, 0) assert.Error(t, err) _, err = divideFunc(5, 2) assert.NoError(t, err) }EqualError
EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool
- 得られたエラーが、期待される文字列表現と合致するかどうかをテストする
- テスト対象(
theError
)のエラーの文字列表現がerrString
と完全一致していればよい- 他の
assert
関数と引数の順番が異なるので注意
実際に得られたエラー
期待されるエラー文字列
の順var firstGrades map[string]string = map[string]string{ "Hanayo": "Koizumi", "Rin": "Hoshizora", "Maki": "Nishikino", } type ErrorNotFirstGrade struct{} func (e *ErrorNotFirstGrade) Error() string { return "not a first grade member" } func getLastName(firstName string) (string, error) { ln, ok := firstGrades[firstName] if !ok { return "", new(ErrorNotFirstGrade) } return ln, nil } func TestEqualError(t *testing.T) { _, err := getLastName("Honoka") assert.EqualError(t, err, "not a first grade member") }ErrorContains
ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool
- 得られたエラーが、期待される文字列を含んでいるかどうかをテストする
- テスト対象(
theError
)のエラーの文字列表現がerrString
を含んでいればよいEqualError
と同様、他のassert
関数と引数の順番が異なるので注意
実際に得られたエラー
期待される部分エラー文字列
の順func TestErrorContains(t *testing.T) { err := errors.New("Honoka and Hanayo were captured by Umi in front of a rice restaurant") assert.ErrorContains(t, err, "captured") }ErrorIs(NotErrorIs)
ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool
- 対象エラーのエラーチェーンの中に、特定のエラーが含まれていることをテストする
- エラーチェーンの探索には
errors.Is()
(https://golang.org/pkg/errors/#Is) が使用されるfunc TestErrorIs(t *testing.T) { errA := errors.New("Umi cannot write lyrics") errB := fmt.Errorf("Umi has escaped: %w", errA) errC := fmt.Errorf("Umi not found: %w", errB) assert.ErrorIs(t, errC, errA) }ErrorAs
ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool
- 対象エラーのエラーチェーンの中に、
target
に代入可能なエラーが含まれていることをテストする- テストが通った場合、
target
にそのエラーが格納される- エラーチェーンの探索および
target
への代入は、errors.As()
(https://golang.org/pkg/errors/#As) の挙動と同じtype KotoriSlumpError struct{} func (e *KotoriSlumpError) Error() string { return "Kotori cannot design dresses" } func TestErrorAs(t *testing.T) { errA := &KotoriSlumpError{} errB := fmt.Errorf("Kotori has escaped: %w", errA) errC := fmt.Errorf("Kotori not found: %w", errB) var err *KotoriSlumpError assert.ErrorAs(t, errC, &err) fmt.Println(err) // Kotori cannot design dresses }Panics(NotPanics)
Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool
- 指定された関数が panic を起こすことをテストする
- 指定できる関数の型は
func()
(assert.PanicTestFunc
型)recover()
実行時に得られる panic value を確認するPanicsWithValue
PanicsWithError
も提供されているfunc TestPanics(t *testing.T) { setQuantity := func(qty int) func() { return func() { if qty <= 0 { panic(fmt.Errorf("qty cannot be negative")) } } } assert.PanicsWithError(t, "qty cannot be negative", setQuantity(-2)) assert.NotPanics(t, setQuantity(5)) }複雑な条件を使う
Condition
Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool
- テストの成否を複雑な条件で判定させたいときに使う
- 成否判定を行う
func() bool
型の関数を引数で渡すtype Member struct { Name string Grade int } func TestComparison(t *testing.T) { is3rdGrade := func(m Member) func() bool { return func() bool { return m.Grade == 3 } } assert.Condition(t, is3rdGrade(Member{"Niko", 3})) //assert.Condition(t, is3rdGrade(Member{"Honoka", 2})) // fail }
- 投稿日:2021-03-06T17:52:28+09:00
プログラミング超入門(3)〜GO言語〜
文字列型
コンピューターで扱われる言葉が人間に理解できるように文字として並べたものを「文字列」と呼びます。GO言語における文字列の正式なデータ型は、string型です。string型の値(文字列値)を作成するには「””」ダブルクォーテーションで文字の並びを囲みます。Println関数やPrintf関数により出力されているのがこの文字列値です。
string.gopackage main import "fmt" func main() { normalString := "はじめまして!" emptyString := "" emptyString += "宜しくお願い致します!" fmt.Println(normalString) fmt.Println(emptyString) }・実行結果
はじめまして! 宜しくお願い致します!上記の場合、従来
var normalString string = "はじめまして!"
が省略されて記述されています。
Println関数のように、fmtパッケージ関数には
文字列の中に埋め込め
て表示形式を指定できる
機能を持ったPrintf関数があります。printf関数をしようして変数aの値が少数のとき、少数第2まで表示させてみます。fmt.Printf("%.2f",a)「””」ダブルクォーテーション内の
%.2f
は表示形式が指定されています。%から始まり、少数点「.」とその後の桁数「2」そしてそれを指定する「f」に分けられ「,」カンマで区切られ、表示したい内容「a」がfmt.Printf
後の ( )カッコ内に記述されています。fmt.Printf("aの値は%.2fです",a)文字列の中に埋め込めるというのは「””」ダブルクォーテーションの中に他の文字を置くことができることを意味します。
DecimalPoint.gopackage main import "fmt" func main() { a := 4.2 b := 6.4 fmt.Printf("a+b=%.2f", a+b) fmt.Printf("a+b=%.2f", a-b) fmt.Printf("a+b=%.2f", a*b) fmt.Printf("a+b=%.4f", a/b) }・実行結果
a+b=10.60a-b=-2.20a*b=26.88a/b=0.6562上記の実行結果は除算を少数第4位まで表示させました。結果がくっついて見づらいため出力結果の文字列に改行の指示を行わせる「\n」を含めるとします。この「\」(円マークまたはバックスラッシュ)をエスケープ記号と呼び、非常に多くの種類があります。例えば、「\n」は改行、「\t」はタブ、「\s」はスペース、「\b」はバックスペース、「\0」は終端文字、を意味します。「\」はエスケープ記号ではなく、文字列として「\」を出力します。「\'」はシングルクォーテーションと「\"」はダブルクォーテーションは文字列の始まりと終わりを示す記号でなく、文字列として「'」「"」を出力します。 では、改行を加えて書き換えてみます。
DecimalPoint.gopackage main import "fmt" func main() { a := 4.2 b := 6.4 fmt.Printf("a+b=%.2f\n", a+b) fmt.Printf("a+b=%.2f\n", a-b) fmt.Printf("a+b=%.2f\n", a*b) fmt.Printf("a+b=%.4f\n", a/b) }・実行結果
a+b=10.60 a-b=-2.20 a*b=26.88 a/b=0.6562
- 投稿日:2021-03-06T13:42:42+09:00
【Golang】go-i18n/v2 の動作サンプル(2021年03月版)【国際化対応】
internationalization
略してi18n
go-i18n
の v2.x が Go v1.15+ で動作確認済みサンプルが欲しい。TOML じゃたまらんので JSON が良い。... うん?国際化?
国際化対応と言っても「用語辞書を言語ごとに用意しておいて、用語の識別子(メッセージ ID)を指定すると、指定した辞書の該当用語が返ってくるので、それを環境に合わせて使う」と言うだけのことです。
Go 言語には国際化対応(
internationalization
)するのに有名なgo-i18n
と言うパッケージがあります。
go-i18n
パッケージ
- github.com/nicksnyder/go-i18n @ GitHub
2021/03/07 現在のバージョンは 2.1.1 なのですが、なんか公式のサンプルも、従来バージョン(v1.x)を理解した人向けでいささかわかりづらいのです。
検索しても
go-i18n
v1.x 時代のものや、Go v1.15 以上で動かなかったり、typo
で動作しないものが多いので自分のググラビリティとして。TL; DR (今北産業)
Bundle
が辞書を保持するもの。Localizer
が辞書を引くもの。
- 入力(メッセージ IDや設定)から対応するメッセージを辞書から引っ張ってくる関数(メソッド)が
Localize()
。MustLocalize()
のようにMust*
が付くとエラーを返さずパニクります。go-i18n
のバージョンによってBundle
やLocalizer
に渡す言語指定方法が異なる。
- この記事は v2.x のサンプル。
? 言語指定で使われる
en-US
やja-JP
はtag
(言語タグ)と呼ばれ、一般的に IETF 言語タグを指します。IETF 言語タグは、インターネットの標準化団体の 1 つである IETF によって制定されたもので、習慣的にBCR47
と言った言い方もされます。これは、制定前の「その時点でベスト」とされる BCP(Best Current Practice
)のうち 47 番目に「言語タグの指定」のベスト・プラクティスとされたためで、現在の IETF 言語タグは RFC 5646 と RFC 4647 で制定されています。IETF言語タグの
"en-US"
や"ja-JP"
は"<language>-<REGION>"
の構成になっており、OS のLocale
の"ja_JP"
や"ja_JP.UTF-8"
と記法が異なるので注意が必要 ←(俺TS; DR (マスター、Go v1.15 以上で動くものをくれ)
基本(文書系向き)
基本(i18n.LocalizeConfigの設定情報で引く)package main import ( "fmt" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) func main() { // デフォルトを英語に設定。language.English = language.Tag の構造体 bundle := i18n.NewBundle(language.English) // 使う辞書の言語設定。language.English.String() = "en" loc := i18n.NewLocalizer(bundle, language.English.String()) // デフォルトの辞書を設定 // メッセージ ID "Emails" は "Name" と "Count" の 2 つの引数を持ち、 // 個数情報(PluralCount)によって単数(One:)、複数(Other:)でメッ // セージ内容が変わる。"Description" はメモ。 messages := &i18n.Message{ ID: "Emails", Description: "The number of unread emails a user has", One: "{{.Name}} has {{.Count}} email.", // 単数系用の構文定義 Other: "{{.Name}} has {{.Count}} emails.", // 複数系用の構文定義 } // 個数を指定(単数・複数でメッセージ内容が変わるかの確認) messagesCount := 2 // 辞書翻訳 translation := loc.MustLocalize(&i18n.LocalizeConfig{ // デフォルトの辞書を定義 DefaultMessage: messages, // メッセージ ID を含めず、"Name", "Count" を持つ辞書登録すべてに // "Theo" と個数をテンプレートとしてマッピング(割り当てを)する。 TemplateData: map[string]interface{}{ "Name": "Theo", "Count": messagesCount, }, // データの個数情報 PluralCount: messagesCount, }) fmt.Println(translation) }
- オンラインで動作をみる @ Go Playground(Go version: 1.16)
ヘルプなどの固定メッセージ向き(JSON 形式)
JSON辞書を使った例(メッセージIDで引く。シンプルでオススメ)package main import ( "encoding/json" "fmt" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) func main() { // デフォルト言語を英語に設定 bundle := i18n.NewBundle(language.English) // パーサーエンジンを JSON にセット bundle.RegisterUnmarshalFunc("json", json.Unmarshal) // JSON の辞書を登録 bundle.MustParseMessageFileBytes([]byte(`{"MSG001": "Hello World!"}`), "en.json") bundle.MustParseMessageFileBytes([]byte(`{"MSG001": "Hola Mundo!"}`), "es.json") bundle.MustParseMessageFileBytes([]byte(`{"MSG001": "ようこそ、世界!"}`), "ja.json") // 辞書を引く内容 referTo := &i18n.LocalizeConfig{MessageID: "MSG001"} // 辞書を引く { // 英語の辞書を使う localizer := i18n.NewLocalizer(bundle, "en-US") fmt.Println(localizer.MustLocalize(referTo)) } { // スペイン語の辞書を使う localizer := i18n.NewLocalizer(bundle, "es-ES") fmt.Println(localizer.MustLocalize(referTo)) } { // 日本語の辞書を使う localizer := i18n.NewLocalizer(bundle, "ja-JP") fmt.Println(localizer.MustLocalize(referTo)) } { // 未定義の辞書を使う(デフォルトの英語を使う) localizer := i18n.NewLocalizer(bundle, "ko-KR") fmt.Println(localizer.MustLocalize(referTo)) } } // Output: // Hello World! // Hola Mundo! // ようこそ、世界! // Hello World!
- オンラインで動作をみる @ Go Playground
- 投稿日:2021-03-06T05:33:47+09:00
go-cache-fetcher Goの DB<->Redis Fetcher Client
- 投稿日:2021-03-06T05:33:47+09:00
GoのCache付きFetcherクライアント go-cache-fetcher
- 投稿日:2021-03-06T01:35:13+09:00
Goの構造体とポインタ
ポインタ型での処理の備忘録.Go初心者,ポインタと不仲なら多分あるあるだと思います.
関数とポインタ
まず,構造体は値型です.ですので関数の引数に構造体を渡して加工してあげても元の構造体が変化することはありません.
package main import ( "fmt" ) type point struct { x, y float64 } func swap(p point) { x, y := p.x, p.y p.x = y p.y = x } func main() { p := point{x: 0, y: 1} swap(p) fmt.Printf("%+v\n", p)// {x:0 y:1} }これがいわゆる値渡しです.関数の引数の構造体を加工してあげるにはポインタで渡してあげます.
package main import ( "fmt" ) type point struct { x, y float64 } func swap(p *point) { // 引数をポインタへ x, y := p.x, p.y p.x = y p.y = x } func main() { p := point{x: 0, y: 1} swap(&p) // 渡す fmt.Printf("%+v\n", p) // {x:1 y:0} }うまく値が入れ替わりました.よかったです.これがいわゆる参照渡しというやつですね.
レシーバとポインタ
Goには構造体に対するメソッドという機能があります(このとき,この構造体をレシーバといいます).一般にメソッドはポインタ型で定義されています.この理由をサンプルを通して確認したいと思います.
まずは値型でメソッドを定義して振る舞いをみます.
package main import ( "fmt" ) type point struct { x, y float64 } // メソッドを値型で定義 func (p point) init(x, y float64) { p.x = x p.y = y } func main() { p1 := point{} // point型 p1.init(5, 10) fmt.Printf("%+v\n", p1) // {x:0 y:0} p2 := point{} // *point型 p2.init(5, 10) fmt.Printf("%+v\n", p2) // {x:0 y:0} }point型, *point型にしてもうまく動作しませんでした.これは先ほどの値渡し同様に,メソッドの呼び出し時にはレシーバのコピーが生成され,そのコピーに対して処理が行われているためです.
例によってレシーバをポインタ型で定義してみます.
package main import ( "fmt" ) type point struct { x, y float64 } // メソッドをポインタ型で定義 func (p *point) init(x, y float64) { p.x = x p.y = y } func main() { p1 := point{} // point型 p1.init(5, 10) fmt.Printf("%+v\n", p1) // {x:5 y:10} p2 := point{} // *point型 p2.init(5, 10) fmt.Printf("%+v\n", p2) // {x:5 y:10} }想定通りの挙動になりました.point型,*point型ともにメソッドを呼び出せしてちゃんと実行できています.また,メソッドならば,ユーザは対象の構造体を値型かポインタ型どちらにするか気にする必要がない恩恵があることもわかります.(このためにポインタの理解がガバりがちです)
以上のことから構造体に対する操作(関数,メソッド)はポインタ型に対して行うべきだとわかりました.
おまけ:rangeとポインタ
DBへのパッチ処理など,データをGET -> 加工 -> PUTする処理をしたい場面はたまにあると思います.そんな時にハマりかけたの経験がこの記事のきっかけでした.最後にサンプルを置いておきます.
package main import ( "fmt" ) type Person struct { Name string Age int } type userInfo struct { Name *string Age *int } func main() { persons := []Person{ { Name: "Bob", Age: 20, }, { Name: "Alice", Age: 10, }, } data := make([]userInfo, 0) for _, p := range persons { input := userInfo{ Name: &p.Name, Age: &p.Age, } data = append(data, input) } for i := 0; i < len(data); i++ { fmt.Printf("data[%d]...Name:%s, Age:%d\n", i, *data[i].Name, *data[i].Age) } } /* data[0]...Name:Alice, Age:10 data[1]...Name:Alice, Age:10 */dataに格納された内容がおかしいようです.rangeは同じポインタがチャネルに入れられており、ループが進むにつれて,参照している値が書き換えられているだけだからです.
そのため,以下のようにインデックスを指定して参照させると思った通りの挙動になります.
https://play.golang.org/p/aO8q4keS5o6for i, _ := range persons { input := userInfo{ Name: &persons[i].Name, Age: &persons[i].Age, } data = append(data, input) }