20220402のGoに関する記事は5件です。

【Golang】Structオリエンテッドまとめ

39 メソッドとポインタレシーバーと値レシーバー メソッド 構造体の値を関数で使用する場合 package main import "fmt" type Ver struct { X, Y int…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nil かもしれないメンバのある構造体の整列

これは何? という記事を見て、自分ならどう書くかなと思って書いたもの。 generics の練習を兼ねて まずはソース go1.18 package main import ( "encoding/json" "fmt" "math/rand" "sort" "golang.org/x/exp/constraints" ) type Sample struct { Name *string Description *string Note *string } func ToPtr[T any](x T) *T { return &x } func NewSample(name, desc, note *string) *Sample { return &Sample{ Name: name, Description: desc, Note: note, } } func comparePtr[T constraints.Ordered](x, y *T) int { if x == y { return 0 } if x == nil { return -1 } if y == nil { return 1 } if *x == *y { return 0 } if *x < *y { return -1 } return 1 } func compareSampleMemberStrPtr(sx, sy *Sample, proc func(*Sample) *string) int { return comparePtr(proc(sx), proc(sy)) } func SampleCompare(a, b *Sample) int { if c := compareSampleMemberStrPtr(a, b, func(s *Sample) *string { return s.Name }); c != 0 { return c } if c := compareSampleMemberStrPtr(a, b, func(s *Sample) *string { return s.Description }); c != 0 { return c } return compareSampleMemberStrPtr(a, b, func(s *Sample) *string { return s.Note }) } func show(title string, samples []*Sample) { fmt.Println(title) for _, v := range samples { jsonBytes, err := json.Marshal(v) if err != nil { panic(err) } fmt.Println(" " + string(jsonBytes)) } } func shuffled(i int) []int { s := make([]int, i) for ix := 0; ix < i; ix++ { s[ix] = ix } rand.Shuffle(i, func(x, y int) { s[x], s[y] = s[y], s[x] }) return s } func main() { samples := []*Sample{} for _, k := range shuffled(27) { name := []*string{ToPtr("名前01"), ToPtr("名前02"), nil}[k%3] desc := []*string{ToPtr("説明01"), ToPtr("説明02"), nil}[(k/3)%3] note := []*string{ToPtr("メモ01"), ToPtr("メモ02"), nil}[(k/9)%3] samples = append(samples, NewSample(name, desc, note)) } show("before sorting", samples) sort.Slice(samples, func(i, j int) bool { return SampleCompare(samples[i], samples[j]) < 0 }) show("after sorting", samples) } ちょっと説明 ToPtr[T any] 何かを何かへのポインタに変換する。 変換元へのポインタではなく、変換元のコピーへのポインタになるので要注意だけど、便利。 そもそも go で &("hoge") って書かせてくれれば要らなくなる関数だけど、いまは書けないので。 comparePtr[T constraints.Ordered] nil は非 nil よりも小さい、という論理で比較する。 両方とも非 nil なら、ポインタのサス先の値の大小関係を使う。 比較結果は -1, 0, 1 で表現する。 compareSampleMemberStrPtr Sample のメンバ(じゃなくてもいいけど)である文字列へのポインタを比較する。 メンバを参照するための関数を受けるのは go1.18 comparePtr(a.Name, b.Note) みたいなミスを避けるため。 C++ なら「メンバへのポインタ型」の値を使うんだけどそういうの無いからね。 SampleCompare 主要部はこれ。 メンバを一個比較して、等しかったら次のメンバを比較、また等しかったら次のメンバを比較。 という流れ。 今回はループにしなかったけど、ループにするのも悪くない。 書いてみて思ったこと やっぱりなんか go で書くと長くなるよなと思う。 go1.18 if *x == *y { return 0 } if *x < *y { return -1 } return 1 は、C/C++ だと C / C++ return (*y<*x) - (*x<*y); だよなぁとか思ったり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XMLをスライスにUnmarshalするときの罠

スライスを Marshal して Unmarshal すると、先頭要素しか取得できません! スライスをもとに戻せない? package main import ( "encoding/xml" "fmt" ) func main() { nums := []int{1, 2, 3} b, _ := xml.Marshal(nums) var copiedNums []int _ = xml.Unmarshal(b, &copiedNums) // 先頭要素しか取れない! fmt.Printf("%#v\n", copiedNums) // []int{1} } JSONのように Marshal と Unmarshal を逆変換として使うことはできません。XMLをオブジェクトのシリアライズに使っている場合は注意が必要です。 バージョン Go 1.18 Why? スライスをmarshalすると各要素が並んだ形式で出力されます。 []int{1, 2, 3} → "<int>1</int><int>2</int><int>3</int>" しかし、unmarshalのデコーダーは最初の要素を読み終えた時点で完結したxmlと判断し終了してしまいます。 <int>1</int> ここでXMLが完結した! → []int{1} その結果、元々いくつ要素があろうが先頭要素しか取得できません。 issueも上がっていますが、Go1の間は後方互換を保証するので実現は難しいかもしれません... 対処法 構造体でラップする 最上位の要素が1つであればいいので、[]int フィールドを持つ構造体をmarshal, unmarshalすれば全要素コピーできます。 package main import ( "encoding/xml" "fmt" ) // ラッパー type IntSlice struct { Nums []int } func main() { nums := IntSlice{[]int{1, 2, 3}} b, _ := xml.Marshal(nums) fmt.Println(string(b)) // "<IntSlice><Nums>1</Nums><Nums>2</Nums><Nums>3</Nums></IntSlice>" var copiedNums IntSlice // IntSliceタグで包まれるので、unmarshalはXMLを最後(IntSliceの閉じタグ)まで読んでくれる if err := xml.Unmarshal(b, &copiedNums); err != nil { fmt.Printf("error: %+v\n", err) return } fmt.Printf("%#v\n", copiedNums) // IntSlice{Nums:[]int{1, 2, 3}} } 自分で Unmarshal を実装する 以下の記事では、スライスのdefined typeにUnmarshalメソッドを生やし、XMLを最後まで読み込むようにする方法が紹介されていました。 おわりに 機会があって、XMLを使ったディープコピーを検討していたらがっつりはまりました... 構文も良く分からずJSONのノリで使っていたしっぺ返しでもあるので、これを機に勉強しようと思います
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GolangでPKI入門 - 6

1.この記事の対象の人 Golang で DN (Subject や Issuer) を扱いたい人 Golang で DN (Subject や Issuer) を扱うライブラリを探している人 2…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go 1.18 を触ってみた(Generics, Fuzzing, Workspaces)

初めに Go 1.18がリリースされたので触ってみました。 今回の重要なリリースは下記の The Go Blog に掲載しているものだと思ったので、ここに書いてあるものを触ってみました。 主な改善点 上記のページを見ると、今回のバージョンアップで特に注目すべきものは以下になります。 Generics Fuzzing Workspaces 20% Performance Improvements それぞれコードを交えながら、触ってみた感想と解説をしていこうと思います。 Generics 日本語だと「ジェネリクス」と読みます。Javaを触ったことある人にとっては知ってて当たり前のものではないでしょうか。 Generics とは、1つのソースコードで様々なデータ型の処理をできるようにするものです。 The Go Blog には、最も頻繁にリクエストされていた機能だと書いてあります。つまりこの機能は多くの人が待ち望んでいた機能だと言えます。 親切にチュートリアルまで用意してくれています。今回はこのチュートリアルを通じて Generics に触れてみました。 Generics の解説で使用したコードは全てこのリンクが出典元となっています。 基本的な使い方 言葉よりコードを見ていただいたほうが理解しやすいと思います。まずは Generics を使っていない下記のコードを見てください。 // SumInts adds together the values of m. func SumInts(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } // SumFloats adds together the values of m. func SumFloats(m map[string]float64) float64 { var s float64 for _, v := range m { s += v } return s } int64 の合計を出す関数と、float64 の合計を出す関数が書かれています。 型は違いますが2つとも処理の内容は同じです。型が違うだけで同じコードを複数用意するのは面倒です。 Generics ならこの問題を解決してくれます。 // SumIntsOrFloats sums the values of map m. It supports both int64 and float64 // as types for map values. func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s } この関数だけで int64 にも float64 にも対応可能となっています。 コード内の [K comparable, V int64 | float64] は、type parameter(型パラメータ)と呼びます。これによって、いくつかの型をあらかじめ宣言しておき、宣言した型の引数しか使えない制約を付与することができます。 int64 と float64 の指定は理解できるが、comparable って何?という方もいるでしょう。これはGo言語が用意したインターフェースで、日本語にすると「比較対象になる」という意味になります。 つまり、comparable は==や!=のような比較演算子を利用できる全ての型を表しています。 map は key の重複がないように値を保管していくデータ構造なため、比較が可能な値を key にする必要があります。 そのため、comparable というインターフェースが利用されています。 ちなみに、関数内のコードが全ての型パラメータに対して動作しなければコンパイルが通らないそうです。 型推論の利用 Generics には型推論も機能しています。 ただ、関数を作成するときではなく呼び出すときに使えるものっぽいです。 func main() { ints := map[string]int64{ "first": 34, "second": 12, } // 型推論なし SumIntsOrFloats[string, int64](ints) // 型推論あり SumIntsOrFloats(ints) } 型制約の代わりのインターフェース 前のコードで型制約をする時に、下記のように記述しました。 func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { // ~ 略 ~ } 型制約をする時に、int64 | float64と記述していますが、複雑さの原因になりそうです。 今回の場合は2つなので問題ありませんが、もっと多くの型を指定すると可読性が落ちそうです。 そこで利用できるのがインターフェースです。 インターフェースを利用した場合、下記のようなコードになります。 type Number interface { int64 | float64 } // SumNumbers sums the values of map m. It supports both integers // and floats as map values. func SumNumbers[K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s } Generics で利用する型の宣言を分離化できるため、複雑性を解消できます。 以上、Generics の紹介でした。 上記のコードは紹介したチュートリアルに全て載っています。 チュートリアルの最後に今回使用した全てのコードが載っているので、コードだけ確認したい方はそちらをご覧ください。 また、この記事により細かく Generics について紹介がされているので、もしよろしければ参考にしてください。 Fuzzing 日本語だと「ファジング」と読みます。 Fuzzing とは、プログラムに意図的に無効なデータ(ファズ)を送り、例外的な状況でも問題なく動くか確かめるソフトウェアテストの一種です。 Go の Fuzzing では下記のことができるようです。 This tutorial introduces the basics of fuzzing in Go. With fuzzing, random data is run against your test in an attempt to find vulnerabilities or crash-causing inputs. Some examples of vulnerabilities that can be found by fuzzing are SQL injection, buffer overflow, denial of service and cross-site scripting attacks. 出典: https://go.dev/doc/tutorial/fuzz 意訳すると、ランダムにデータを走らせて脆弱性やクラッシュの原因となるインプットを見つけると書いてあります。これによってSQLインジェクション、バッファオーバーフロー、サービス拒否攻撃、クロスサイトスクリプティングなどに対する脆弱性を発見できます。 Fuzzing でも親切にチュートリアルを用意してくれています。なので ここもチュートリアルを通じて学習していきました。 これから記載する解説コードは全てこのリンクが出典元となっています。 Fuzzing テストコードの書き方 Fuzzing はテストに使用するため、まずはテスト検証用のコードを用意します。 下記のコードでは文字列を与えたときに順番を逆にする関数を用意しています。 main.go package main import "fmt" // 文字列の順番を逆にする関数 func Reverse(s string) string { b := []byte(s) for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { b[i], b[j] = b[j], b[i] } return string(b) } func main() { input := "The quick brown fox jumped over the lazy dog" rev := Reverse(input) doubleRev := Reverse(rev) fmt.Printf("original: %q\n", input) fmt.Printf("reversed: %q\n", rev) fmt.Printf("reversed again: %q\n", doubleRev) } Fuzzing を利用する前に、普通の単体テストを使ってみましょう。 作成した関数に対する単体テストは以下になります。 reverse_test.go package main import ( "testing" ) func TestReverse(t *testing.T) { testcases := []struct { in, want string }{ {"Hello, world", "dlrow ,olleH"}, {" ", " "}, {"!12345", "54321!"}, } for _, tc := range testcases { rev := Reverse(tc.in) if rev != tc.want { t.Errorf("Reverse: %q, want %q", rev, tc.want) } } } 上記のコードを用意すれば、変数 testcases に検証したいinputと出力してほしい文字を記載するだけでテストができます。 しかし、考えついたテストケースのみしかテストが行われないため、検証しきれていないケースが存在する可能性があります。 そこで Fuzzing を使い、考えつかなかったテストケースからエッジケースの識別を行います。 Fuzzing を利用したコードは以下になります。 ファイル名は単体テストと同じ *_test.go になります。 reverse_test.go package main import ( "testing" "unicode/utf8" ) func FuzzReverse(f *testing.F) { testcases := []string{"Hello, world", " ", "!12345"} for _, tc := range testcases { f.Add(tc) // Use f.Add to provide a seed corpus } f.Fuzz(func(t *testing.T, orig string) { rev := Reverse(orig) doubleRev := Reverse(rev) if orig != doubleRev { t.Errorf("Before: %q, after: %q", orig, doubleRev) } if utf8.ValidString(orig) && !utf8.ValidString(rev) { t.Errorf("Reverse produced invalid UTF-8 string %q", rev) } }) } 普通の単体テストと違う点は 関数名が FuzzXxx となっている 引数が *testing.F となっている t.Run ではなく f.Fuzz を実行している f.Fuzz のことをファズターゲットと呼ぶ パラメータは*testing.Tとテストに利用するデータの型を指定した関数。ここにテストの内容を記述していく テストに利用するデータの型の指定(今回の場合 orig string)は、引数名の決まりはなく、型もstring以外のものを指定できる f.Add というものがある。ここで seed corpus(テストに渡すデータセット) を追加 f.Fuzz(func(t *testing.T, orig string) を行う前に記述する必要がある。f.Fuzz で指定されている型(このコードの場合はstring型)と異なる型の場合、エラーとなる 上記を踏まえて、今回のコードで何をしているかまとめると以下のようになります。 f.Add(tc) によって Hello, world、 、!12345 の3つはテストで行われるようになる。 string型のデータをランダムに作成してテストを行う。 Reverse関数を行った後にもう一度Reverse関数を行い、元のデータと一致する確かめる。 UTF-8にエンコードされているか確かめる Fuzzing テストを実行してみる Fuzzing を利用したテストを行う場合、go test -fuzz=Fuzz というコマンドを使えばできます。 このコマンドにより、ランダムに生成された文字列入力が行われ、失敗したら seed corpus file(テストに渡すデータセットのファイル) を生成して失敗したデータを書き込んでくれます。 また、testdata / fuzz / 関数名(今回の場合FuzzReverse) というディレクトリも自動で作成し、FuzzReverseの中に corpus file を入れてくれます。 失敗の原因となった文字列を確認したい場合、このファイルを見れば確認できます。 % go test -fuzz=Fuzz fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 12 workers fuzz: minimizing 33-byte failing input file fuzz: elapsed: 0s, minimizing --- FAIL: FuzzReverse (0.08s) --- FAIL: FuzzReverse (0.00s) reverse_test.go:20: Reverse produced invalid UTF-8 string "\x94\xdf" Failing input written to testdata/fuzz/FuzzReverse/a430e9f383c554b44a13dbaa6dbc3609ad0c48c4e94923b6845b42b07d9392de To re-run: go test -run=FuzzReverse/a430e9f383c554b44a13dbaa6dbc3609ad0c48c4e94923b6845b42b07d9392de FAIL exit status 1 FAIL example/fuzz 0.321s メッセージを確認すると、Reverse をして生み出したものがUTF-8 にとって無効なものだったから失敗したぽいです。 go test -fuzz=Fuzz を行なったあとのディレクトリ構造 . ├── go.mod ├── main.go ├── reverse_test.go └── testdata └── fuzz └── FuzzReverse └── a430e9f383c554b44a13dbaa6dbc3609ad0c48c4e94923b6845b42b07d9392de 自動生成されたファイル(a430e9f383c554b4...)の中身 go test fuzz v1 string("ߔ") go test -fuzz=Fuzz でファイルが作成されると、オプションなしで go test を行うときに、そのファイルの中にある文字列もテスト対象にしてくれます。 % go test --- FAIL: FuzzReverse (0.00s) --- FAIL: FuzzReverse/a430e9f383c554b44a13dbaa6dbc3609ad0c48c4e94923b6845b42b07d9392de (0.00s) reverse_test.go:20: Reverse produced invalid UTF-8 string "\x94\xdf" FAIL exit status 1 FAIL example/fuzz 0.246s ちなみに go test -fuzz=Fuzz はテストが失敗しない限り永遠に実行されます。止めるときは ctrl-C を使います。 また、-fuzztime [秒数]s というオプションを使えば、指定した時間の間テストを実行し続け、時間が経ったら終了してくれるようになります。 今回使ったコードに対するデバッグ方法もチュートリアルには載っていますが、本題とは逸れるので割愛します。 気になる方はぜひチェックしてみてください。 また、より詳しい内容を見たい方は、こちらのドキュメントを読んでみてください。 Workspaces Workspaces を使えば複数のモジュールでの作業が簡単になるとのことです。 ここでもチュートリアルが用意されています。 基本的な使い方 Workspace として扱いたいディレクトリで go work init [モジュールのパス] これを行うと go.work というファイルが作成され、モジュールがそのファイルに登録されます。 それでは実際にコードを使って見ていきましょう。 下記のディレクトリ構成で使うとどうなるか見ていきましょう。 . ├── hello │ ├── go.mod │ └── hello.go └── main ├── go.mod └── main.go main.go package main import "example.com/hello" func main() { hello.Hello() } hello.go package hello import ( "fmt" ) func Hello() { fmt.Println("hello") } main.go で hello.go の関数を参照する状態になっています。 この状態で main.go を実行してもエラーが発生します(mainディレクトリで実行しています。) % go run main.go main.go:3:8: no required module provides package example.com/hello; to add it: go get example.com/hello それでは go work init で go.work ファイルを作成し、2つのモジュールパスを登録します % go work init ./main ./hello go.work go 1.18 use ( ./hello ./main ) このファイルには、モジュールと Go のバージョンが記述されます。 ここに記載されたモジュールは全てメインモジュールとして扱われます。 モジュールの外部であっても、Workspace 内であればモジュールのパッケージを参照できます。 ではもう一度 main.go を実行してみます(mainディレクトリで実行します) % go run main.go hello 無事に成功しました。 このように Workspace化をさせれば他のモジュールを呼び出すことが可能になります。 go.mod ファイルで replace を使うことも可能ですが、Workspace化させたほうが管理が楽になります。 ちなみに go.work ファイルをリポジトリに登録するのは非推奨らしいです。 他のコマンド 今回は go work init のみ使用しましたが、他にもコマンドはあります。 go work use [-r] [dir] ディレクトリが存在すればworkファイルに追加、存在しなければworkファイルから削除します。 また、-r を付ければサブディレクトリを再帰的に調べます。 go work edit go.work ファイルを編集します。go mod edit と同じように利用できるとのことです。 go work sync Workspace のビルドリストを Workspace のモジュールに同期します。 ビルドリストとは、Workspace でビルドを実行するために使用されるすべての依存関係モジュールのバージョンのセットのことです。 詳しい説明は Go Modules Reference に載っているので、ぜひ参照してください。 20% Performance Improvements この見出しは機能の追加ではなく、パフォーマンスが20%向上したという報告です。 Go 1.17 で導入されたレジスタABI呼び出し規約が Apple M1、ARM64、PowerPC64 に拡張されたことにより、最大20%のCPUパフォーマンス向上となったとのことです。 最後に 以上、go1.18 について見ていきました。 どの改善点も軽く触れた程度なので、使いこなすにはもっと勉強が必要です。 引き続きキャッチアップをしていきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む