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

GoのHTTPサーバにてログ用途などのためにrequestのbodyを"消費"させない読み取り方とそれに関連するnet/httpの実装

はじめに 〜 やりたいこと Request Bodyをログ用に引っ張ってもアプリの読み取りに影響が無いようにしたい。 前提 本記事でのライブラリコードでの日本語のコメントは筆者による日本語訳と筆者自身のコメントが混ざっているのでご注意ください。 環境 $ go version go version go1.17.2 darwin/amd64 読み取りが失敗してしまう実装 失敗してしまう実装 package main import ( "fmt" "io/ioutil" "net/http" ) // HTTPサーバ func hello(w http.ResponseWriter, req *http.Request) { bufForLog, _ := ioutil.ReadAll(req.Body) // 読み取り済みフラグがtrueになり次回はもう読み取れなくなる fmt.Println(bufForLog) // [123 10 32 32 32 32 34 97 34 10 125] bufForApp, _ := ioutil.ReadAll(req.Body) // すでに"消費"済みなので読み取れない fmt.Println(bufForApp) // [] fmt.Fprintf(w, "hello\n") } func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":8090", nil) } こちらの実装では上記コード内のコメントにあるように、2回目のioutil.ReadAllではBody読めなくなってしまいます。 読み取れなくなる理由をライブラリ実装を読んで理解する net/httpの標準ライブラリを追うとわかります。 http.Request.Bodyはio.ReadCloserインターフェース型であり実態であるインスタンスはnet/httpのtransfer.goにあるbody構造体です。関係しうるフィールドのみ抜粋すると以下になります。 Trailerについて1 ソースコードリンク net/http/transfer.go // bodyはReaderインターフェースをReadCloserインターフェースにしています。 // Closeメソッドはbodyが完全に読まれたことを保証し、必要ならばtrailerを読み込みます。 type body struct { src io.Reader r *bufio.Reader // trailerのための配下にあるワイヤーフォーマットreader // ... 省略 sawEOF bool closed bool // ... 省略 } ioutil.ReadAll(req.Body)にてhttp.Request.BodyのRead関数が呼ばれることになりますが、そこを追うと以下のコードがあります。 ソースコードリンク net/http/transfer.go func (b *body) Read(p []byte) (n int, err error) { b.mu.Lock() defer b.mu.Unlock() if b.closed { return 0, ErrBodyReadAfterClose } return b.readLocked(p) } // このメソッドを呼ぶときはb.muをロックしておかなければならない。 func (b *body) readLocked(p []byte) (n int, err error) { if b.sawEOF { // ② 読み込み済みならば即EOFを返す return 0, io.EOF } n, err = b.src.Read(p) if err == io.EOF { b.sawEOF = true // ① 読み込み終わったらbodyにあるsawEOFフラグをtrueにする // Chunked case. Read the trailer. if b.hdr != nil { if e := b.readTrailer(); e != nil { // ... 省略 上記のコードに①②として筆者がコメントを書いたように、body.sawEOFフラグにより読み取り済みかそうでないかを制御しており、冒頭のような結果になることがわかります。 requestのbodyを"消費"させない読み取り方 手がかりとなる標準ライブラリ実装 解決の手がかりにはnet/http/httputilパッケージのDumpRequest関数が参考になります。なぜならこの関数の振る舞いはRequest.Bodyを消費しないためです。この関数の処理を追うと以下の実装に行き着きます。 ソースコードリンク net/http/httputil/dump.go // drainBodyはbをすべて読み取り同等の2つのReadClosersを返します。 func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { if b == nil || b == http.NoBody { // No copying needed. Preserve the magic sentinel meaning of NoBody. return http.NoBody, http.NoBody, nil } var buf bytes.Buffer if _, err = buf.ReadFrom(b); err != nil { // A. 消費されbのsawEOFはtrueになる。 return nil, b, err } if err = b.Close(); err != nil { return nil, b, err } // B. 読み取ったbufを使いダンプ(ログ)用とRequest.Bodyへ消費されていないものを // 元に戻す(アプリ)用の新しいio.ReadCloserを2つ作る。 return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil } // ...省略 // bodyが存在するならばDumpRequestもbodyを返します。そうするためにDumpRequestはreq.Bodyを消費し // それを同じバイト数の新しいio.ReadCloserへと置き換えます。 func DumpRequest(req *http.Request, body bool) ([]byte, error) { var err error save := req.Body if !body || req.Body == nil { req.Body = nil } else { save, req.Body, err = drainBody(req.Body) // 上述 B. if err != nil { return nil, err } } こういう実装で解決できる 消費させない読み取り方 package main import ( "bytes" "fmt" "io/ioutil" "net/http" ) // HTTPサーバ func hello(w http.ResponseWriter, req *http.Request) { buf, _ := ioutil.ReadAll(req.Body) bufForLog := bytes.NewBuffer(buf) bufForApp := bytes.NewBuffer(buf) fmt.Println(bufForLog) req.Body = ioutil.NopCloser(bufForApp) // req.Bodyに新規に作ったものを入れ込む fmt.Println(req.Body) fmt.Fprintf(w, "hello\n") } 残る疑問について ここまでで筆者が疑問に思ったことを自身で調査しその答えを以下に簡潔に記載します。 Q. そもそも何故、リクエストのbodyを読み切る読みきらないの制御が必要なのか A. クライアントとの接続にてコネクションを再利用するかしないかの判断材料に利用したいから Bodyを読みきったときコネクションを再利用し、そうでないときコネクションを再利用しないという振る舞いになります。これはHTTP1.1での仕様由来です。 HTTPサーバにとってBodyが読み切れていないということはつまり接続に問題があるとと解釈しそのコネクションはCloseにして新しいコネクションにするようにし、逆にBodyを読み切れているときはそのコネクションは問題の無いコネクションだから再利用しようという背景であると推測しています。 より詳細な理由は下記の【参考にさせてもらった記事】を参照ください。 Q. 「消費させない読み取り方」にてもともとhttp.Request.Bodyのbody構造体だったのにioutil.NopCloserで作ったものにして良いのか A. ここでの実装よりrequest bodyがio.EOFとして読み込み済みであれば結果的に接続のコネクションの振る舞いは変わらないから。 ソースコードを読みそう解釈していますが筆者としては影響が全く無いと言い切れないです。こちらは別途、より調査してみようと思います。 参考にさせていただいた記事 HTTPの仕様にあるメタデータを仕込むためのヘッダーです。bodyの消費は接続再利用のCloseに関連しますがTrailerはClose時に読み込まれるということをライブラリはコメントアウトで述べています。https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

