2021/09/24


Go のテンプレートエンジンは、一般的なテンプレートエンジンの記法と気色が異なり、独特の文法で記述するのですが、ループ制御構文に関してはお世辞にも満足できる物ではありませんでした。それは continue や break が無いというのが理由です。continue や break が無かったので、無駄に if をネストして条件にあった値を出力しなければならず、必然的に無駄な if のネストが起きていました。

■ビーフストロガノフ
  2011年 受賞
  2019年 受賞
■カレーライス
  2012年 受賞
■満漢全席
  2015年 受賞
  2019年 受賞

例えば上記の様な出力をするには、以下の様なテンプレートを書かなければなりませんでした。

package main

import (
    "os"
    "text/template"
)

var tmpl = `
{{- range .}}
{{- if gt .Value 3 -}}
■{{.Name}}
{{- range .Years }}
  {{ . -}}年 受賞
{{- end}}
{{end -}}
{{- end -}}
`

type Foo struct {
    Name  string
    Value int
    Years []int
}

func main() {
    tpl := template.Must(template.New("").Parse(tmpl))
    tpl.Execute(os.Stdout, []Foo{
        {Name: "ぶどう", Value: 1, Years: []int{201120142019}},
        {Name: "みかん", Value: 2, Years: []int{2011}},
        {Name: "ぎょうざ", Value: 3, Years: []int{2020}},
        {Name: "ビーフストロガノフ", Value: 4, Years: []int{20112019}},
        {Name: "カレーライス", Value: 5, Years: []int{2012}},
        {Name: "満漢全席", Value: 6, Years: []int{20152019}},
    })
}
html/template, text/template: implement break and continue for range … · golang/go@d0dd26a · GitHub

…loops Break and continue for range loops was accepted as a proposal in June 2017. It was implemente...

https://github.com/golang/go/commit/d0dd26a88c019d54f22463daae81e785f5867565

continue を使える様になった事で、ループのネストが下げられる様になりました。

package main

import (
    "os"
    "text/template"
)

var tmpl = `
{{- range .}}
{{- if le .Value 3 -}}{{continue}}{{end -}}
■{{.Name}}
{{- range .Years }}
  {{ . -}}年 受賞
{{- end}}
{{end -}}
`

type Foo struct {
    Name  string
    Value int
    Years []int
}

func main() {
    tpl := template.Must(template.New("").Parse(tmpl))
    tpl.Execute(os.Stdout, []Foo{
        {Name: "ぶどう", Value: 1, Years: []int{201120142019}},
        {Name: "みかん", Value: 2, Years: []int{2011}},
        {Name: "ぎょうざ", Value: 3, Years: []int{2020}},
        {Name: "ビーフストロガノフ", Value: 4, Years: []int{20112019}},
        {Name: "カレーライス", Value: 5, Years: []int{2012}},
        {Name: "満漢全席", Value: 6, Years: []int{20152019}},
    })
}

サンプルが単純な物なので効果が伝わりにくいですが、テンプレートを書く手間が少し楽になった気がします。また break が入った事で if にマッチしない部分が無駄にループを回す事もなくなったので、Go 側から巨大な配列を事前にカットして渡す必要も無くなる事になります。

Posted at by



2021/09/21


みなさん Fuzz testing ってご存じでしょうか。

人間が作る物は必ずといっていいほどバグが存在します。そしてそのコードをテストする人間も必ずバグを見逃します。

想定していなかった境界値テスト等、人間には先入観という物があり、それが邪魔をして簡単にバグを見逃します。昨今、この様な誰も気付かなかったバグの隙間を突く様な脆弱性が沢山見つかっています。

物によっては重大インシデントに発展する物まであります。

こういった人間では想定できない様なバグを見付けてくれるのが Fuzz testing です。Fuzz testing を実施する事で、ソフトウェアは頑丈になり安全にもなりえます。

本日、Go の master ブランチに Fuzz testing の機能が入りました。

