- 投稿日:2020-02-17T23:57:42+09:00
JavaとGoの値渡し/参照渡しを整理する
自分はこれまでにJava, Goを勉強してきた。この二言語における値渡し/参照渡しをここで整理してしまう。
そもそも定義
値渡し
(call by value): メソッドの引数に、新たに値のコピーを作って渡す方法のこと。参照渡し
(call by reference): メソッドの引数に、変数そのものの参照を渡す方法のこと。Javaの場合
- メソッドへの引数は100%
値渡し
で渡される。- が、Javaのオブジェクトが変数に収納されている時、そこに収納されているのは「そのオブジェクトが収納された場所を示す
参照値
(reference value)」である- よって、オブジェクトを引数として渡すときにはその
参照値
(reference value)のコピーが渡されている。- この結果、「引数に渡したオブジェクトが参照渡しで書き換えられている」かのように勘違いしそうになる。
Goの場合
- ポインタを引数として渡すことで
参照渡し
を実現する。- Golangの構造体/Arrayは(Javaのオブジェクトとは違って)
値型
。すなわちその全てのフィールド/要素を収納する場所を確保し、それを構造体を表す変数に収納しているイメージ。- よって構造体を
値渡し
すると、そのフィールドの全てがコピーされる。これを書き換えても元の構造体は書き変わらない。- 一方で、map/sliceは(Javaのオブジェクトと同様)
参照型
。すなわち変数に入っているのは要素のポインタが入った容器であり、各要素にアクセスするにはポインタを辿っているイメージ(参考)。- このため、これらを
値渡し
したとしても、元のmap/sliceが書き換わってしまう。よって、普通これらをポインタ渡し
する意味はなく、普通そうしない(参考)。補足
参照渡し ≒ ポインタ渡し
であるかのように書いたが、そこの細かいところはこちらに詳しい。値型
/参照型
の説明は正確ではないかもしれないのであくまでイメージとして捉えてほしい。参考
- もう参照渡しとは言わせない (だいぶ参考にさせていただきました)
- Goの値型と参照型
- 投稿日:2020-02-17T23:11:14+09:00
[Go 1.13~] errors.Is と errors.As の違いについてお気持ちを理解する
TL;DR
これまで Go 1.13 から登場した
errors.As
の使い所があまり良くわからなかったのですが,
全ての答えは公式 Doc にありました.In the simplest case, the errors.Is function behaves like a comparison to a sentinel error, and the errors.As function behaves like a type assertion.
errors.Is
: 特定のエラーとの比較errors.As
: エラーに対する型アサーションここでは実例をもとに理解を深めていきたいと思います.
実例
error.Is
errors.Is
はあるエラーが特定のエラーを Wrap したものかを判別するためのものです.import ( "fmt" "errors" ) ErrFoo := errors.New("foo error") func main() { wrapped := fmt.Errorf("wrapped woo: %w", ErrFoo) if errors.Is(wrapped, ErrFoo) { fmt.Println("this error is caused by %v", ErrFoo) } }こんな感じで特定のエラーの起源をたどって,例外処理ができます.
比較的単純で「なるほど,便利.」という感じです.実例
errors.As
errros.As
は冒頭でも書いたとおり,エラーに対する型アサーションです.
言い換えると自前でエラー interface を書いたときに,ビルトインのエラーなのか自前で用意したエラーなのかを判別するときに使えます.
また,その付加効果として(こっちがメインかもしれないが),動的に自前のエラー独自のメソッドを利用することができます.少し長いですがここでは go-playground/validator を用いた例について紹介します.
コード自体は基本的にmain
部に注目してくれればよいです.import ( "fmt" "errors" "github.com/go-playground/validator/v10" ) type Foo struct { NaturalNumber int `validate:"gt=0"` } func validateFoo(f *Foo) error { v := validator.New() return v.Struct(f) } func buildFoo(n int) (*Foo, error) { foo := &Foo{NaturalNumber: n} if err := validateFoo(foo); err != nil { return nil, fmt.Errorf("cannot build Foo: %w", err) } return foo, nil } func main() { foo, err := buildFoo(-1) if err != nil { var verrs validator.ValidationErrors if errors.As(err, &verrs) { for _, verr := range verrs { fmt.Println(verr) } return } fmt.Printf("unknown error is occurred: %v", err) return } fmt.Printf("this is %v", foo) }
go-playground/validator
はいわゆる validation エラーを扱うためのパッケージで,ValidationErrors
という独自のエラーを内部で持っています.ここで
buildFoo
はFoo
インスタンスを作成するための関数で,validation エラーが起きた場合はそれを Wrap して返すものとします.
よって,返ってくるエラーは特定のエラーインスタンスを Wrap したものではなく,ValidationErrors
という interface を満たしたものを Wrap していることに注意してください.つまり,エラーインスタンスに対する比較であるerrors.Is
はここで用いることはできません.そこで型アサーションの役割を果たす
errors.As
を使います.
errors.As
ではerr
を UnWrap していくとverr
(ValidationErrors
) として扱うことができるかということをチェックします.
もし可能なのであれば,errors.As
はtrue
を返しつつ,verr
に Wrap して出てきたValidationErrors
を代入します.このため if 文の中ではverrs
はerr
から抽出された実態を持たせて扱うことができ,動的にValidationErrors
の機能を用いることができます.
実際に上記コードの if 文内の for 文はValidationErrors
が持っている機能です.まとめ
実例を見てみると,
errors.Is
: 特定のエラーとの比較errors.As
: エラーに対する型アサーションというのはかなりしっくりきます.
加えてerrors.As
は動的に interface 独自の処理を扱えるようになるので良いですね.参考
- 公式 Doc
- Go 1.13時代のエラー実装者のお作法: 実装面の解説でとてもわかりやすかったです!
- 投稿日:2020-02-17T22:23:27+09:00
【Golang】テンプレートを簡単に使える外部ライブラリを見つけた (echo)
前提
- 言語は、Golang。フレームワークは echo を使用しております。
- templateをより簡単に使えるパッケージを紹介しています。
詳細
https://github.com/foolin/echo-templateDeprecated!!!
Please consider trying to migrate to Goview↓
https://github.com/foolin/goview
はじめ、前者の echo-template を発見しましたが、後者の goview に統合されたらしく、後者を使用しました。一応、 前者も使えます。
echo, Gin, Go-chi, go.rice 等にサポートしているらしいです。インストール
go get github.com/foolin/echo-template
↓
go get github.com/foolin/goview/supports/echoview-v4
diff
どれだけ簡略化されるのか。diff形式で。
diff(main.go)package main import ( - "html/template" - "github.com/labstack/echo" + "github.com/foolin/goview/supports/echoview" "net/http" ) -type Template struct { - templates *template.Template -} - -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - func main() { // Echo instance e := echo.New() - t := &Template{ - templates: template.Must(template.ParseGlob("views/*.html")), - } - e.Renderer = t + e.Renderer = echoview.Default() e.GET("/page", func(c echo.Context) error { return c.Render(http.StatusOK, "page.html", echo.Map{"title": "Page file title!!"})diff元のソースコードは下に貼っておくので、気になったら見てください。
結果
コードを省略できる。
備考
呼び出しについて
.html
や.tpl
等、拡張子無しでRenderする場合は、/views/layouts/master.html
が呼び出される。ソースコード
通常
main.gopackage main import ( "html/template" "github.com/labstack/echo" "net/http" ) type Template struct { templates *template.Template } func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } func main() { // Echo instance e := echo.New() t := &Template{ templates: template.Must(template.ParseGlob("views/*.html")), } e.Renderer = t e.GET("/page", func(c echo.Context) error { return c.Render(http.StatusOK, "page.html", echo.Map{"title": "Page file title!!"}) }) // Start server e.Logger.Fatal(e.Start(":9090")) }echoviewを用いた場合
main.go(echoview)package main import ( "github.com/labstack/echo" "github.com/foolin/goview/supports/echoview" "net/http" ) func main() { // Echo instance e := echo.New() e.Renderer = echoview.Default() e.GET("/page", func(c echo.Context) error { return c.Render(http.StatusOK, "page.html", echo.Map{"title": "Page file title!!"}) }) // Start server e.Logger.Fatal(e.Start(":9090")) }その他
マークダウンで ```diff ってやると、差分にちゃんと色が着くってことに驚いている。
- 投稿日:2020-02-17T22:15:31+09:00
handy-spannerを使ってspannerのテストをしよう
はじめに
こんにちわ、すえけん(@sueken5)です。この記事ではspannerのエミュレータであるhandy-spannerを使ってspannerのコードをテストするやり方を紹介します。
handy-spanner
handy-spannerはgcpugで管理されているプロジェクトで非公式ではありますがspannerをエミュレートするツールです(リンク)。
できることとできないことがREADME.mdに書かれているのでチェックしておきましょう。
built in server
handy-spannerはgolangで書かれていてテストプロセスの中でエミュレータを起動して使うことができます。なのでDockerなどを用意する必要がないためとても簡単にテストできます。
//https://github.com/gcpug/handy-spanner/blob/master/fake/example_test.goより抜粋 func ExampleSpannerClient() { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" // Run fake server srv, conn, err := fake.Run() if err != nil { log.Fatal(err) } defer srv.Stop() defer conn.Close() // Prepare spanner client client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { log.Fatalf("failed to connect fake spanner server: %v", err) } // Use the client _, _ = client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Test", []string{"ColA", "ColB"}, []interface{}{"foo", 100}, ), }) // output: }実際のテストコード
//init_test.go package spanner_test import ( "fmt" "log" "os" "testing" "github.com/gcpug/handy-spanner/fake" "google.golang.org/grpc" ) var ( testServer *fake.Server testConnection *grpc.ClientConn ) func TestMain(m *testing.M) { fmt.Println("test main") // Run fake server server, conn, err := fake.Run() if err != nil { log.Fatal(err) } defer server.Stop() defer conn.Close() testServer = server testConnection = conn ret := m.Run() os.Exit(ret) }
TestMain
でhandy-spannerサーバーを起動しておきます。func TestGet_OK(t *testing.T) { databaseID := "get-ok" dbName := fmt.Sprintf(dbFormat, projectID, instanceID, databaseID) ctx := context.Background() testCase := struct { input []byte key string expect []byte }{ input: []byte("OK"), key: "test", expect: []byte("OK"), } if err := createDatabase(context.Background(), dbName); err != nil { panic(err) } //fakeサーバー起動時に渡されたコネクションを使ってクライアント生成 client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(testConnection)) require.Nil(t, err) s := &spanner.Store{ PKey: testCase.key, Value: testCase.input, } mut := s.Insert(ctx) _, err = client.Apply(ctx, []*libspanner.Mutation{mut}) require.Nil(t, err) actual, err := Get(ctx, testCase.key) require.Nil(t, err) assert.Equal(t, testCase.expect, actual) }
TestMain
でサーバーを起動した時に生成されたコネクションを使ってspannerのクライアントを生成します。あとはいつも通りテストするだけです。まとめ
簡単にテストできたのでとてもよかったです。
- 投稿日:2020-02-17T15:01:20+09:00
Golangのネットワーク関連ライブラリを雑多にまとめる
Golangのネットワーク関連のライブラリを自分用にざっとメモ。
HTTPリクエストを飛ばす
簡単な方法
// リクエスト resp, _ := http.Get("http://example.com") // そのリスポンスをprint defer resp.Body.close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(endpoint)難しい方法
// request structの作成 req, _ := http.NewRequest( "Post", "https://test", bytes.NewBuffer([]byte("request body"), ) // そのheaderを指定 req.Header.Add("Content-Type", "application/json") // クライアントを初期化 var client *http.Client = &http.Client{} // そこからリクエストを飛ばす resp, _ := client.Do(req) // そのリスポンスをprint body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body))エンドポイントURLをいじる
base, _ := url.Parse("https://test/mypage/") reference, _ := url.Parse("myPicture?a=1&b=2") endpoint := base.ResolveReference(reference).String() // URLを結合する // クエリパラメタの確認 req, _ := http.NewRequest("GET", endpoint) fmt.Println(q:= req.URL.Query()) // map fmt.Println(q.Encode()) //クエリ形式の文字列MarshalとUnmarshal
- Marshalは構造体→JSON文字列のバイト配列
- UnmarshalはJSON文字列のバイト配列→構造体
type Person struct { Name string `json:"name"` // Marshal時のkey名をnameに Age int `json:"age,string"` // Marshal時valueがstringに Friends int `json:omitempty` // Marshal時valueがempty(0)だと無視される password string `json:-` // Marshal時無視される } func main() { b := []byte{`{"name":"John", "age":25, "nickname": "Johnny"}`} var p Person // byte文字列をPerson構造体へMarshalする (属性名はCaseInsensitive) if err := json.Unmarshal(b, &p); err!= nil { fmt.Println(err) } // Person構造体をbyte文字列へUnmarshalする v, err := json.Marshal(p) fmt.Println(string(v)) }
- Person構造体のカスタム(アン)マーシャルを定義。Person構造体に以下のメソッドが存在すると、
json.Marshal(Person p)
やjson.Unmarshal(b, &p)
が呼び出された際、以下が使われる。// カスタムマーシャル func (p Persion) MarshalJSON() ([]byte, error) { // nameだけをマーシャル v, err := json.Marshal(&struct{ Name string }{ Name: "Mr." + p.Name, } return v, err } // カスタムアンマーシャル func (p Person) UnMarshalJSON(b []byte) error { // まずPerson2という構造体を定義し、Person2でアンマーシャル type Person2 struct { Name string } var p2 Person2 err := json.Unmarshal(b, &p2) // それを使いpに放り込む p.Name = "Mr. " + p2.Name return err }HMAC(Hash-based Message Authentication Code)
HMACの仕組み
事前に
APIKey
とSecretKey
(秘密鍵=publicKeyではない!!!)を発行し、その両方をクライアントサイドに保存するブラウザからサーバへ以下を送信する
APIKey
バイト配列のデータ
HMAC
= SecretKeyを用いハッシュ関数を利用してデータを暗号化したもの。HeaderのSignatureに、使用したハッシュ関数の種類とともに収納する。用いたハッシュ関数の種類
(SHA-1、MD5など)サーバサイドでは以下の手順でデータが改竄されていないことを確認する
- APIKeyに対応するSecretKeyを取り出す
- MACをその秘密鍵でdecryptし、それが「データ」と一致するか確かめる
Golangで実装
// クライアントサイド func main() { const apiKey = "api_key" const apiSecret = "api_secret" data = []byte("data") h := hmac.New(sha.New, []byte(apiSecret)) h.Write(data) sign := hex.EncodeToString(h.Sum(nil)) // これをハッシュ関数サーバへ送る } // サーバサイド func Server() { // 受け取ったapiKeyとHMACで... apiSecret := DB[apiKey] h := hmac.New(sha256.New, []byte(apiSecret)) h.Write(data) expectedMAC = hex.EncodeToString(h.Sum(nil)) // これが受信したデータと一致するか確認 }
- 投稿日:2020-02-17T11:51:23+09:00
goenvでGo1.13.6をインストールメモ
背景
homebrewでインストールしたgoenvをupgradeしたがインストールできるバージョンの最大が1.12だったので使いたかった1.13.6が使えなかった。
$ goenv install -l Available versions: [省略] 1.10.7 1.11.0 1.11beta2 1.11beta3 1.11rc1 1.11rc2 1.11.1 1.11.2 1.11.3 1.11.4 1.12beta1作業メモ
goenvのディレクトリとbrewのgoenvを一旦削除
$ rm -rf `goenv root` $ brew uninstall goenvgoenv/INSTALL.mdの手順通りにコマンド実行
$ git clone https://github.com/syndbg/goenv.git ~/.goenv $ echo 'export GOENV_ROOT="$HOME/.goenv"' >> ~/.bash_profile $ echo 'export PATH="$GOENV_ROOT/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(goenv init -)"' >> ~/.bash_profile $ echo 'export PATH="$GOROOT/bin:$PATH"' >> ~/.bash_profile $ echo 'export PATH="$PATH:$GOPATH/bin"' >> ~/.bash_profile $ eval "$(goenv init -)" $ brew install goenvインストールされている事を確認
$ goenv install -l Available versions: [省略] 1.11.12 1.11.13 1.12.0 1.12beta1 1.12beta2 1.12rc1 1.12.1 1.12.2 1.12.3 1.12.4 1.12.5 1.12.6 1.12.7 1.12.8 1.12.9 1.12.10 1.12.11 1.12.12 1.12.13 1.12.14 1.12.15 1.12.16 1.12.17 1.13.0 1.13beta1 1.13rc1 1.13rc2 1.13.1 1.13.2 1.13.3 1.13.4 1.13.5 1.13.6 1.13.7 1.13.8 1.14beta1 1.14rc1Go1.13.6をインストール
$ goenv install 1.13.6 Downloading go1.13.6.darwin-amd64.tar.gz... -> https://dl.google.com/go/go1.13.6.darwin-amd64.tar.gz Installing Go Darwin 64bit 1.13.6... Installed Go Darwin 64bit 1.13.6 to /Users/xxxxxxxxx/.goenv/versions/1.13.61.13.6が使えるようにする
$ goenv global 1.13.6 $ goenv rehash $ goenv versions system * 1.13.6アップグレードする場合
$ cd ~/.goenv $ git pull参考