API Gateway + Lambda でリダイレクトする方法(Go言語)

目的 API Gateway + Lambda でリダイレクトさせたいときの Go 言語のコードを紹介します。 リダイレクトさせる Go 言語のコード APIGatewayProxyResponse は、リクエストに対して API Gateway によって返されるレスポンスを構成します。よって、APIGatewayProxyResponse にリダイレクト(3xx)のステータスコードとリダイレクト先 URL をセットしてあげれば、クライアントにリダイレクトさせることが可能です。 package main import ( "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { return events.APIGatewayProxyResponse{ StatusCode: http.StatusFound, // ステータスコード 302 を指定 Headers: map[string]string{ "Location": "https://aws.amazon.com/jp/", // リダイレクト先 URL を指定 }, }, nil } func main() { lambda.Start(handler) } APIGatewayProxyResponse の内容について APIGatewayProxyResponse で指定したリダイレクトのステータススコードの代表的なものとロケーションヘッダーについて簡単に解説します。詳細を知りたい方はリンク先を参照してください。 代表的なリダイレクトのステータススコード ステータスコードは http パッケージの定数を利用しています。以下に、リダイレクトのステータスコードの代表的なものを紹介します。用途にあわせて使い分けてください。 301 Moved Permanently:リクエストされたリソースが Location ヘッダーで示された URL へ完全に移動したことを示します。ブラウザは、301 リダイレクトを受け取ると、古い URL から新しい URL へのリダイレクトをキャッシュに記憶します。もう一度ブラウザで古い URL を表示しようとすると、古い URL にアクセスすることなく新しい URL にアクセスします。 302 Found:リクエストされたリソースが一時的に Location で示された URL へ移動したことを示します。ブラウザが 302 リダイレクトを受け取っても、リダイレクトをキャッシュに記憶することはありません。もう一度ブラウザで古い URL を表示しようとすると、ブラウザは古い URL にアクセスしたあとでリダイレクトします。 303 See Other:リダイレクトが新しくアップロードされたリソースではなく、 (確認ページやアップロード進捗ページのような) 別なページにリンクすることを示します。このレスポンスコードはふつう、 PUT または POST の結果として送り返されます。また、リダイレクトすときは GET メソッドを使用します。 307 Temporary Redirect:リクエストされたリソースが一時的に Location で示された URL へ移動したことを示します。ブラウザが 307 リダイレクトを受け取っても、リダイレクトをキャッシュに記憶することはありません。307 と 302 の唯一の違いは、 307 はリダイレクトされたリクエストが行われるときに、メソッドと本文が変更されないことが保証されることです。 使用されるメソッドを GET に変更したい場合は、代わりに 303 See Other を使用してください。 308 Permanent Redirect:リクエストされたリソースが Location ヘッダーで示された URL へ完全に移動したことを示します。ブラウザは、308 リダイレクトを受け取ると、古い URL から新しい URL へのリダイレクトをキャッシュに記憶します。301 の場合は GET メソッドに変更される可能性があるのに対し、308 の場合はリクエストメソッドと本文が変更されません。 ロケーションヘッダー ロケーションヘッダー はリダイレクト先の URL を示します。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】Qiita デイリー LGTM 数ランキング【自動更新】

他のタグ AWS Android Docker Git Go iOS Java JavaScript Linux Node.js PHP Python Rails React Ruby Swift TypeScript Vim Vue.js 初心者 集計について 期間: 2021-10-23 ~ 2021-10-24 GitHub スターをもらえるととっても励みになります LGTM 数ランキング 1 位: ディレクトリ、ファイルのコピーを Golang で書いてみる Go 3 LGTM 1 ストック @hfh3oa さん ( 2021-10-23 23:58 に投稿 )
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

git hooksでpre-commit hooksを使って自動化した話

git のcommit前にlintやらを差し込みたいなということgit hooksを使って設定してみました。 git hooksとは gitが提供している公式の機能でgitの特定のイベントをトリガーにして自分が設定したscriptを実行させる機構です。 例えば、push前にtestを実行をさせたり、commit前にlintを走らせたりできます。 今回は、commit前にlintを走らせる処理を書きました。 設定方法(pre-commit) git hooks自体はgitに内蔵されています。 具体的には.gitの中にscriptのサンプルファイルがあります。 terminal でgitで管理しているプロジェクトのrootに移動して 下記のコマンドでhooksファイルがあるディレクトリに移動しましょう。 cd .git/hooks/ すると、以下のファイルがあり、 .sample という文字列をファイル名が除外して chmod a+x <ファイル> と権限を与えればhooksが有効になります。 applypatch-msg.sample fsmonitor-watchman.sample pre-applypatch.sample pre-merge-commit.sample pre-rebase.sample prepare-commit-msg.sample commit-msg.sample post-update.sample pre-commit.sample pre-push.sample pre-receive.sample update.sample 今回は pre-commitを使いたいので以下の用にリネームします。 pre-commit.sample -> pre-commit pre-commitのscriptを書く 実際にscriptを書きましょう。  今回はgolang-ci-lintを使って lintを実行させる formatを実行させる を行いたいので以下のようにしました。 #!/bin/sh echo "lint start" cd $(git rev-parse --show-toplevel) RESULT=$(golangci-lint run pkg/...) if [ -n "$RESULT" ]; then echo "lint failed" exit 1 fi git add . echo "lint finish" lintの実行場所を固定にする 下記は必ずscriptの実行場所が必ずproject rootになるように、gitの管理場所を取得して、そのディレクトリに移動するようにしています。 git rev-parse --show-toplevel lintの実行結果を格納 下記はlintの実行結果を格納しています。 これはあまりよろしくなく、本来は終了ステータスを格納すべきですが、誤ってそのまま実行結果の文字列を格納しちゃってます。 RESULT=$(golangci-lint run pkg/...) 強制的にformatをかけてcommitさせる pre-commit内でgit addしてる理由は、format後のファイルをステージするためにです。 git add . golangci-lintはauto formatの機能があるので、実際のcommitは以下の処理順番になります。 git commit コマンドを打つ pre-commitが発火 golangci-lint run pkg/... でファイルにformatがかかる git add . formatがかかって変更されたファイルが新たにstageされる commit 処理が走る このようにしておくと、formatのかけ忘れなどを防ぐことができます。 管理方法 これでhooksは完成しましたが、.git内にファイルがある関係上このままではgit管理ができません。 なのでhooksファイルとhooksの参照先ファイルを変更します。 変更方法は以下の通り 適当なディレクトリを作り、そこのhooksファイルを移動 mkdir .githooks mv ./git/hooks/pre-commit .githooks/ gitのhooksファイルの参照先を変更 git config core.hooksPath .githooks hooksに実行権限を付与 chmod a+x .githooks/pre-commit これで、hooksファイルの管理場所が.gitでは無くなったのでgit管理できるようになりました。 最後に hooks といえば husky 自分で作るのは面倒くさいと敬遠していたのですが、重い腰をあげて作ってみると思いのほか簡単にできました。 何かの参考になれば幸いです。 それでは良い git hooksライフを
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでcsvデータを読み取り、DBに保存

はじめに csvのデータをDBに保存しなけらばならなくなり csvデータをDBに保存する 機能を作成しましたのでメモ程度に記載します。 環境は echo+gorm+docker です ディレクトリ Project ├──docker | ├──go | | └──Dockerfile | └──mysql | ├──docker-compose.yml └── src | └──go ├── controllers ├── database ├── csv | └──schooldata.csv | ├── db (migationとかseedとか) ├── dbconfig.yml ├── go.mod ├── go.sum ├── main.go └── fresh.conf 実行するgoファイル 今回は schoolsというテーブルに csvの学校データを大量に読み込むという想定で作成しました。 package main import ( "encoding/csv" "os" "fmt" "api/database" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) type Schools struct { Area string `gorm:"column:area"` //学校の所在地 Name string `gorm:"column:name"` //学校名 } func main() { db := dbconnect.Connect() defer db.Close() if err := readingcsv(db);err != nil { fmt.Println(err) } } func readingcsv(db *gorm.DB) error { //読み込むCSVファイルを記載 csvFile, err := os.Open("csv/schooldata.csv") fmt.Println("csvは読みこめませんでした") if err != nil { return err } defer csvFile.Close() reader := csv.NewReader(csvFile) reader.FieldsPerRecord = -1 record, err := reader.ReadAll() if err != nil { fmt.Println(err) os.Exit(1) } for i := 0; i < len(record); i++ { fmt.Println(record[i][0]) fmt.Println(record[i][1]) schools := Schools{Area: (record[i][0]),Name:(record[i][1])} if err := db.Create(&schools).Error; err != nil { fmt.Printf("%+v", err) } } return nil } あとは実行するだけ docker-compose exec go go run main.go
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む