[dev.fuzz] Merge remote-tracking branch 'origin/dev.fuzz' into merge-… · golang/go@6e81f78 · GitHub

+1 −0 api/except.txt +36 −0 api/next.txt +63 −15 src/cmd/go/alldocs.go +10 −0 src/cmd/go/internal/ca...

https://github.com/golang/go/commit/6e81f78c0f1653ea140e6c8d008700ddad1fa0a5
これまで Go でテストを書いた事がある皆さんであれば、Go のテストは xxx_test.go というファイルに、 Test から始まり引数に *testing.T を持つ関数を定義される事はご存じだと思います。今回入る Fuzz はこれに似ており先頭が Fuzz で引数に *testing.F を持つ関数が実行対象となります。
package foo_test

import "testing"

func FuzzFoo(f *testing.F) {
    f.Log("all good here")
    f.Fuzz(func(*testing.T, []byte) {})
}

例えば以下の関数があったとしましょう。b のゼロ長さチェックはしていますが、2バイト必要かどうかのテストが抜けています。

func doSomething(s stringbool {
    if len(s) > 0 && s[0] == 0xff && s[1] == 0xff {
        return true
    }
    return false
}

以下の様に、f.Fuzz の第二引数に受け取りたい fuzz 値を得られる様にして実装します。

package foo_test

import "testing"

func doSomething(s stringbool {
    if len(s) > 0 && s[0] == 0xff && s[1] == 0xff {
        return true
    }
    return false
}

func FuzzFoo(f *testing.F) {
    f.Log("all good here")
    f.Fuzz(func(f *testing.T, b []byte) {
        doSomething(string(b))
    })
}

コマンドラインから以下の様に実行します。

$ go test -fuzz=.

すると以下の様に落ちる箇所を見付けてくれます。

gathering baseline coverage, elapsed: 0s, workers: 4, left: 2
fuzz: found a 31-byte crash input; minimizing...
FAIL
fuzz: elapsed: 1s, execs: 9807 (14334/sec), workers: 4, interesting: 0
--- FAIL: FuzzFoo (0.69s)
    foo_test.go:13: all good here
        foo_test.go:13: all good here
        --- FAIL: FuzzFoo (0.00s)
            testing.go:1244: panic: runtime error: index out of range [1] with length 1
                goroutine 3187 [running]:
                runtime/debug.Stack()
                    C:/go/src/runtime/debug/stack.go:24 +0x90
                testing.tRunner.func1()
                    C:/go/src/testing/testing.go:1244 +0x545
                panic({0x65c2e0, 0xc000014138})
                    C:/go/src/runtime/panic.go:814 +0x207
                github.com/mattn/go-fuzz-example_test.doSomething(...)
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:6
                github.com/mattn/go-fuzz-example_test.FuzzFoo.func1(0x0, {0xc000180000, 0x0, 0x4f37d9})
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:15 +0xfb
                reflect.Value.call({0x63c8e0, 0x67a178, 0x30}, {0x66a8b0, 0x4}, {0xc008d80090, 0x2, 0x18})
                    C:/go/src/reflect/value.go:542 +0x814
                reflect.Value.Call({0x63c8e0, 0x67a178, 0x5a33c0}, {0xc008d80090, 0x2, 0x2})
                    C:/go/src/reflect/value.go:338 +0xc5
                testing.(*F).Fuzz.func1.1(0x0)
                    C:/go/src/testing/fuzz.go:412 +0x20b
                testing.tRunner(0xc008d961a0, 0xc008d98090)
                    C:/go/src/testing/testing.go:1352 +0x102
                created by testing.(*F).Fuzz.func1
                    C:/go/src/testing/fuzz.go:401 +0x4df
                
        --- FAIL: FuzzFoo (0.00s)
            testing.go:1244: panic: runtime error: index out of range [1] with length 1
                goroutine 3189 [running]:
                runtime/debug.Stack()
                    C:/go/src/runtime/debug/stack.go:24 +0x90
                testing.tRunner.func1()
                    C:/go/src/testing/testing.go:1244 +0x545
                panic({0x65c2e0, 0xc000014180})
                    C:/go/src/runtime/panic.go:814 +0x207
                github.com/mattn/go-fuzz-example_test.doSomething(...)
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:6
                github.com/mattn/go-fuzz-example_test.FuzzFoo.func1(0x0, {0xc008d820b0, 0x0, 0x4f37d9})
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:15 +0xfb
                reflect.Value.call({0x63c8e0, 0x67a178, 0x30}, {0x66a8b0, 0x4}, {0xc008d80180, 0x2, 0x18})
                    C:/go/src/reflect/value.go:542 +0x814
                reflect.Value.Call({0x63c8e0, 0x67a178, 0x5a33c0}, {0xc008d80180, 0x2, 0x2})
                    C:/go/src/reflect/value.go:338 +0xc5
                testing.(*F).Fuzz.func1.1(0x0)
                    C:/go/src/testing/fuzz.go:412 +0x20b
                testing.tRunner(0xc008d96340, 0xc008d98120)
                    C:/go/src/testing/testing.go:1352 +0x102
                created by testing.(*F).Fuzz.func1
                    C:/go/src/testing/fuzz.go:401 +0x4df
                
    
    Crash written to testdata\fuzz\FuzzFoo\e3443030b91d09bbdd7fbe67dbbbc3093401b277aa711c82dd97ee80606f56ab
    To re-run:
    go test github.com/mattn/go-fuzz-example -run=FuzzFoo/e3443030b91d09bbdd7fbe67dbbbc3093401b277aa711c82dd97ee80606f56ab
FAIL
exit status 1
FAIL    github.com/mattn/go-fuzz-example    1.012s

バイト列以外の mutate に関しては現在は TODO の様です。

実装しておくだけで勝手にバグを見付けてくれるのですから、とても便利ですね。ぜひ活用していきましょう。

Posted at by



2021/05/04


Go ではポインタは扱えるけれど、ポインタ演算は扱えないというのが共通認識でした。もちろん unsafe.Pointeruintptr を使う事で、出来なくはなかったのですが簡単ではありませんでした。

package main

import (
    "unsafe"
)

type foo struct {
    k int64
    v int64
}

func main() {
    f := &foo{3,4}

    // unsafe.Pointer() で匿名ポインタにして
    // uintptr() で演算可能にして
    // +8 バイト(64bit)足して
    // unsafe.Pointer で匿名ポインタに戻して
    // そこにはフィールド v があるはずなので *int64 にキャストして
    // デリファレンスすれば出来上がり
    *(*int64)(unsafe.Pointer((uintptr(unsafe.Pointer(f))+8))) = 5 // グヒヒ

    println(f.v) // 5
}

Go 1.17 からは unsafe.Addunsafe.Slice が導入される事で、少しだけ簡単にポインタ演算そしてポインタからスライスを復元する事ができる様になります。(参照)

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    b := []byte{12345}
    pb := &b[3]
    fmt.Println(unsafe.Slice(pb, 2)) // 4, 5

    // sizeof(byte) 分ポインタをずらす
    pb = (*byte)(unsafe.Add(unsafe.Pointer(pb), 1))
    fmt.Println(*pb) // 5

    v := []int{23456}
    pi := &v[3]
    // オフセット3からのスライスを得る
    fmt.Println(unsafe.Slice(pi, 2)) // 5, 6

    // sizeof(int) 分ポインタをずらす
    pi = (*int)(unsafe.Add(unsafe.Pointer(pi), 8))
    fmt.Println(*pi) // 6
}

もちろん不正なオフセットを指定して unsafe.Add を呼び出してデリファレンスしたり、不正なサイズを指定してスライスを復元したりすると panic する事になります。

Posted at by