- 投稿日:2020-02-25T15:23:45+09:00
Golang context.Value() でKey周りを調べた にゃ
context.WithValue() の Key はどうなのか調べた奴
メンバーに変数がないときは気をつけよう・・・・ 構造体を切り替えればオケ(Qiitaに書くようなレベルなのだろうか・・・)
サンプルコード
ctx.gopackage main import "context" type ( plainKey struct{} empty struct{} key struct{ s string } value struct{ s string } ) var ( key1 = key{s: "k1"} key2 = key{s: "k2"} pKey1 = plainKey{} pKey2 = plainKey{} ekey = empty{} ) func main() { v1 := value{s: "one"} v2 := value{s: "two"} v3 := value{s: "three"} v4 := value{s: "four"} ctx := context.Background() ctx = context.WithValue(ctx, key1, v1) ctx = context.WithValue(ctx, key2, v2) println("Key1:", ctx.Value(key1).(value).s) println("Key2:", ctx.Value(key2).(value).s) ctx = context.WithValue(ctx, &key1, v3) ctx = context.WithValue(ctx, &key2, v4) println("Key3:", ctx.Value(&key1).(value).s) println("Key4:", ctx.Value(&key2).(value).s) v5 := value{s: "five"} v6 := value{s: "six"} ctx = context.WithValue(ctx, pKey1, v1) ctx = context.WithValue(ctx, pKey2, v2) ctx = context.WithValue(ctx, ekey, v5) println("pKey1:", ctx.Value(pKey1).(value).s) println("pKey2:", ctx.Value(pKey2).(value).s) println("ekey5:", ctx.Value(ekey).(value).s) ctx = context.WithValue(ctx, &pKey1, v3) ctx = context.WithValue(ctx, &pKey2, v4) ctx = context.WithValue(ctx, ekey, v6) println("pKey3:", ctx.Value(&pKey1).(value).s) println("pKey4:", ctx.Value(&pKey2).(value).s) println("ekey6:", ctx.Value(ekey).(value).s) }実行
go run ctx.go Key1: one Key2: two Key3: three Key4: four pKey1: two pKey2: two ekey5: five pKey3: four pKey4: four ekey6: six
- 投稿日:2020-02-25T14:19:40+09:00
COTOHA でキーワードの抽出 (Golang)
COTOHA API Portal の使用例です。
フォルダー構造
$ tree -a . ├── akai_rousoku.txt ├── .env ├── get_config.go ├── get_token.go └── key_word.gokey_word.go// --------------------------------------------------------------- // // key_word.go // // Feb/25/2020 // --------------------------------------------------------------- package main import ( "fmt" "os" "encoding/json" "net/http" "strings" "io/ioutil" ) // --------------------------------------------------------------- func key_word_proc(config map[string]interface{},doc string) { out_filename := "out01.json" // fmt.Printf("%s\n",doc) data := make (map[string]interface{}) data["document"] = doc data["type"] = "default" str_json, _ := json.Marshal(data) // fmt.Printf("%s\n",str_json) url_base := config["url_base"] fmt.Printf("%s\n",url_base) url_target := url_base.(string) + "v1/keyword" fmt.Printf("%s\n",url_target) req, _ := http.NewRequest("POST", url_target, strings.NewReader(string(str_json))) req.Header.Set("Content-Type","application/json") req.Header.Set("Authorization", "Bearer " + config["access_token"].(string)) client := new(http.Client) resp, error := client.Do(req) if error != nil { fmt.Println("*** error *** client.Do ***") fmt.Println("Request error:", error) } bb, err := ioutil.ReadAll(resp.Body) if err == nil { ioutil.WriteFile (out_filename,bb,0666) var unit_aa map[string]interface{} json.Unmarshal ([]byte(string(bb)), &unit_aa) fmt.Printf("len(unit_aa) = %d\n", len(unit_aa)) unit_bb := unit_aa["result"] fmt.Println(unit_bb) } } // --------------------------------------------------------------- func main() { fmt.Fprintf (os.Stderr,"*** 開始 ***\n") file_in := os.Args[1] fmt.Printf ("%s\n",file_in) buff,_ := ioutil.ReadFile (file_in) doc := string(buff) config := get_config_proc () access_token := get_token_proc (config) config["access_token"] = access_token fmt.Printf("%s\n",config["access_token"]) // sentence := "特急はくたかで富山に向かいます。それから、金沢に行って、兼六園に行きます。" key_word_proc(config,doc) fmt.Fprintf (os.Stderr,"*** 終了 ***\n") } // ---------------------------------------------------------------get_config.go get_token.go はこちら
COTOHA API で構文解析 (Golang)実行コマンド
go run key_word.go get_config.go get_token.go akai_rousoku.txt次のJSONファイルが作成されます。
out01.json{ "result" : [ { "form" : "猿", "score" : 135.52966 }, { "form" : "蝋燭", "score" : 83.9601 }, { "form" : "花火", "score" : 78.08584 }, { "form" : "亀", "score" : 43.078 }, { "form" : "火", "score" : 42.81965 } ], "status" : 0, "message" : "" }
- 投稿日:2020-02-25T13:54:40+09:00
COTOHA で固有名詞の抽出 (Golang)
COTOHA API Portal の使用例です。
フォルダー構造
$ tree -a . ├── .env ├── get_config.go ├── get_token.go └── proper_noun.goproper_noun.go// --------------------------------------------------------------- // // proper_noun.go // // Feb/25/2020 // --------------------------------------------------------------- package main import ( "fmt" "os" "encoding/json" "net/http" "strings" "io/ioutil" ) // --------------------------------------------------------------- func proper_noun_proc(config map[string]interface{},sentence string) { out_filename := "out01.json" fmt.Printf("%s\n",sentence) data := make (map[string]interface{}) data["sentence"] = sentence data["type"] = "default" str_json, _ := json.Marshal(data) fmt.Printf("%s\n",str_json) url_base := config["url_base"] fmt.Printf("%s\n",url_base) url_target := url_base.(string) + "v1/ne" fmt.Printf("%s\n",url_target) req, _ := http.NewRequest("POST", url_target, strings.NewReader(string(str_json))) req.Header.Set("Content-Type","application/json") req.Header.Set("Authorization", "Bearer " + config["access_token"].(string)) client := new(http.Client) resp, error := client.Do(req) if error != nil { fmt.Println("*** error *** client.Do ***") fmt.Println("Request error:", error) } bb, err := ioutil.ReadAll(resp.Body) if err == nil { ioutil.WriteFile (out_filename,bb,0666) var unit_aa map[string]interface{} json.Unmarshal ([]byte(string(bb)), &unit_aa) fmt.Printf("len(unit_aa) = %d\n", len(unit_aa)) unit_bb := unit_aa["result"] fmt.Println(unit_bb) } } // --------------------------------------------------------------- func main() { fmt.Fprintf (os.Stderr,"*** 開始 ***\n") config := get_config_proc () access_token := get_token_proc (config) config["access_token"] = access_token fmt.Printf("%s\n",config["access_token"]) sentence := "特急はくたかで富山に向かいます。それから、金沢に行って、兼六園に行きます。" proper_noun_proc(config,sentence) fmt.Fprintf (os.Stderr,"*** 終了 ***\n") } // ---------------------------------------------------------------get_config.go get_token.go はこちら
COTOHA API で構文解析 (Golang)実行コマンド
go run proper_noun.go get_config.go get_token.go次のJSONファイルが作成されます。
out01.json{ "result" : [ { "begin_pos" : 7, "end_pos" : 9, "form" : "富山", "std_form" : "富山", "class" : "LOC", "extended_class" : "", "source" : "basic" }, { "begin_pos" : 21, "end_pos" : 23, "form" : "金沢", "std_form" : "金沢", "class" : "LOC", "extended_class" : "", "source" : "basic" }, { "begin_pos" : 28, "end_pos" : 31, "form" : "兼六園", "std_form" : "兼六園", "class" : "LOC", "extended_class" : "", "source" : "basic" } ], "status" : 0, "message" : "" }
- 投稿日:2020-02-25T13:39:11+09:00
COTOHA API で構文解析 (Golang)
COTOHA API Portal の使用例です。
フォルダー構造
$ tree -a . ├── .env ├── get_config.go ├── get_token.go └── parsing.go// --------------------------------------------------------------- // // parsing.go // // Feb/25/2020 // --------------------------------------------------------------- package main import ( "fmt" "os" "encoding/json" "net/http" "strings" "io/ioutil" ) // --------------------------------------------------------------- func parse_proc(config map[string]interface{},sentence string) { out_filename := "out01.json" fmt.Printf("%s\n",sentence) data := make (map[string]interface{}) data["sentence"] = sentence data["type"] = "default" str_json, _ := json.Marshal(data) fmt.Printf("%s\n",str_json) url_base := config["url_base"] fmt.Printf("%s\n",url_base) url_target := url_base.(string) + "v1/parse" fmt.Printf("%s\n",url_target) req, _ := http.NewRequest("POST", url_target, strings.NewReader(string(str_json))) req.Header.Set("Content-Type","application/json") req.Header.Set("Authorization", "Bearer " + config["access_token"].(string)) client := new(http.Client) resp, error := client.Do(req) if error != nil { fmt.Println("*** error *** client.Do ***") fmt.Println("Request error:", error) } bb, err := ioutil.ReadAll(resp.Body) if err == nil { ioutil.WriteFile (out_filename,bb,0666) var unit_aa map[string]interface{} json.Unmarshal ([]byte(string(bb)), &unit_aa) fmt.Printf("len(unit_aa) = %d\n", len(unit_aa)) unit_bb := unit_aa["result"] fmt.Println(unit_bb) } } // --------------------------------------------------------------- func main() { fmt.Fprintf (os.Stderr,"*** 開始 ***\n") config := get_config_proc () access_token := get_token_proc (config) config["access_token"] = access_token fmt.Printf("%s\n",config["access_token"]) sentence := "特急はくたか" parse_proc(config,sentence) fmt.Fprintf (os.Stderr,"*** 終了 ***\n") } // ---------------------------------------------------------------get_config.go// --------------------------------------------------------------- // // get_config.go // // Feb/25/2020 // --------------------------------------------------------------- package main import ( "fmt" "os" "github.com/joho/godotenv" ) // --------------------------------------------------------------- func get_config_proc () map[string]interface{} { err := godotenv.Load() if err != nil { fmt.Println("Error loading .env file") } config := make (map[string]interface{}) config["grantType"] = "client_credentials" config["clientId"] = os.Getenv("CLIENT_ID") config["clientSecret"] = os.Getenv("CLIENT_SECRET") config["url_base"] = os.Getenv("DEVELOPER_API_BASE_URL") config["url_target"] = os.Getenv("ACCESS_TOKEN_PUBLISH_URL") return (config) } // ---------------------------------------------------------------get_token.go// --------------------------------------------------------------- // // get_token.go // // Feb/25/2020 // --------------------------------------------------------------- package main import ( "fmt" "strings" "io/ioutil" "net/http" "encoding/json" ) // --------------------------------------------------------------- func get_token_proc (config map[string]interface{}) string { access_token := "" str_json, _ := json.Marshal(config) url_target := config["url_target"].(string) response, error := http.Post(url_target,"application/json", strings.NewReader(string(str_json))) if error != nil { fmt.Println("Request error:", error) } bb, err := ioutil.ReadAll(response.Body) if err == nil { // fmt.Println(string(bb)) var unit_aa map[string]interface{} json.Unmarshal ([]byte(string(bb)), &unit_aa) access_token = unit_aa["access_token"].(string) } return (access_token) } // ---------------------------------------------------------------実行コマンド
go run parsing.go get_config.go get_token.go次のJSONファイルが作成されます。
out01.json{ "result" : [ { "chunk_info" : { "id" : 0, "head" : 1, "dep" : "D", "chunk_head" : 1, "chunk_func" : 2, "links" : [ ] }, "tokens" : [ { "id" : 0, "form" : "特急", "kana" : "トッキュウ", "lemma" : "特急", "pos" : "名詞", "features" : [ "形容" ], "attributes" : { } }, { "id" : 1, "form" : "は", "kana" : "ハ", "lemma" : "はく", "pos" : "動詞語幹", "features" : [ "K" ], "dependency_labels" : [ { "token_id" : 0, "label" : "nmod" }, { "token_id" : 2, "label" : "aux" } ], "attributes" : { } }, { "id" : 2, "form" : "く", "kana" : "ク", "lemma" : "く", "pos" : "動詞接尾辞", "features" : [ "連体" ], "attributes" : { } } ] }, { "chunk_info" : { "id" : 1, "head" : -1, "dep" : "O", "chunk_head" : 0, "chunk_func" : 0, "links" : [ { "link" : 0, "label" : "adjectivals" } ] }, "tokens" : [ { "id" : 3, "form" : "たか", "kana" : "タカ", "lemma" : "鷹", "pos" : "名詞", "features" : [ ], "dependency_labels" : [ { "token_id" : 1, "label" : "amod" } ], "attributes" : { } } ] } ], "status" : 0, "message" : "" }
- 投稿日:2020-02-25T13:01:41+09:00
ローカル環境で、Go、C#、Java、Pythonそれぞれで簡易サーバを実装して、クラウドサービスのWebhookの通知を受ける
1. やること
ローカル環境のWindows, Mac, Linuxなどの上で、
Go、C#、Java、Pythonのいずれかの言語で簡易サーバを作ります。
作ったローカル環境の簡易サーバで、
クラウドベースの各種チャットサービスやSNSなどのリアルタイムの通知をWebhookで受けます。2. 必要なもの
インターネットにつながるWindows、Mac、Linuxなどが必要です。
中から外に繋がればいいので、Webhook用のpublicなURL(外からアクセスできるhttpsサーバなど)は必要ありません。
インターネット上のWebサイトが見れるような環境であればOKです。
ngrok
(後述)とプログラミング言語を利用するので、それらをサポートしている環境である必要はあります。3. ngrokの準備と起動
かなり、ざっくりな説明をすると、
ngrok
は、インターネットに抜けられるローカルの環境(インターネット上のWebサイトが見れるような環境)で、
public URLへのリクエストを受けられるトンネリングサービスです。
TCPのトンネリングもできますが、今回はhttpのトンネリングに関してのみ触れます。
あえて日本語で読むと、「エングロック」になります。こちらが公式サイトです:
https://ngrok.com/無償版、有償版があります。
無償版では、1分間当たり40コネクション(40リクエストではありません)までですが、
ちょっとしたテストをする分には、充分だと思います。
有償版ではプランによって、できることが増えていきます。3.1. ngrokのダウンロード
公式サイトの[DOWNLOAD]ページからダウンロードできます。
ダウンロードしたら、適当なディレクトリに解凍しましょう。
ngrokのアカウントを作らずに動作させた場合は、アプリ起動ごとに8時間のみ動作します。
起動しなおせば、再度8時間利用できますが、無償アカウントを作っておいた方が制限も緩和されるのでよいと思います。
公式サイトの[SING UP]からアカウントの作成ができます。3.2. ngrokのアカウントに接続する
ngrokのアカウントを作った場合は、今後、ngrokを起動する場合にアカウントと関連付くようにします。
<YOUR_AUTH_TOKEN>
は、SING UP後やLOGIN後に取得できます。
LOGIN後のページトップに『Setup & Installation』が表示されて、
『(3) Connect your account』にまさに実行すべきコマンドがそのまま書かれています。Windows> .\ngrok authtoken <YOUR_AUTH_TOKEN>Mac/Linux$ ./ngrok authtoken <YOUR_AUTH_TOKEN>このコマンドを実行すると、各OSのユーザのホームディレクトリ配下の以下の場所に(Ver 2.xの場合)、
{userhome}/.ngrok2/ngrok.yml
というファイルが出来上がって、そこに上記のアカウントのトークン情報などが保存されます。
ngrok.yml
には、その他さまざま設定を書くこともできますが、説明は省略します。
--config
オプションで、設定ファイルのパスも指定できるので、例えば、ngrokをdockerコンテナで実行する場合に、
ホスト側のディレクトリに設定ファイル置いて、マウントさせて設定参照などもできます。3.3. ngrokを起動する
Webhook用のpublicなURLは、ngrok側が準備してくれます。
Webhook用にTLSを利用しない、http
を使うのは、通常はおろかな行為なので、https
だけ準備されるようにオプションを指定(--bind-tls=true
)します。
以下コマンドの、8080
は、ローカル環境のサーバのポート番号です。使っていない適切なポート番号を指定します。
この時点では、まだローカル側のサーバは作っていませんが、空いている適切なポートを指定しましょう。Windows> .\ngrok http 8080 --bind-tls=trueMac/Linux$ ./ngrok http 8080 --bind-tls=trueうまく行くと、以下のような出力が得られるはずです。
{random-id}
の部分は、実行するたびに変わります(有償版では、この部分を指定することもできます)。
Region
も指定できますが、今回はデフォルトでよいと思います。出力例ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Account Your Name (Plan: Free) Version 2.x.yz Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding https://{random-id}.ngrok.io -> http://localhost:8080 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.003.4. ここまでの手順でngrokがやってくれること
Forwarding https://{random-id}.ngrok.io -> http://localhost:8080
という出力が示すように、
ngrokサービスが、publicなURLであるhttps://{random-id}.ngrok.io
を準備してくれています。
このURLへの要求は、ローカル環境(ngrokコマンドを実行したマシン)のhttp://localhost:8080
にフォワードされます。つまり、ローカルマシン上では、
http://localhost:8080
(http://127.0.0.1:8080
)へのリクエストを受けられるようにアプリを実装すればよいです。
有償版ではプランによって、ローカルのhttpsサーバ(ローカル側がTLSあり)にフォワードすることもできますが、今回は、ローカル側はTLSなしのhttpサーバとします。ざっくり図解すると、今回のオプション指定では、以下のような接続になっています。
矢印の方向は接続開始時の要求の方向を示しています。リクエストとレスポンスを処理するので、データのやりとりの観点では双方向です。ローカルにあるngrokのアプリが、クラウドにあるngrokサービスとセキュアに常時接続しています。
ngrokのpublicなURLにリクエストがあると、この常時接続を通じて、ローカル側のngrokのアプリにリクエストが通知されて、
ローカル側のngrokのアプリが、ローカル側のサーバにリクエストを出します。
ngrokが準備したPublicなURLがhttpsの場合は、ここのTLSのサーバ証明書はngrok側のものになります。
この証明書チェーンは、標準的な環境では信頼済みのものであるので、これが非常に便利な場合もあります。ngrokサービス提供元を信頼するという前提の下では、通信経路は暗号化されてTLSのレベルでデータが守られるということになると思います。
3.5. Web Interfaceも便利
Web Interface http://127.0.0.1:4040
と出力されていると思いますが、テストではこれは便利に使えます。
(オプションでWeb Interfaceを無効にすることもできます。)http://127.0.0.1:4040
にアクセスすることで、ngrokを経由した通信内容を閲覧したりできます。
例えば、httpのリクエストとレスポンスのヘッダやBodyの中身を調べたりすることができます。また、[Replay]機能が非常に便利で、ngrok経由で過去に行われたhttpなどのリクエストを、
再度ローカルのサーバに好きなタイミングで送信することができます。
リクエストを編集して送信することもできます。
テストでは結構便利で、Webhookの実際の通知を再度発生させることなく、以前の通知内容をちょっと変えて試すということもできます。
(本格的なテストを行うときは、ngrokで本機能を使うのではなく、より高度なテストの自動化をする場合が多いと思います。)3.6. APIもあります
Web Interfaceを有効にしているとAPIも使えます。
APIの詳細はこちらのページ(https://ngrok.com/docs#client-api)に記載されています。
ngrokサービスが割り当ててくれるpublicなURLは、無償版ではランダムになりますが、
APIで何を割り当ててもらったかを取得することもできます。Web Interfaceの
http://127.0.0.1:4040/api/tunnels
に、HTTP GETのリクエスト投げると、レスポンスで各種情報が得られます。
リクエスト時には、Accept
やContent-Type
を、application/json
にしておくと、JSON形式でレスポンス返ってきます。例えば、以下のような感じです。
metrics
も興味深いですが、今回は中身省略して記載しています。/api/tunnels{ "tunnels": [ { "name": "command_line", "uri": "/api/tunnels/command_line", "public_url": "https://{random-id}.ngrok.io", "proto": "https", "config": { "addr": "http://localhost:8080", "inspect": true }, "metrics": { } } ], "uri": "/api/tunnels" }今回はngrok起動コマンドで、明示的に
https
のみ指定したので、"tunnels"
配列には、1つのオブジェクトのみ含まれます。
そのオブジェクトの"public_url"
から、ngrok側が割り当てたpublicなURLが取得できます。
"config"
オブジェクトの"addr"
からは、フォワード先のローカル側のURLが取得できます。また、先ほどWeb Interfaceで説明したリクエストやレスポンスのヘッダやBodyの中身の時系列での情報取得や、
Replayを行うAPIもあります。4. ここまでで、もうWebhookの通知は受けられます
ngrokサービス
が準備してくれた、public URLをWebhookの通知先として、各種チャットサービスやSNSなどのAPIサービス側に登録すれば、実際に動作するはずです。
この時点ではローカル側にまだサーバーがないので、正常なレスポンスを返すことができませんが、
Web Interface ( http://127.0.0.1:4040 )から、Webhookで通知されたリクエストのヘッダやBodyの中身は確認できます。
Forwarding https://{random-id}.ngrok.io -> http://localhost:8080
のように出力されている場合は、
https://{random-id}.ngrok.io/webhook
などを、Webhookの通知先のURLとして登録します。
{random-id}の部分は、ngrokコマンドを実行した環境によって、また無償版では(有償版で指定していない場合も)、起動ごとに変わります。
後ろにくっつけたパス部分の、/webhook
は好きなパスに指定できます。
/
とかでも良いですが、この後のコードでは、https://{random-id}.ngrok.io/webhook
のように、パスは/webhook
を指定したものとして記載します。Webhookの通知先の登録方法は利用するクラウドサービスなどによって異なります。
開発者用のサイトから登録できるものや、APIで登録する場合などあります。
試してみたいサービスごとに登録方法は確認する必要があります。サービスによっては、登録作業を行った瞬間に、Webhookの通知先にリクエストを出して、
適切なレスポンスを返さないと、登録に失敗する仕組みのものもあります。
こちらの場合でも、登録時のリクエスト内容は、上記のWeb Interfaceで確認できます。また、ここで、Webブラウザなどで、同じマシンからでも、別のマシンからでも、
https://{random-id}.ngrok.io/webhook
にアクセスすると、
ngrokコマンドを実行しているコンソールや、Web Interfaceにも出力があるはずです。
HTTP GET以外でもいろいろ試してみたい場合は、Postman などを使って、とりあえず、動作を見ることはできると思います。5. 各種言語で簡易サーバを実装する
今のままでは、ngrokがローカルにフォワードする先のサーバがないので、簡易サーバを作っていきます。
httpのリクエストに対して、どんなレスポンスを返すべきかは、サービスごとにことなるので、
今回は、とりあえず、200 OK
で、Bodyは以下のようなJSONを固定で返すことにします。
サービスによっては、204 No Content
で、Bodyなしで返せばよいものもあります。とりあえず、レスポンスBodyに入れる内容(実際は利用するサービスによって適切なものを返す必要あり):
今回の例で固定で返すレスポンスBody{ "status" : "OK" }この後、例として記載するいずれかのコードを実行した上で、
ngrok
を実行して、
ngrokが準備したhttpsのURLを元にしたURL(https://{random-id}.ngrok.io/webhook
とか)を、
対象のクラウドサービス側にWebhook通知先として登録すると動作します。
例では、ngrokが準備したURLに、"/webhook"のパスが追加されている前提のコードになっています。
例では、サービスからの通知されたリクエストのBodyを表示して、固定のレスポンスを返しているだけですが、
利用するサービスに応じて、処理を少し追加すると、いろいろできると思います。今回は、さくっと例を記載したいだけなので、処理に成功したかどうかのチェック等は省略しています。
煩雑になりすぎないように、必要最小限に近いくらいのコードになるようにしていますが、実際のアプリでは各種チェックが必要になります。また、Webhookは、クラウドサービスの場合は、publicなURLで受ける場合が多いので、
偽装した通知が送られるような場合も想定しておく必要があると思いますが、その辺りの対策も今回の実装例には盛り込んではいません。関連してですが、(いろんな意見あると思いますが)、個人的には、偽装通知する人のヒントになるような、
404 Not Found
とか405 Method Not Allowed
とかも返すべきでないと思っていますが、その辺も今回の実装例には盛り込んでいません。
不正な通知に対しては、「204 No Content
で成功で返す」、
「レスポンス自体返さず相手はレスポンス待ち状態にする(TCPのコネクションは切断しない)」、
「レスポンスは返さず、TCPのコネクションを切断する」などの対処があると思います。
ただし、コネクションを切断しないパターンは、
サーバ側の残りの接続数やスレッドプール数に悪影響がでやすい(リソースを枯渇させる攻撃が考えられる)ので、
一般的には実装が難しいです(TCP/Socketレベルで実装考えないといけなくなると思います)。5.1. Goでの実装の例
標準の、
net/http
パッケージを利用した例です。
※ 説明用にコメントは多めに書いています。go1.13 + Windows 10 Pro 64bitで動作確認しています。
webhook_listener.gopackage main import ( "fmt" "io/ioutil" "net/http" ) // Webhookに通知が来た時に呼ばれるハンドラ。 func handleWebhook(w http.ResponseWriter, r *http.Request) { // Webhook通知元の仕様にもよるが、HTTPのPOSTかGETかで通知が来る前提にして、 // それ以外は、"204 No Content"を返す。 if r.Method != http.MethodPost && r.Method != http.MethodGet { w.WriteHeader(http.StatusNoContent) return } // Bodyの内容を読み込んで表示するだけ。 body, _ := ioutil.ReadAll(r.Body) fmt.Println("ReceivedData:", string(body)) //////////////////////////////////////////////////////////////// // 以下、レスポンスで何を返すべきかは通知元のサービス側の仕様にもよる。 // "204 No Content"を返せばいい場合は、以下にコメントアウトした1行だけでBody出力不要。 // w.WriteHeader(http.StatusNoContent) // レスポンスヘッダで、Content-Type: application/jsonにする。 w.Header().Set("Content-Type", "application/json") // レスポンスのBodyは決め打ち。 fmt.Fprint(w, "{\"status\" : \"OK\"}") } func main() { // ローカルサーバの"/webhook"にリクエストが来た時に呼ばれるハンドラを登録。 http.HandleFunc("/webhook", handleWebhook) // ローカルの8080番ポート(ngrok起動時のオプションで指定した番号)で待ち受け開始。 // ngrokが同じローカルマシンで動いているので、"127.0.0.1"だけで待ち受ければよい。 // (外部の環境からリクエストを受ける必要がない) http.ListenAndServe("127.0.0.1:8080", nil) }5.2. C#での実装の例
.NET Core 2.0以降や、.NET Frameworkなどで利用できる
System.Net.HttpListener
を利用した例です。
※ 説明用にコメントは多めに書いています。.NET Core 2.0 + Windows 10 Pro 64bitで動作確認しています。
WebhookListener.csusing System; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; namespace WebhookListener { class Program { static void Main(string[] args) { // System.Net.HttpListenerを利用してサーバを実装します。 using (var listener = new HttpListener()) { // ローカルの8080番ポート(ngrok起動時のオプションで指定した番号)で待ち受け開始。 // ngrokが同じローカルマシンで動いているので、"127.0.0.1"だけで待ち受ければよい。 // (外部の環境からリクエストを受ける必要がない) // また、"/webhook"のパスも入れておく。Prefix指定時は、"/"で終わるようにしておく必要がある。 listener.Prefixes.Add("http://127.0.0.1:8080/webhook/"); // HttpListenerの待ち受けを開始します。 listener.Start(); // スレッドプール上で待ち受けるようにする。 // 今回は1スレッドだが、例えばループで64回Task作成すれば、64スレッドで待ち受けるようになる。 Task.Run( async () => { // HttpListenerが待ち受け中はループする。 while(listener.IsListening) { //////////////////////////////////////////////////////////////// // このサンプルの実装では、whileブロック内で例外が発生すると後続の待ち受けも中断されます。 // 実際には、適切に例外を処理する必要があります。 // どの例外をcatchすべきかは、whileブロック内での処理内容にもよります。 // catchしすぎると、意図せずループが続く場合もあるので注意が必要です。 // 基本的には、例外が発生しても待ち受けを継続したいような場合の例外は、whileブロックの中、 // 待ち受けを継続しても意味がないような例外は、whileブロックの外側で受けるように実装します。 // 先ほど登録したアドレス、ポート、パスに合致するリクエストが来ると、処理用のContextが取得できる。 var context = await listener.GetContextAsync(); // リクエストとレスポンス処理用のインスタンス取得。 var request = context.Request; var response = context.Response; try { if (request.HttpMethod != "POST" && request.HttpMethod != "GET") { // Webhook通知元の仕様にもよるが、HTTPのPOSTかGETかで通知が来る前提にして、 // それ以外は、"204 No Content"を返す。 response.StatusCode = 204; } else { // Bodyの内容を読み込んで表示するだけ。 if (request.HasEntityBody) { using (var reader = new StreamReader(request.InputStream, request.ContentEncoding)) { Console.WriteLine("RequestData: {0}", reader.ReadToEnd()); } } //////////////////////////////////////////////////////////////// // 以下、レスポンスで何を返すべきかは通知元のサービス側の仕様にもよる。 // "204 No Content"を返せばいい場合は、以下にコメントアウトした1行だけでBody出力不要。 // response.StatusCode = 204; // レスポンスヘッダで、Content-Type: application/jsonにする。 response.ContentType = "application/json"; // レスポンスのBodyは決め打ちで書き込む。 using (var writer = new StreamWriter(response.OutputStream)) { writer.Write("{\"status\" : \"OK\"}"); } } } finally { // ResponseはClose()を呼ぶ必要があります。 response.Close(); } } }); // 何かキーを押したら終了させる。 Console.WriteLine("終了するには何かキーを押してください。"); Console.ReadKey(false); } } } }5.3. Javaでの実装の例
Java 1.6以降利用可能な
com.sun.net.httpserver.HttpServer
を利用した例です。
パッケージは、com.sun.net
配下になっていますが、OpenJDKでも利用できます。
※ 説明用にコメントは多めに書いています。OpenJDK11(AdoptOpenJDK) + Windows 10 Pro 64bitで動作確認しています。
WebhookListener.javapackage thrzn41.samples; import java.io.IOException; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; public class WebhookListener { /** * リクエストが来た時に処理するハンドラクラス。 */ private static class WebhookHandler implements HttpHandler { /** * HttpServer.createContext()で登録したパスにHTTPリクエストがあると、このメソッドが呼ばれる。 */ @Override public void handle(HttpExchange exchange) throws IOException { // Webhook通知元の仕様にもよるが、HTTPのPOSTかGETかで通知が来る前提にして、 // それ以外は、"204 No Content"を返す。 String method = exchange.getRequestMethod(); if( !method.equals("POST") && !method.equals("GET") ) { exchange.sendResponseHeaders(204, -1); return; } // Bodyの内容を読み込んで表示するだけ。 try(var input = exchange.getRequestBody()) { // 本来は、ちゃんとリクエストヘッダのエンコーディングを見た方がいいですが、 // 今回は、"utf-8"である前提で変換しています。 String body = new String(input.readAllBytes(), "utf-8"); System.out.printf("RequestData: %s%n", body); } //////////////////////////////////////////////////////////////// // 以下、レスポンスで何を返すべきかは通知元のサービス側の仕様にもよる。 // "204 No Content"を返せばいい場合は、以下にコメントアウトした1行だけでBody出力不要。 // exchange.sendResponseHeaders(204, -1); // レスポンスのBodyは決め打ちで書き込む。 var responseBytes = "{\"status\" : \"OK\"}".getBytes("utf-8"); try(var output = exchange.getResponseBody()) { // レスポンスヘッダで、Content-Type: application/jsonにする。 exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(200, responseBytes.length); output.write(responseBytes); } } } public static void main(String[] args) { try { // ローカルの8080番ポート(ngrok起動時のオプションで指定した番号)で待ち受けます。 // ngrokが同じローカルマシンで動いているので、"127.0.0.1"だけで待ち受ければよい。 // (外部の環境からリクエストを受ける必要がない) var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 8080), -1); // ローカルサーバの"/webhook"にリクエストが来た時に呼ばれるハンドラを登録。 server.createContext("/webhook", new WebhookHandler()); // 今回はデフォルトのExecutorを利用してHTTPリクエストを処理するように指定(null)。 server.setExecutor(null); // HttpServerの待ち受けを開始します。 server.start(); System.out.println("終了するには何かキーを押してください。"); System.in.read(); // HttpServerの待ち受けを停止します。 server.stop(0); } catch(IOException ioex) { ioex.printStackTrace(); } } }5.4. Pythonでの実装の例
標準の
http.server
モジュールを使っても実装できますが、今回はflask
を使っちゃいます。
※ 説明用にコメントは多めに書いています。flask入ってなかったら、以下でインストール(
pip
使う場合の例)。pip使ってflaskインストールする例$ pip install flaskPython 3.6 + flask 1.1 + Windows 10 Pro 64bitで動作確認しています。
webhook_listener.pyfrom flask import Flask, request # 起動時の自分の名前からFlaskのインスタンス作成。 app = Flask(__name__) # Webhookに通知が来た時に呼ばれるハンドラ。 # 以下の指定では、"/webhook"に対するHTTP POST, GET, PUT, DELETEリクエストの場合に呼ばれます。 # パスが違うと"404 Not Found", リストにないメソッドの場合は、"405 Method Not Allowed"が返ります。 @app.route("/webhook", methods=["POST", "GET", "PUT", "DELETE"]) def handle_webhook(): # Webhook通知元の仕様にもよるが、HTTPのPOSTかGETかで通知が来る前提にして、 # それ以外は、"204 No Content"を返す。 if request.method != 'POST' and request.method != 'GET': # レスポンスのBodyとStatus Codeをタプルで返します(make_response()でタプル指定するのと同じ)。 return ('', 204) # Bodyの内容を読み込んで表示するだけ。 print(request.get_data(as_text=True)) # ============================================================== # 以下、レスポンスで何を返すべきかは通知元のサービス側の仕様にもよる。 # "204 No Content"を返せばいい場合は、以下にコメントアウトした1行だけでBody出力不要。 # return ('', 204) # レスポンスのBodyは決め打ち。 # ディクショナリで返せば、jsonに変換(jsonify()を呼ぶのと同じ)して、Content-Typeヘッダも"application/json"にしてくれます。 # より厳密には、make_response()が呼ばれて、その中でjsonify()が呼ばれて、その中で、Content-Typeが"application/json"に設定される。 return { 'status' : 'OK' } if __name__ == '__main__': # ローカルの8080番ポート(ngrok起動時のオプションで指定した番号)で待ち受け開始。 # ngrokが同じローカルマシンで動いているので、"127.0.0.1"だけで待ち受ければよい。 # (外部の環境からリクエストを受ける必要がない) app.run('127.0.0.1', 8080)
- 投稿日:2020-02-25T06:42:38+09:00
GOCVの環境構築とサンプル一部紹介
はじめに
goで画像処理してみたいと思ってネットサーフィンしていたらGOCVなるものをみつけたので
環境構築とサンプルコードの紹介をします。
環境
- windows10
- go version go1.12.6 windows/amd64
パッケージダウンロード
PS C:\Users\Hoge> go get -u -d gocv.io/x/gocv
MinGW-64 のインストール
MInGW-W64って?
64bit windowsOSでCやC++をコンパイルできるようにするツールらしい(曖昧です)以下ページを参考にインストールしました
https://www.javadrive.jp/cstart/install/index6.html
cmakeのインストール
[Download] から cmake-3.17.0-rc1-win64-x64.msi を選択してダウンロード & インストール
環境変数の設定
自分がどのディレクトリにいても、hoge.exeを実行できるようにwindows側に.exeファイルが存在するPath(環境)を登録しておきましょう。
環境変数の設定は次のような順番で行えます
1. [スタート] - [設定] - [設定の検索]で環境変数を編集で検索
Path変数を選択して、編集をクリック
Pathに C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin と C:\Program Files\CMake\bin を追加してOKを選択する
※ インストールしたバージョンとインストールしたフォルダ名で上記の名前と違うことがあります。opencvの環境構築
%GOPATH%\gocv.io\x\gocv に win_build_opencv.cmd が存在するのでダブルクリックで実行しましょう
opencvのビルドが始まります
ビルド完了後、C:\opencv\build フォルダが作られているはずなので、
C:\opencv\build\install\x64\mingw\bin を再度環境変数のPathに登録しましょう。
sample code
環境構築が完了したら、readme.mdの説明を見ながらいくつか動かしてみるといいでしょう。
readmeやソースコード内にusageがまとめられてるのもgoodですね。
C:\Users\hoge\go\src\gocv.io\x\gocv\cmd\readme.md
何個か試してみて面白かったものを紹介します。
hello sample
カメラデバイスにアクセスして、映像を垂れ流しするサンプル
サンプルのためカメラデバイスは0番目で決め打ちされているし、key入力受付してexitもないのには注意
// gocv.io\x\gocv\cmd\hello\main.go // +build example package main import ( "gocv.io/x/gocv" ) func main() { webcam, _ := gocv.OpenVideoCapture(0) window := gocv.NewWindow("Hello") img := gocv.NewMat() for { webcam.Read(&img) window.IMShow(img) window.WaitKey(1) } }windowにカメラ映像が表示されていればOKです
motion-detect
IntelのOpenVinoを使って物体認識をしているサンプルだと思います。
ある程度分類はできているようなので、返り値の値を上手く使って表示を工夫できれば面白そうですね。
どのカメラに接続するか引数で渡す必要があります。
go run gocv.io\x\gocv\cmd\motion-detect\main.go 0
facedetect
カスケード分類器を使った顔認識サンプルですが、あまり精度が良くない印象です。
第一引数にカメラ、第二引数にカスケード分類器のxmlが必要で、xmlはdataフォルダにあらかじめ準備されているので、それを使いましょう。go run gocv.io\x\gocv\cmd\facedetect\main.go 0 gocv.io\x\gocv\data\haarcascade_frontalface_default.xmlfaceblur
facedetectの発展形で顔認識した空間にblur処理(ぼかし)を行うサンプルです。
gocv.io\x\gocv\cmd\faceblur\main.go 0 gocv.io\x\gocv\data\haarcascade_frontalface_default.xml
- blur結果
アニメキャラクターの顔も認識してますね。face_detect_url
facedetectの発展形で引数に与えたurlから取得した画像を対象に顔検出して、画像として保存します。
今回は画像処理で有名なレナさんの画像をwikipediaから取得して顔検出してみます。
go run gocv.io\x\gocv\cmd\faceblur\main.go https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png gocv.io\x\gocv\data\haarcascade_frontalface_default.xml output.png
- 結果
まとめ
goで何か作りたいなと思ってた時に、CVのライブラリを見つけたので導入方法をまとめてみました。
暇があればgoでクローラーとかも作ってみたいです。
- 投稿日:2020-02-25T02:02:58+09:00
GolangのAPIサーバをAWS Lambdaへ移植してみた
背景
現在Golang + Nginxで動いているAPIをLambda関数へ移行したい。
API仕様
- おみくじAPI
/fortune
へのアクセスで大吉・中吉・小吉のどれかをjsonとして返してくれる/list
でおみくじで得ることが出来る結果一覧をJSONで取得できる/version
でプレーンテキストとしてAPIのバージョンを取得できる- 無効なパスへのアクセスは404を返す
環境
OS: Ubuntu 18.04
バージョン
$ go version go version go1.10.3 gccgo (Ubuntu 8.3.0-6ubuntu1~18.04.1) 8.3.0 linux/amd64実際に移植してみる!
ざっくりやること
- Lambda関数作成
- ALBとターゲットグループを作成
- ビルドしてLambdaへデプロイ
移植前のソース
下記のコードをLambdaで動くように修正します。
移植前ソース
package main import ( "encoding/json" "fmt" "log" "math/rand" "net/http" ) var fortune = []string{"大吉", "中吉", "小吉"} func fortuneHandler(w http.ResponseWriter, r *http.Request) { res, err := json.Marshal(fortune[rand.Intn(3)]) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, string(res)) } func versionHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plane; charset=utf-8") fmt.Fprint(w, string("version 1.0.0")) } func listHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, fortune) } func notFoundHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(404) fmt.Fprint(w, "404 not found") } func main() { http.HandleFunc("/fortune", fortuneHandler) http.HandleFunc("/version", versionHandler) http.HandleFunc("/list", listHandler) http.HandleFunc("/", notFoundHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }Lambda環境作成
Lambda関数の作成
ランタイムが
Go 1.x
になっていることを確認し、関数を作成します。
ALBとターゲットグループを作成
Application Load Balancer
の作成
を押下
ロードバランサーの設定は各々の環境に合わせて設定してください
ターゲットグループを新規作成し、
ターゲットの種類
がLambda
になっていることを確認し、ターゲットの登録ボタンを押下
リストからターゲットにしたいLambdaを選択し、確認を押下
確認して問題なければ作成ボタンを押下
動作確認
作成したALBのDNS名をコピーして、実際にアクセスしてみて確認
Hello from Lambda!
と表示されればOK$ curl <ALBのDNS名> -D - HTTP/1.1 200 OK Server: awselb/2.0 Date: Mon, 24 Feb 2020 12:47:12 GMT Content-Type: application/octet-stream Content-Length: 18 Connection: keep-alive Hello from Lambda!Lambda関数として動くようにする
- やること
- サーバとしてではなく関数として動作するようにする
*http.Request
とhttp.ResponseWriter
をALBTargetGroupRequest
とevents.ALBTargetGroupResponse
へ置き換える置き換え後
置き換え後ソース
package main import ( "encoding/json" "fmt" "math/rand" "context" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) var fortune = []string{"大吉", "中吉", "小吉"} func fortuneHandler() (events.ALBTargetGroupResponse, error) { res, err := json.Marshal(fortune[rand.Intn(3)]) if err != nil { return events.ALBTargetGroupResponse { StatusCode: 500, Body: fmt.Sprintf("%s", err), }, err } return events.ALBTargetGroupResponse { Headers: map[string]string{ "content-type": "application/json", }, Body: fmt.Sprintf("%s", string(res)), }, nil } func versionHandler() (events.ALBTargetGroupResponse, error) { return events.ALBTargetGroupResponse { Headers: map[string]string{ "content-type": "text/plane; charset=utf-8", }, Body: fmt.Sprintf("%s", string("version 1.0.0")), }, nil } func listHandler() (events.ALBTargetGroupResponse, error) { return events.ALBTargetGroupResponse { Headers: map[string]string{ "content-type": "application/json", }, Body: fmt.Sprintf("%s", fortune), }, nil } func notFoundHandler() (events.ALBTargetGroupResponse, error) { return events.ALBTargetGroupResponse { StatusCode: 404, Headers: map[string]string{ "content-type": "text/plain; charset=utf-8", }, Body: fmt.Sprintf("404 not found\n"), }, nil } func main() { lambda.Start(handleRequest) } func handleRequest(ctx context.Context, request events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { switch request.Path { case "/fortune": return fortuneHandler() case "/version": return versionHandler() case "/list": return listHandler() default: return notFoundHandler() } }
修正差分
6d5 < "log" 8c7,9 < "net/http" --- > "context" > "github.com/aws/aws-lambda-go/events" > "github.com/aws/aws-lambda-go/lambda" 13c14 < func fortuneHandler(w http.ResponseWriter, r *http.Request) { --- > func fortuneHandler() (events.ALBTargetGroupResponse, error) { 17,18c18,21 < http.Error(w, err.Error(), http.StatusInternalServerError) < return --- > return events.ALBTargetGroupResponse { > StatusCode: 500, > Body: fmt.Sprintf("%s", err), > }, err 21,22c24,29 < w.Header().Set("Content-Type", "application/json") < fmt.Fprint(w, string(res)) --- > return events.ALBTargetGroupResponse { > Headers: map[string]string{ > "content-type": "application/json", > }, > Body: fmt.Sprintf("%s", string(res)), > }, nil 25,27c32,38 < func versionHandler(w http.ResponseWriter, r *http.Request) { < w.Header().Set("Content-Type", "text/plane; charset=utf-8") < fmt.Fprint(w, string("version 1.0.0")) --- > func versionHandler() (events.ALBTargetGroupResponse, error) { > return events.ALBTargetGroupResponse { > Headers: map[string]string{ > "content-type": "text/plane; charset=utf-8", > }, > Body: fmt.Sprintf("%s", string("version 1.0.0")), > }, nil 30,32c41,47 < func listHandler(w http.ResponseWriter, r *http.Request) { < w.Header().Set("Content-Type", "application/json") < fmt.Fprint(w, fortune) --- > func listHandler() (events.ALBTargetGroupResponse, error) { > return events.ALBTargetGroupResponse { > Headers: map[string]string{ > "content-type": "application/json", > }, > Body: fmt.Sprintf("%s", fortune), > }, nil 35,38c50,57 < func notFoundHandler(w http.ResponseWriter, r *http.Request) { < w.Header().Set("Content-Type", "text/plain; charset=utf-8") < w.WriteHeader(404) < fmt.Fprint(w, "404 not found") --- > func notFoundHandler() (events.ALBTargetGroupResponse, error) { > return events.ALBTargetGroupResponse { > StatusCode: 404, > Headers: map[string]string{ > "content-type": "text/plain; charset=utf-8", > }, > Body: fmt.Sprintf("404 not found\n"), > }, nil 42,46c61,64 < http.HandleFunc("/fortune", fortuneHandler) < http.HandleFunc("/version", versionHandler) < http.HandleFunc("/list", listHandler) < http.HandleFunc("/", notFoundHandler) < log.Fatal(http.ListenAndServe(":8080", nil)) --- > lambda.Start(handleRequest) > } > > func handleRequest(ctx context.Context, request events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 47a66,71 > switch request.Path { > case "/fortune": return fortuneHandler() > case "/version": return versionHandler() > case "/list": return listHandler() > default: return notFoundHandler() > }差分解説
func main() { lambda.Start(handleRequest) } func handleRequest(ctx context.Context, request events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { switch request.Path { case "/fortune": return fortuneHandler() case "/version": return versionHandler() case "/list": return listHandler() default: return notFoundHandler() } }
lambda.Start
lambda.Start(HandleRequest) を追加すると、Lambda 関数が実行されます。
ALBTargetGroupRequest
- ここにリクエストのパスなどが入ってきます
- golangの
*http.Request
相当
func listHandler() (events.ALBTargetGroupResponse, error) { return events.ALBTargetGroupResponse { Headers: map[string]string{ "content-type": "application/json", }, Body: fmt.Sprintf("%s", fortune), }, nil }
- ALBTargetGroupResponse
- この構造体をLambdaからreturnする事で任意のレスポンスを返すことが出来ます
- 基本的に、
http.ResponseWriter
をALBTargetGroupResponse
へ置き換えるだけで任意のレスポンスを返すように出来ますビルド&動作確認
ビルド
ここでのポイントはクロスコンパイルするところです
$ GOOS=linux GOARCH=amd64 go build -o fortuneここで作ったバイナリをLambdaへアップロードして、動作確認をします!
動作確認
ここで大吉・中吉・小吉のどれかが出力されればOKです
$ curl <ALBのDNS名>/fortune -D - HTTP/1.1 000 Server: awselb/2.0 Date: Mon, 24 Feb 2020 16:18:30 GMT Content-Type: application/json Content-Length: 8 Connection: keep-alive "小吉"記事外で関数に移植する際に困ったところ
条件によって特定のヘッダーを付与してALBTargetGroupResponseを返したい
ALBTargetGroupResponseを一度変数に入れて変数値を操作したものをreturnすればいい
下記コードだとif err {
の部分response := events.ALBTargetGroupResponse { StatusCode: http.StatusInternalServerError, Body: fmt.Sprintf("%d", http.StatusInternalServerError), Headers: map[string]string{}, } if err { response.Headers["X-Error-Message"] = fmt.Sprintf("error: %v", err) } response.StatusCode = statusCode response.Headers["Content-Type"] = "text/plane; charset=utf-8" response.Body = http.StatusInternalServerError return responseおわりに
置き換えはLambdaのお作法に従うだけで意外と簡単に移植できるみたいです。
参考資料
AWS Lambda で Go が使えるようになったので試してみた
Go の AWS Lambda 関数ハンドラー
README_ALBTargetGroupEvents.md
type ALBTargetGroupResponse
- 投稿日:2020-02-25T00:05:47+09:00
hash/maphashコードリーディング
お前誰よ
- 渋川よしき
- フューチャー株式会社で仕事してます
- いろいろ本を書いたりしています
- みなさん買ってくださっていますよね?
- 今日は、家族がいろいろ体調を崩していて時間が取れなかったのでQiitaスライドで
今回の内容
フューチャー社内で行ったコードリーディングの勉強会の再演です。現在、標準ライブラリをみんなで読んで解説しています。
わたしは1.14で追加されたhash/maphashを説明しました。
1.14の変更
- Go 1.14のリリースノート: https://tip.golang.org/doc/go1.14
- hash/maphash https://tip.golang.org/pkg/hash/maphash/
ここによると、1.14ではぼちぼち新しいメソッドやRISC-V実験サポート開始や、lock周りやdeferの高速化、テストが後処理ができるようになるなどがあるが、完全な新機能という点では、hash/maphashだけのようなので、ここを見てみることにします。
Readmeの説明
- バイトシーケンスに対して、ランダムな文字列を生成する。ハッシュテーブルのようなデータ構造を自分で作る人向けに、任意の文字列やバイト列を、分散した数値に変換する。
- ただし、sha256やsha512のような暗号目的には使えないとされています。64ビットでビット数が少ないので。
フォルダ構造
実体は1ファイル。
maphash.go maphash_test.go smhasher_test.go
テストを見る(maphash_test.go)
インタフェースのチェックはもう基本テクですね
// Make sure a Hash implements the hash.Hash and hash.Hash64 interfaces. | var _ hash.Hash = &Hash{} var _ hash.Hash64 = &Hash{}
仕様を確認
1文字ずつ入れても、3文字入れても結果は同じ
func TestHashGrouping(t *testing.T) { b := []byte("foo") h1 := new(Hash) h2 := new(Hash) h2.SetSeed(h1.Seed()) h1.Write(b) for _, x := range b { err := h2.WriteByte(x) if err != nil { t.Fatalf("WriteByte: %v", err) } } if h1.Sum64() != h2.Sum64() { t.Errorf("hash of \"foo\" and \"f\",\"o\",\"o\" not identical") } }
fooを[]byteで入れても、文字列で入れても結果は同じ。
func TestHashBytesVsString(t *testing.T) { s := "foo" b := []byte(s) h1 := new(Hash) h2 := new(Hash) h2.SetSeed(h1.Seed()) n1, err1 := h1.WriteString(s) if n1 != len(s) || err1 != nil { t.Fatalf("WriteString(s) = %d, %v, want %d, nil", n1, err1, len(s)) } n2, err2 := h2.Write(b) if n2 != len(b) || err2 != nil { t.Fatalf("Write(b) = %d, %v, want %d, nil", n2, err2, len(b)) } if h1.Sum64() != h2.Sum64() { t.Errorf("hash of string and bytes not identical") } }
テストを見る(smhasher_test.go)
ハッシュ関数に対する拷問テストのツールがあり、それをGoに移植しているとのこと。元はGoogle製で、非暗号的ハッシュ関数の性能(分散や、速度)を計測するらしい。
https://github.com/aappleby/smhasher
// Smhasher is a torture test for hash functions. // https://code.google.com/p/smhasher/ // This code is a port of some of the Smhasher tests to Go.内容的には、ハッシュの衝突を記録する構造体があって、循環する文字列、密度が薄い(Sparse)文字列、1ビットだけ変更した文字列で半数のビットが書き換わるか・・・といったテストがある模様。
テストコードで見つけた配慮。
if runtime.GOARCH == "wasm" { t.Skip("Too slow on wasm") } if testing.Short() { t.Skip("Skipping in short mode") }
ここから実装の方を見ていきます
Hash構造体
type Hash struct { _ [0]func() // not comparable seed Seed // initial seed used for this hash state Seed // current hash of all flushed bytes buf [64]byte // unflushed byte buffer n int // number of unflushed bytes }
比較不能
本当かどうか試してみた。 https://play.golang.org/p/Obem8g4xdXm
確かに比較不能というコンパイルエラー。
./prog.go:16:23: invalid operation: a == b (struct containing [0]func() cannot be compared)
- 固定長配列なら、中の要素が比較可能ならOK
- スライスはダメ(nilとの比較のみ)
- 単体の要素でも関数などは比較不能(nilとの比較のみ)
- 単体の変数ならnilとの比較のみ可能、という具体的なエラーメッセージになるが、構造体にラップされており、その内部の要素に比較不能なものがあると、理由なく「比較不能」というメッセージになる
ハッシュの計算
Write()、WriteByte()、WriteString()があるが、どれもやっていることは変わらないので、シンプルなWriteByteで見てみる。
バッファ(64バイト)分データが溜まったらflush()メソッドを呼ぶ。その後はバッファにデータを積む。
hash.Hashインタフェースを満たすためのシグニチャになっているだけで、errorを返す宣言になっているが、エラーは絶対に発生しない。
// WriteByte adds b to the sequence of bytes hashed by h. // It never fails; the error result is for implementing io.ByteWriter. func (h *Hash) WriteByte(b byte) error { if h.n == len(h.buf) { h.flush() } h.buf[h.n] = b h.n++ return nil }
flush()は、rthashという関数を呼んでいる。溜まったバッファの値と、現在のstateの情報をわたし、現在のstateを更新する。現在のstate情報も参照するため、64バイトごとに同じデータを送っても、結果が循環することはない。
// precondition: buffer is full. func (h *Hash) flush() { if h.n != len(h.buf) { panic("maphash: flush of partially full buffer") } h.initSeed() h.state.s = rthash(h.buf[:], h.state.s) h.n = 0 }
rthash関数はこんな実装。
unsafe.Sizeof(uintptr(0)) == 8
で64ビットかどうかを判定している模様。32ビットだと、上位ビットと下位ビットを計算してから合成して64ビットにしている。func rthash(b []byte, seed uint64) uint64 { if len(b) == 0 { return seed } // The runtime hasher only works on uintptr. For 64-bit // architectures, we use the hasher directly. Otherwise, // we use two parallel hashers on the lower and upper 32 bits. if unsafe.Sizeof(uintptr(0)) == 8 { return uint64(runtime_memhash(unsafe.Pointer(&b[0]), uintptr(seed), uintptr(len(b)))) } lo := runtime_memhash(unsafe.Pointer(&b[0]), uintptr(seed), uintptr(len(b))) hi := runtime_memhash(unsafe.Pointer(&b[0]), uintptr(seed>>32), uintptr(len(b))) return uint64(hi)<<32 | uint64(lo) }
runtime_memhashは次のようになっています。実体のない関数定義にlinknameディレクティブ。これはunsafeがついているパッケージのみで利用でき、外部パッケージのプライベートな関数を呼び出すためのスタブ関数を生成してくれるとのこと。これにより、プライベート関数が呼び出せる。
https://sitano.github.io/2016/04/28/golang-private/
//go:linkname runtime_memhash runtime.memhash //go:noescape func runtime_memhash(p unsafe.Pointer, seed, s uintptr) uintptr
memhashの実体はアセンブリ言語で実装されている。各CPUごとの実装がある(memhashFallbackというGo実装もある)。
// func memhash(p unsafe.Pointer, h, s uintptr) uintptr // hash function using AES hardware instructions TEXT runtime·memhash(SB),NOSPLIT,$0-32 CMPB runtime·useAeshash(SB), $0 JEQ noaes MOVQ p+0(FP), AX // ptr to data MOVQ s+16(FP), CX // size LEAQ ret+24(FP), DX JMP aeshashbody<>(SB) noaes: JMP runtime·memhashFallback(SB)最終的にはAES-NIという、TLS暗号の中で使われる最近のCPUで使われる専用命令でハッシュ計算をしていました。
まとめ
やっていることはシンプルで、mapで使われているハッシュ関数に公開APIがついた程度。完全な新機能を見ていたはずだが・・・
テストとか、中のテクニックはなかなか興味深いものもあったかも
明日からは役に立たない知識を中心にまとめました