20210306のGoに関する記事は7件です。

AtCoder Beginner Contest 194のメモ

前置き

Atcoderをやってみたので、自分用のメモです。
あとから加筆・修正する予定です。

問題

A

Q_A.go
package 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.go
package 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

覚えてたら後で書きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
  • オブジェクトの内容が空であることをテストする
  • 次項の「EmptyZero の違い」も参照

Zero(NotZero)

Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool
  • 値がその型のzero valueであることをテストする
EmptyZero の違い
  • 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
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング超入門(3)〜GO言語〜

文字列型

コンピューターで扱われる言葉が人間に理解できるように文字として並べたものを「文字列」と呼びます。GO言語における文字列の正式なデータ型は、string型です。string型の値(文字列値)を作成するには「””」ダブルクォーテーションで文字の並びを囲みます。Println関数Printf関数により出力されているのがこの文字列値です。

string.go
package 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.go
package 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.go
package 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

←前回の記事 次の記事へ→

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang】go-i18n/v2 の動作サンプル(2021年03月版)【国際化対応】

internationalization 略して i18n

go-i18n の v2.x が Go v1.15+ で動作確認済みサンプルが欲しい。TOML じゃたまらんので JSON が良い。

... うん?国際化?

国際化対応と言っても「用語辞書を言語ごとに用意しておいて、用語の識別子(メッセージ ID)を指定すると、指定した辞書の該当用語が返ってくるので、それを環境に合わせて使う」と言うだけのことです。

Go 言語には国際化対応(internationalization)するのに有名な go-i18n と言うパッケージがあります。

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 のバージョンによって BundleLocalizer に渡す言語指定方法が異なる。
    • この記事は v2.x のサンプル。

?   言語指定で使われる en-USja-JPtag(言語タグ)と呼ばれ、一般的に IETF 言語タグを指します。IETF 言語タグは、インターネットの標準化団体の 1 つである IETF によって制定されたもので、習慣的に BCR47 と言った言い方もされます。これは、制定前の「その時点でベスト」とされる BCPBest Current Practice)のうち 47 番目に「言語タグの指定」のベスト・プラクティスとされたためで、現在の IETF 言語タグは RFC 5646RFC 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)
}

ヘルプなどの固定メッセージ向き(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!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-cache-fetcher Goの DB<->Redis Fetcher Client

DBなどのfetcherメソッドにアクセスすると、自動でキャッシュに保存され、キャッシュにある場合はそっちを使うよくあるパターンのGolang クライアントが無かったので、作りました。

Redisで使う想定ですが、Clientを実装すればどのようなキャッシュにも使えます。
serializeして set, getすることもできます。

Install

go get github.com/peutes/go-cache-fetcher

使い方はREADMEを見てください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoのCache付きFetcherクライアント go-cache-fetcher

DBなどのfetcherメソッドにアクセスすると、自動でキャッシュに保存され、キャッシュにある場合はそっちを使うよくあるパターンのクライアントが無かったので、作りました。

Redisで使う想定ですが、Clientを実装すればどのようなキャッシュにも使えます。

Install

go get github.com/peutes/go-cache-fetcher

使い方はREADMEを見てください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/aO8q4keS5o6

for i, _ := range persons {
    input := userInfo{
        Name: &persons[i].Name,
        Age:  &persons[i].Age,
    }
    data = append(data, input)
}

参考:https://tour.golang.org/methods/1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む