20200225のGoに関する記事は8件です。

Golang context.Value() でKey周りを調べた にゃ

context.WithValue() の Key はどうなのか調べた奴
メンバーに変数がないときは気をつけよう・・・・ 構造体を切り替えればオケ

(Qiitaに書くようなレベルなのだろうか・・・)

サンプルコード

ctx.go
package 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

COTOHA でキーワードの抽出 (Golang)

COTOHA API Portal の使用例です。

フォルダー構造

$ tree -a
.
├── akai_rousoku.txt
├── .env
├── get_config.go
├── get_token.go
└── key_word.go
key_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" : ""
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

COTOHA で固有名詞の抽出 (Golang)

COTOHA API Portal の使用例です。

フォルダー構造

$ tree -a
.
├── .env
├── get_config.go
├── get_token.go
└── proper_noun.go
proper_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" : ""
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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" : ""
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカル環境で、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]ページからダウンロードできます。

https://ngrok.com/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=true
Mac/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.00

3.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.png

ローカルにある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で本機能を使うのではなく、より高度なテストの自動化をする場合が多いと思います。)

Ngrok_Web_Interface.png

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のリクエスト投げると、レスポンスで各種情報が得られます。
リクエスト時には、AcceptContent-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.go
package 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.cs
using 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.java
package 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 flask

Python 3.6 + flask 1.1 + Windows 10 Pro 64bitで動作確認しています。

webhook_listener.py
from 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)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 のインストール

http://mingw-w64.org/doku.php

  • MInGW-W64って?

    64bit windowsOSでCやC++をコンパイルできるようにするツールらしい(曖昧です)

  • 以下ページを参考にインストールしました

https://www.javadrive.jp/cstart/install/index6.html

cmakeのインストール

https://cmake.org/

[Download] から cmake-3.17.0-rc1-win64-x64.msi を選択してダウンロード & インストール

環境変数の設定

自分がどのディレクトリにいても、hoge.exeを実行できるようにwindows側に.exeファイルが存在するPath(環境)を登録しておきましょう。

環境変数の設定は次のような順番で行えます
1. [スタート] - [設定] - [設定の検索]で環境変数を編集で検索
setting.png

  1. Path変数を選択して、編集をクリック

  2. Pathに C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\binC:\Program Files\CMake\bin を追加してOKを選択する
    ※ インストールしたバージョンとインストールしたフォルダ名で上記の名前と違うことがあります。

opencvの環境構築

%GOPATH%\gocv.io\x\gocvwin_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です

hello.png

motion-detect

IntelのOpenVinoを使って物体認識をしているサンプルだと思います。

ある程度分類はできているようなので、返り値の値を上手く使って表示を工夫できれば面白そうですね。

どのカメラに接続するか引数で渡す必要があります。

go run gocv.io\x\gocv\cmd\motion-detect\main.go 0

motion_detect.png

facedetect

カスケード分類器を使った顔認識サンプルですが、あまり精度が良くない印象です。

第一引数にカメラ、第二引数にカスケード分類器のxmlが必要で、xmlはdataフォルダにあらかじめ準備されているので、それを使いましょう。

go run gocv.io\x\gocv\cmd\facedetect\main.go 0 gocv.io\x\gocv\data\haarcascade_frontalface_default.xml

faceblur

facedetectの発展形で顔認識した空間にblur処理(ぼかし)を行うサンプルです。

gocv.io\x\gocv\cmd\faceblur\main.go 0 gocv.io\x\gocv\data\haarcascade_frontalface_default.xml
  • blur結果
    アニメキャラクターの顔も認識してますね。

face_blur.png

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
  • 結果

output.png

まとめ

goで何か作りたいなと思ってた時に、CVのライブラリを見つけたので導入方法をまとめてみました。

暇があればgoでクローラーとかも作ってみたいです。

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

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 になっていることを確認し、関数を作成します。
image.png

ALBとターゲットグループを作成

EC2の画面を開き、左のメニューからロードバランサーを選択
スクリーンショット 2020-02-24 21.35.05.png

画面上位の ロードバランサーの作成 を押下
image.png

Application Load Balancer作成 を押下
スクリーンショット 2020-02-24 21.39.16.png

ロードバランサーの設定は各々の環境に合わせて設定してください

ターゲットグループを新規作成し、 ターゲットの種類Lambda になっていることを確認し、ターゲットの登録ボタンを押下
image.png

リストからターゲットにしたいLambdaを選択し、確認を押下
image.png

確認して問題なければ作成ボタンを押下

動作確認

作成したALBのDNS名をコピーして、実際にアクセスしてみて確認
スクリーンショット 2020-02-24 21.50.00.png

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.Requesthttp.ResponseWriterALBTargetGroupRequestevents.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.ResponseWriterALBTargetGroupResponse へ置き換えるだけで任意のレスポンスを返すように出来ます

ビルド&動作確認

ビルド

ここでのポイントはクロスコンパイルするところです

$ 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

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

hash/maphashコードリーディング

お前誰よ

  • 渋川よしき
    • フューチャー株式会社で仕事してます
    • いろいろ本を書いたりしています
      • みなさん買ってくださっていますよね?
    • 今日は、家族がいろいろ体調を崩していて時間が取れなかったのでQiitaスライドで

今回の内容

フューチャー社内で行ったコードリーディングの勉強会の再演です。現在、標準ライブラリをみんなで読んで解説しています。
わたしは1.14で追加されたhash/maphashを説明しました。


1.14の変更

ここによると、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がついた程度。完全な新機能を見ていたはずだが・・・

テストとか、中のテクニックはなかなか興味深いものもあったかも

明日からは役に立たない知識を中心にまとめました

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