- 投稿日:2019-11-24T22:18:54+09:00
Go Modules で stringer なんかの開発用ツールを管理するには
はじめに
- Go Modules で stringer や lint など開発用のツールを依存パッケージとして管理する方法についてまとめます。
Go Modules で開発ツールを管理できない問題
- stringer などの開発ツールを go get すると一時的に go.mod に記載されますが、コードの中で import されていないため go mod tidy すると go.mod から外れてしまいます。
解決策
- コード中で import されてさえいれば Go Modules の管理対象にできるので、開発ツールを import するだけのファイルをおいておきます。
- ファイル名は何でも良いのですがとりあえず
tools.go
で統一しておくとわかりやすいかもしれません。tools.go// +build tools package main import _ "golang.org/x/tools/cmd/stringer"解説
- 行頭の
// +build
はビルドタグと呼ばれるもので、go build 時に固有環境向けのコードを実装するために利用されます- tools.go のビルドタグには
// +build tools
と記載されているため tools タグがある場合のみコンパイル対象になります- 実際には tools タグ付きでビルドされることはないので、実行時に何らかの影響を与えることなく Go Modules のために開発ツールを import するということが tools.go によって実現されています。
- かなりバッドノウハウっぽいですが、Go Modules の Wiki に記載されている手法なのでとりあえずこうしておくのが良さそうです。
- 投稿日:2019-11-24T21:04:12+09:00
Go使ってAWS Lambdaでラムダ計算した
tl; dr
ソースコードはこちら
エンドポイントは
https://nd88j25ztg.execute-api.ap-northeast-1.amazonaws.com/dev/lambda
でヘッダにContent-type: application/json
とX-API-KEY: uwuZMJIWbqpmTpfzdEci2YaMGWFSvWz9ZfWFIjVf
を仕込んで{"step":1,"src":"(λx.x)a"}
みたいにPostしてください。curlでやると$ curl -X POST -H "Content-type: application/json" -d '{"step":1, "src": "(λx.x)a"}' -H 'X-API-KEY: uwuZMJIWbqpmTpfzdEci2YaMGWFSvWz9ZfWFIjVf' https://nd88j25ztg.execute-api.ap-northeast-1.amazonaws.com/dev/lambdaみたいな感じ。なお予告なく削除することもあるのでご了承を。
前置き
AWS Lambdaでラムダ計算をするというしょうもないネタ、絶対誰かやってると思ったんですが案外誰もやってない。そんじゃいっちょやってみっかてな感じで、じゃあ言語どうするかって考えたらやっぱ静的型付けがいいよねということでGoに決定。Goやるやる詐欺やめて触ってみるいい機会なのでやってみました。パッケージわけが面倒だったので全部
main
です。そもそもラムダ計算とは
正しく説明できそうにないので参考文献を挙げるにとどめておきます……。ここでは形無しラムダ計算のみ考えます。
ラムダ計算入門 https://www.slideshare.net/_yingtai/lambda-guide
ラムダ計算ABC - Sendai Logic Homepage https://sites.google.com/site/sendailogichomepage/files/ref/ref_03ラムダ計算のモデルを使ってもチューリングマシンと同じ表現力がありますよというお話。
Goで実装する
ラムダ計算
型システム入門を読みながらポチポチやってたらめっちゃ簡単でした
lambda.gopackage main import ( "sort" "strconv" ) type Name string type Names []Name func (xs Names) Len() int{ return len(xs) } func (xs Names) Less(i, j int) bool { return xs[i] < xs[j] } func (xs Names) Swap(i, j int) { xs[i], xs[j] = xs[j], xs[i] } type Var struct { name Name } type Lam struct { name Name expr Expr } type App struct { f Expr arg Expr } type Sym struct{ symbol rune } type Expr interface { reduce() Expr } func (x Var) reduce() Expr { return x } func (x Lam) reduce() Expr { return x } func (x App) reduce() Expr { switch g := x.f.(type) { case Var: return App{g, x.arg.reduce()} case Lam: return subst(g.name, x.arg.reduce(), g.expr.reduce()) case App: return App{x.f.reduce(), x.arg.reduce()}.reduce() default: panic("") } } func (x Sym) reduce() Expr { return x } var cnt = 0 func subst(x Name, s Expr, y Expr) Expr { switch z := y.(type) { case Var: if x == z.name { return s } else { return y } case Lam: if x == z.name { return z } else { if !elem(z.name, free(s)) { return Lam{z.name, subst(x, s, z.expr)} } else { n := Name((string)(z.name) + strconv.Itoa(cnt)) cnt++ return Lam{n, subst(x, s, subst(z.name, Var{n}, z.expr))} } } case App: return App{subst(x, s, z.f), subst(x, s, z.arg)} default: panic("") } } func free(x Expr) Names { switch y := x.(type) { case Var: return Names{y.name} case Lam: return remove(y.name, free(y.expr)) case App: return union(free(y.f), free(y.arg)) default: return Names{} } }Go言語で union とか直和型のようなデータを表現したいときは interface を使う - 嵐の小舟より https://tmrtmhr.info/tech/sum-type-in-golang/
こちらを参考にADTっぽいものをやってみました。中々それっぽく出来ていい感じ。パーサー
こっちのほうが難航したという。まず以下の部分で逆ポーランド記法に直してます。
parser.gofunc toRpn(str string) ([]rune, error) { stack := []rune{} rpn := []rune{} isParam := false lamCnt := 0 for _, c := range str { switch c { case '\\', 'λ': if isParam { return nil, errors.New("lmbda in parameters") } stack = push('\\', stack) isParam = true case '.': for { xs, x, err := pop(stack) if err != nil { return nil, errors.New("mismatched lambda") } stack = xs if x == '\\' { break } rpn = append(rpn, x) } isParam = false case '(': if isParam { return nil, errors.New("parens in parameters") } stack = push('(', stack) case ')': if isParam { return nil, errors.New("parens in parameters") } for { xs, x, err := pop(stack) if err != nil { return nil, errors.New("mismatched parens") } stack = xs if x == '(' { break } rpn = append(rpn, x) } for i := 0; i < lamCnt; i++ { rpn = append(rpn, '\\') } lamCnt = 0 case ' ', ' ': default: rpn = append(rpn, c) if isParam { lamCnt++ rpn = append(rpn, '.') } } } if isParam { return nil, errors.New("lacking expression") } for { xs, x, err := pop(stack) if err != nil{ break } if x == '(' || x == ')' || x == '\\' || x == '.' { return nil, errors.New("invalid tokens remain") } stack = xs rpn = append(rpn, x) } for i := 0; i < lamCnt; i++ { rpn = append(rpn, '\\') } return rpn, nil }逆ポーランド記法にすることにより括弧を除去できます。これをパーサに読み込ませてASTを得ます。
parser.gofunc parse(str string) (Expr, error) { rpn, err := toRpn(str) if err != nil { return nil, err } stack := []Expr{} for _, t := range rpn { switch t { case '\\': lam := true for lam { xs, x, _ := popExpr(stack) ys, y, err := popExpr(xs) if err != nil { return nil, errors.New("token exhausted") } stack = ys if w, ok := x.(Sym); ok { x = Var{Name(string([]rune{w.symbol}))} } if z, ok := y.(Sym); ok { if z.symbol == '.' { vs, v, err := popExpr(stack) if err != nil { return nil, errors.New("argument notfound") } stack = vs if u, ok := v.(Sym); ok { stack = pushExpr(Lam{Name(string([]rune{u.symbol})), x}, stack) lam = false } else { return nil, errors.New("argument must be symbol") } } else { y = Var{Name(string([]rune{z.symbol}))} stack = pushExpr(App{y, x}, stack) } } } default: stack = pushExpr(Sym{t}, stack) } } for { xs, x, err1 := popExpr(stack) if err1 != nil { return nil, errors.New("no result") } if x0, ok := x.(Sym); ok { x = Var{Name(string([]rune{x0.symbol}))} } ys, y, err2 := popExpr(xs) if err2 != nil { return x, nil } else { if y0, ok := y.(Sym); ok { y = Var{Name(string([]rune{y0.symbol}))} } stack = pushExpr(App{y, x}, ys) } } }
λ
でExpr
を消費しつつApp
にまとめていってます。prettify
てけとーにASTを文字列に直してるだけなので特に語ることなし。
main
Serverless Framework(Go) でHello worldしてみる - Qiita https://qiita.com/seike460/items/b54a61ec8d07b2be8c87
こちらを参考に、リクエストに対してレスポンスを返す関数を書くだけでした。簡単!
感想
AWS LambdaでWebAPIをさらっと作りたいときはserverless本当に便利ですね。Terraformの特化版のよう。Goは初めて書きましたが思いの外サクサク書けて良い感じ。C言語のような原初の風景も感じますが、エディタの補完が強力なので気持ちよく書けます。今度は非同期処理とかもやってみたいですね。
- 投稿日:2019-11-24T17:50:11+09:00
[Go] 音声実況動画をMacで作りたくて(その一)
はじめに
ゆっくり実況の使う音声はライセンス的に,いまいち自由に使いにくい。
https://manjubox.net/ymm4/
しかもwindows環境ならまだしも、Macでうまいことやろうとすると、virtual boxを入れたりとか
音声とは別に字幕作ったりとか正直めんどくさい手間がかかる。
ネットの海を探したら、あるのかもしれないが、そんなググる力はなかった。だったらGoで作った方が
楽しそう融通がきいてカスタマイズが自由自在だから良いと思う(小並感)ツール探し
音声
ライセンス的に自由に使えそうなのを探した結果、以下のOpenJTalkを採用。
コマンドラインから使えるので、ラップして使うことに決定
参考
OpenJTalk
http://kuuur.net/tech/movie-voice/openjtalk字幕画像作成
Go製のツールでやりたいことから以下ツールを選定
"github.com/disintegration/imaging" "github.com/fogleman/gg"実行環境
13 inch mac book pro
go version go1.13 darwin/amd64実装
自分だけしか使わないのでGUIを実装を諦めてコマンドラインツールとして作成することにした。このツールできること
・複数人の会話劇
・音声ファイルの出力
・読み上げキャラの画像を埋め込み
・字幕画像の出力
・文字色の指定
・文字のプレフィックスによるキャラクターわけ
・一部自動改行いまいちな点
画像内の文字の位置計算が微妙で結構見切れる(未完成)
処理が効率化されていない(未完成)
READMEが役割を果たしていない...
コメントが中途半端実行方法
1, 以下のようなテキストファイルを用意する。
sample.txt1.こんにちは。ゆっくり実況始めるよーーー2, 設定ファイルに音声ファイルや、画像サイズ、キャラクター画像を設定する
3, go run main.goで実行する出力結果
得られた知見
・他オープンソースツール公開者への感謝
・modの使い方
・基本的な画像処理
・viperの使い方
・tomlの扱い方その他
一応参考までにgit hubにあげておきますが、ソースコードを信用してはいけません。個人ツールです。
https://github.com/hiromichi-n/text2talkそもそも素材収録してないので、結局何がしたかったのか忘れたorz。
次は使い始めてからのバグ修正編を書きたい(願望)
- 投稿日:2019-11-24T17:11:58+09:00
GoでUnion型/直和型をいい感じに表現する方法
TL;DR
直和型を実装するのではなく、(型の)パターンマッチの方を実装します。
型のパターンマッチはクロージャーを使って以下のように表現できます。IntOrString.Match(Cases{ // 型のパターンマッチ Int: func(i Int) { sum += int(i) }, // Int型だった場合の処理 String: func(s String) { sum += len(string(s)) }, // String型だった場合の処理 })そもそもUnion型/直和型とは
Union型/直和型とはどちらか片方の型の値を取るような型のことで典型的には以下のような構文で表現されます。
type IntString = Int | Stringこの機能を持っている言語としては静的型付け関数型言語(Haskell/OCaml/F#)、強力な型機能のある言語(Rust/TypeScript)などがあります。
Goを使っていても、"複数の型を取りうる型"を考えたくなることがあり、この場合よくやられている方法として次のようなものがあると思います。
GoでUnion型/直和型を表現するこれまでの(あまりイケてない)やり方
型のswitch文を使う
以下のような
value.(type)
を使った書き方はよく見られます。switch v := value.(type) { case string: sum += len(v) case int: sum += v }ここでの問題は
value
をinterface{}
として扱う必要があり、型による保証を受けられず型安全ではありません。structを使う
型を要素として含むような
struct
を作るやり方もよく見られます。type IntStringUnion struct { Int int String string }この方法の問題点は不整合な値が許容されることです。
具体的には複数のフィールドに非ゼロな値が入ってしまうことが可能で、こうなってしまうともやは直和型の値とは呼べません。v := IntStringUnion{ Int: 10, String: "hoge", }また、元となる型のzero-valueをどう表現するのかという問題もあります。
今回提案する表現方法
直和型を利用する際には、パターンマッチを使って元の型を取り出して処理しますが、このような処理を実装することを考えます。
具体的には以下のようなパターンマッチを表現するメソッドMatch
を持った型(インターフェイス)を考えます。// IntとStringの直和型 type IntStringUnion interface { Match(Cases) }メソッド
Match
に渡す構造体Cases
は、各型で行う処理を収めた構造体です。// Int/String型の場合に実行する処理を格納する構造体 type Cases struct { Int func(Int) String func(String) }構造体
Cases
に処理内容を入れて組み立て、メソッドMatch
を呼び出すことで各型ごとの処理へとディスパッチすることができます。item.Match(Cases{ Int: func(i Int) { sum += int(i) }, // Int型だった場合の処理 String: func(s String) { sum += len(string(s)) }, // String型だった場合の処理 })
IntStringUnion
を構成する元の型、Int
とString
は以下のように定義します。
この定義により、それぞれIntStringUnion
のインターフェイスを満たすことに注意してください。type Int int func (i Int) Match(c Cases) { c.Int(i) } // IntStringUnionインターフェイスの実装 type String string func (i String) Match(c Cases) { c.String(i) } // IntStringUnionインターフェイスの実装
Int
とString
共にIntStringUnion
のインターフェイスを満たすので、以下のような表現が可能となります。unionArray := []IntStringUnion{String("1"), Int(2), String("123"), Int(4)}全体のコードの例は以下のようになります。
package main import ( "fmt" ) // IntとStringの直和型 type IntStringUnion interface { Match(Cases) } // Int/String型の場合に実行する処理を格納する構造体 type Cases struct { Int func(Int) String func(String) } type Int int func (i Int) Match(c Cases) { c.Int(i) } // IntStringUnionインターフェイスの実装 type String string func (i String) Match(c Cases) { c.String(i) } // IntStringUnionインターフェイスの実装 func main() { // IntStringUnion型からなる配列 unionArray := []IntStringUnion{String("1"), Int(2), String("123"), Int(4)} // 総和を計算する、Int => その値そのまま、String => 文字列の長さ、として各要素を評価して加算する sum := 0 for _, item := range unionArray { item.Match(Cases{ // ここで型のパターンマッチを行う Int: func(i Int) { sum += int(i) }, // Int型だった場合の処理 String: func(s String) { sum += len(string(s)) }, // String型だった場合の処理 }) } fmt.Printf("%d", sum) // => 10が出力される }補遺
上記の
IntStringUnion
インターフェイスの実装だと例えばCase.Int
がnil
の場合ランタイムエラーが起きるので、以下のような実装のほうがより安全でしょう。type Int int func (i Int) Match(c Cases) { if c.Int != nil { c.Int(i) } }
- 投稿日:2019-11-24T16:40:34+09:00
gRPCの通信をgzip圧縮する(go言語)
サーバ・クライアント間でデータをやり取りするとき、ネットワーク帯域がボトルネックになるケースは多いです。
gRPCはProtocol Buffersによってデータのシリアライズをしており変数名などはタグ化されるし、数値型はちゃんと数値データとして送信されるので、JSONに比べて通信量的に有利です。
しかし所詮はその程度なので、大量のデータをやり取りすることを考えるときちんとデータを圧縮するべきです。go言語のgRPCパッケージにはgzip圧縮用のライブラリが組み込まれており、少ないコードで通信をgzip圧縮できます。
サーバ側
server.go(省略) import ( "google.golang.org/grpc" _ "google.golang.org/grpc/encoding/gzip" : (省略)"google.golang.org/grpc/encoding/gzip"をアンダースコア付でインポートする。それだけです。
こうすると、起動時にinit関数が呼ばれて、gzip圧縮ができるサーバとして動作します。クライアント側
client.go(省略) import ( "google.golang.org/grpc" "google.golang.org/grpc/encoding/gzip" : (省略) func main() { conn, err := grpc.Dial("localhost:19003", grpc.WithInsecure()) if err != nil { log.Fatal("client connection error:", err) } defer conn.Close() client := hogegrpc.NewHogegrpcClient(conn) message := &hogegrpc.GetDataMessage{TargetCode: 0} response, err = client.GetData(context.TODO(), message, grpc.UseCompressor(gzip.Name)) (省略)サーバ側と同じくgzip圧縮のパッケージをimportします。こちらはアンダースコア無し。
そしてgRPCで実行する関数の可変引数部分に、grpc.UseCompressorを指定してあげます。
こうすると、クライアントからサーバに接続する時に「gzip圧縮できるよ」という情報が渡り、gzip圧縮してデータのやりとりができます。見ての通り関数呼び出しごとに指定するため、文字列等の圧縮が効きやすいデータはgzip圧縮して通信、メディア等の圧縮済みのデータをやりとりするなら無圧縮、という使い分けが容易です。
性能試験
適当なオープンデータを垂れ流すgRPCサーバを立てて試験しました。
内容は数値半分、文字列半分。1レコードあたり400byte前後のデータになります。送信するパターンは1レコードずつ送信・全レコード(4,900件)一括送信の2つ。
試験環境はサーバ/クライアントを同一マシン上に置いたパターンと、別マシン上に置いて無線LANで通信させるパターンで測定します。時間は関数呼び出しの前後で取得し、「クライアントから要求を出して、クライアント上でデータのデシリアライズが終わるまで」を測定します。
1レコードずつ送信
1回400byte前後のデータをやりとりします。
通信先 無圧縮 gzip圧縮 localhost 0.10 ms 0.50 ms 無線LAN上 1.87 ms 2.20 ms えー、大々的に言ったわりにgzip圧縮は遅い。
圧縮・展開にかかるオーバーヘッドがかなり大きいことと、たかが400byte程度のペイロードでは1パケットに収まってしまうので、圧縮しようがしまいが通信量が変わらないことが原因でしょう。全レコード一気に送信
1回1,900Kbyte程度のデータをやりとりします。
通信先 無圧縮 gzip圧縮 localhost 15.81 ms 30.08 ms 無線LAN上 190.06 ms 56.51 ms 通信量が増えるとgzip圧縮の効果が見えてきます。
localhost上ではさすがにネットワーク帯域がボトルネックにならないので圧縮するだけ不利ですが、無線LAN上のマシンに対しては3~4倍近い性能差が出せています。
ちなみにスループットは100Mbps超ぐらい出ており、明らかに無線LANの帯域がボトルネックです。まとめ
以下を満たす条件では、gzip圧縮を検討する価値があります。
- ネットワーク帯域がボトルネックだ
- 通信するデータが文字列や数値など、圧縮効果が見込める
- 一度に転送するデータが1パケットに収まらない
gRPCがなんだか遅いな、っていう人は試してみるといいんじゃないでしょうか。
- 投稿日:2019-11-24T15:57:51+09:00
テンプレートのパイプライン
- 投稿日:2019-11-24T15:35:24+09:00
GAE/Go go112 移行 のなにか ニャ
何?
苦しい。。。移行だった? 多分移行のさせ方がひどいと思ったが正しい認識(ユーザー視点)
やった表
※ lib change -> appengine ライブラリから cloud.google.com のものに変更
項目 Code Local Env GCP Env Datastore lib change
・Connection Pool,Namespace周り ConfigurationしたEmulatorに切り替え 特にすることなし
・Viewer npm のものを利用中
・dev_appserver.py を利用にはpython は gRPCのインストールが必要CloudTask lib change Local で使えないので、POST するやつ自作対応 API の認可 Memcached 部分的に LRU 今の所なし 代替え先サービスを利用していない Logging 自作 (Log参考リンクから対応) Stdout 出力 GCPの仕様に合わせて、出力 app.yaml login required google_sign-in に移行 変更なし 許可ドメイン設定などをする dev_appserver.py app.yaml を利用しないので、起動時の環境変数とか自前で設定が必要になる。
・Localでの静的なファイルのRoutingがなかなか面倒realise に変更 app.yaml 移行箇所編集 runtime, handlers ... gcloud app deploy .gcloudignore を書く N/A gcloud build 使うようになった CloudSQL 変更なし 変更なし unix socket を使ったDSNに変更 PubSub 変更なし 変更なし 変更なし 参考リンク
- Logging
- 投稿日:2019-11-24T14:49:14+09:00
c++ゲームエンジニアからGolang Webエンジニアになりましてはや数ヶ月。
Go6 Advent Calendar 2019 の 12/6 の記事になります。
はじめに
新参Golangerのuechocoです。
もともとはブラウザゲームのWebエンジニアでしたが、アプリ化の波に乗って直近では4年半ほどスマートフォンゲームの開発でc++を書いていました。c++のスキルがめっちゃ高くなったかというと初中級程度だと思います。そう言っておかないとその道の猛者たちに問い詰められそうなのでそう言っておきます。そして3ヶ月ほど前にまたWebエンジニアに戻りました。
メインループの中でポインタの寿命や所有権を気にしながら(いうほど気にしてなかったかもしれないけど)常に処理を行い続けるゲーム開発から、(だいぶシンプルに言うと)リクエストを受け取ってレスポンスを返すのWeb開発にパラダイムシフトしました。もちろんWebにはWebの厄介事があり、ネットワークがどうとか、データストレージ(RDBMSなど)がどうとか、非同期メッセージがどうとか、まぁ大変ですね。
何が言いたいかといえば、楽しくやっております(近況報告)。さて、c++の世界からGoの世界に来て3ヶ月ほど立ちましたので、比較して気づいたことをつらつらと書き留めておこうと思いました。
すんなり馴染めたこと
静的型付け
安心感あるね。
sliceのlenとcapの概念
これはc++のstd::vectorなどのコンテナでも同様の概念があります。
事前にcapを確保することがよいという考え方も、std::vectorなどのreserve()にあたるもので、当然のことと思えました。便利だと思ったこと
関数ポインタ型に比べて、func型は書きやすい
Go's Declaration Syntax にもそこら辺のことが書いてありますね。もっとも、c++11であればstd::functionなどもあるので、その差はだいぶ縮まったかもしれません。
ガーベジコレクション
よほど変な使い方しなければメモリ勝手に開放してくれるっていいですね。
c++のゲーム開発の中盤から終盤にかけて、メモリの開放漏れを潰していく作業はどこの現場でもありますよね(悲しい目インターフェースによるダックタイピング
これは書き方の違いかな。c++にもテンプレートでダックタイピングできますけど、テンプレートっていろいろ大変。個人的には、c++のテンプレートとは、開発チームのテンプレートに対する熟練度に合わせてテンプレートも使っていくのがいいと思っています。私自身も使いこなせているわけではないし。と思ってしまうくらいには複雑なものという印象。
一方でGo言語のインターフェースは、個人的な感想ですけどすんなり書けますね。まぁインターフェースを使わないとできないことにすぐにぶち当たるといったほうがいいのかもしれませんが。気になってしまったこと
string型の引数に怯えてた
func hoge(text string) error { // 処理 }なんてことはない文字列を受け取ってなにかの処理をする関数ですが、最初はこれが怖かったんです。 このstringってメモリ全コピーされないの?これはCopy-On-Writeとか最適化かかっている?ポインタにしないで使っているのやばくない?どのくらいの文字列長なら気にしないでいいとかある? とか考えてました。c++では文字列型に
std::string
クラスを用いることが多いと思いますが、値を変更する必要のない文字列を引数に与えるときはたいていconst std::string&
のように明示的に参照渡しかつ変更しないことを指定していたのです。どうやらGoのstringは、文字列データに対する長さとスライスを格納するstructのようなもので、string型をコピーしただけでは、長さとスライスのポインタアドレスがコピーされるだけのようでした。メモリ全コピーのような高コストなことはなさそうでした。
- Arrays, slices (and strings): The mechanics of 'append' - The Go Blog
- Does Go language use Copy-on-write for strings - Stack Overflow
ポインタ気軽に使えすぎてぬるぽへの恐怖が薄れてきた
ちょっと郷に従いすぎてしまったんでしょうか。
- ドット演算子が有能すぎてポインタであるかどうかを意識しない。
- あまりにも気軽にポインタ型を作れて返却して引き回してしまう。
- ポインタの所有権や寿命に関して意識することがない。
- レシーバーも大抵はポインタで書いてしまうことが多いし。
- err != nil はお決まりのフレーズ。
- 総じて、ポインタというものへの取り扱いが雑になってしまった。
その結果
*data.hoge
って書いたときにたまにnilぽしてpanicする。
いや、熟練度が足りていないだけです。
ただ、c++時代に比べると、ポインタに対する意識がほんとに変わってしまいました。ガッ
ranged-for的に書こうとしてポインタでやらかした
実際に業務でやらかした事例です。
structのコピーに抵抗があったので、map化するときにポインタを取得しようとしたんですけど、ハマりました。
// https://play.golang.org/p/uWlsye5ovBl package main import ( "fmt" ) type SomeModel struct { ID uint Name string State uint } func main() { models := make([]SomeModel, 0) m1 := SomeModel{ID: 1, Name: "田中", State: 1} m2 := SomeModel{ID: 2, Name: "佐藤", State: 2} m3 := SomeModel{ID: 3, Name: "池田", State: 5} models = append(models, m1) models = append(models, m1) models = append(models, m2) models = append(models, m3) if err := save(models); err != nil { fmt.Printf("%s", err.Error()) panic(0) } } func save(models []SomeModel) error { countMap := make(map[uint]uint, len(models)) modelMap := make(map[uint]*SomeModel, len(models)) for _, model := range models { countMap[model.ID]++ if _, ok := modelMap[model.ID]; !ok { modelMap[model.ID] = &model // ココ } } // TODO: DEBUG CODE 消す fmt.Printf("countMap: %+v\n", countMap) for k, v := range modelMap { fmt.Printf("modelMap[%d] = %v (p=%p)\n", k, *v, v) } // ... 処理 return nil }
save()
メソッドのPrintfの結果、こうなりました。countMap: map[1:2 2:1 3:1] modelMap[1] = {3 池田 5} (p=0x40a0f0) modelMap[2] = {3 池田 5} (p=0x40a0f0) modelMap[3] = {3 池田 5} (p=0x40a0f0)期待していた結果はこちらでした(修正後: https://play.golang.org/p/1SAkmkR24hn)
countMap: map[1:2 2:1 3:1] modelMap[1] = {1 田中 1} (p=0x432100) modelMap[2] = {2 佐藤 2} (p=0x432120) modelMap[3] = {3 池田 5} (p=0x432130)c++11には、ranged-forという構文がありまして、Goのrangeとよく似た書き方でループ処理が書けたりします。
std::vector<Data> v; for (const Data& elem : v) { // 処理 }このranged-for構文は、受け取る変数の型が指定できるのですが、
Data&
const auto&
のように参照渡しで書くことが多いです。Goのfor/rangeは構文がよく似ているので「参照渡しされているポインタをmapに詰め直せば良い」と思い込んでしまいました。実際には参照渡しされていなかったというわけです。先入観は良くないですね、、、標準関数の少なさ。algorithm.hがない
これはc++と比較しなくてもよく言われていることだと思います。
c++にはalgorithm.hというstdコンテナに対する便利ライブラリがあります。sortはGoにもありますが、unique, find_if, remove_ifとかがありません。同等のコードを一体何回書いただろうか、、、おわりに
Goの正規表現なんとかならないの。
- 投稿日:2019-11-24T11:58:49+09:00
サードパッケージを使用したGoのアプリケーションをHerokuにデプロイする
Portfolioを作るに当たってGoでHerokuにデプロイしようとしたらハマった
結論からいうとパッケージのコミット漏れHerokuで公式HPにデプロイ方法は載っていたが、サードパッケージを使用した際はハマるので今回の事象を押さえておく
Goでアプリケーションを作成
今回はginでプロジェクトを作成する
Heroku用のフォルダを作成し、以下のファイルを配置main.gopackage main import ( "github.com/gin-gonic/gin" ) type User struct { Name string Age int } func main() { router := gin.Default() // css、js などの静的ファイルを読み込む場合。今回は使用しない。 // router.Static("/assets", "./assets") router.LoadHTMLGlob("templates/*.html") router.GET("/", handler) router.Run() } func handler(ctx *gin.Context) { user := User{"User", 20} ctx.HTML(200, "index.html", gin.H{ "user": user, }) }templates/index.html<!DOCTYPE html> <html> <div> <p>Name: {{.user.Name}} </p> <p>Name: {{.user.Age}} </p> </div> </html>govendorを使用してサードパッケージを追加
govendor をインストールしてから行ってください。
https://github.com/kardianos/govendorgovendorについては下記の記事がとても理解できたので参考にさせて頂きました。
Heroku への Go 言語製アプリケーションのデプロイと依存パッケージ管理方法の比較# 初期化 $ govendor init # 依存パッケージのダウンロード $ govendor fetch +out後はHeroku公式のコマンド通り、コミット→プッシュを行う
$ git init && git add -A . Initialized empty Git repository in /Users/kazu/workspace/go/src/Heroku/.git/ $ git commit -m "init" create mode 100644 main.go create mode 100644 templates/index.html 〜〜〜 以下、vendor配下のファイル 〜〜〜〜〜〜 create mode 100644 vendor/*** 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 create mode 100644 vendor/vendor.jsonHerokuのアプリを作成
$ heroku create Creating app... done, ⬢ ancient-mesa-82891 https://ancient-mesa-82891.herokuapp.com/ | https://git.heroku.com/ancient-mesa-82891.git $ git push heroku masterGoのアプリケーションのデプロイ完了
ちなみにpushする際にgovendor fetch +out
のコマンドを実行していない場合、下記のようなエラーが出る。-----> Go app detected -----> Fetching jq... done -----> Fetching stdlib.sh.v8... done -----> Checking vendor/vendor.json file. !! The 'heroku.goVersion' field is not specified in 'vendor/vendor.json'. !! !! Defaulting to go1.12.12 !! !! For more details see: https://devcenter.heroku.com/articles/go-apps-with-govendor#build-configuration !! -----> New Go Version, clearing old cache -----> Installing go1.12.12 -----> Fetching go1.12.12.linux-amd64.tar.gz... done -----> Fetching govendor... done !! Installing package '.' (default) !! !! To install a different package spec set 'heroku.install' in 'vendor/vendor.json' !! !! For more details see: https://devcenter.heroku.com/articles/go-apps-with-govendor#build-configuration !! -----> Fetching any unsaved dependencies (govendor sync) -----> Running: go install -v -tags heroku . main.go:4:2: cannot find package "github.com/gin-gonic/gin" in any of: /tmp/tmp.hTplalCHiU/.go/src/Portfolio/vendor/github.com/gin-gonic/gin (vendor tree) /app/tmp/cache/go1.12.12/go/src/github.com/gin-gonic/gin (from $GOROOT) /tmp/tmp.hTplalCHiU/.go/src/github.com/gin-gonic/gin (from $GOPATH) ! Push rejected, failed to compile Go app. ! Push failedHerokuのGithubにサードパッケージをコミットできていないためビルドエラー起きてしまうとのことでした。
公式のやり方だけでは静的なアプリケーションをデプロイできてもサードパッケージを使用した際にできないため、少し工夫が必要でした。
GoPathの考え方に慣れていかないとですかね。アプリケーション名の変更
Heroku公式の通りに進めていくとアプリケーション名が適当な文字列で作成されてしまい
URLもそのままの文字列になってしまうため、変更したい場合はHerokuのマイページ→設定から変更しましょう。
その場合はgitで作成したリモートのパスを合わせて変更する事を忘れずに
- 投稿日:2019-11-24T11:17:29+09:00
GoでシンプルなHTTPサーバを自作する
Go アドベントカレンダーその 6 の 5 日目のエントリーです。
はじめに
HTTP サーバを自作してみよう!という試みです。もちろん実践的には net/http パッケージや Echo や Gin といったフレームワークを用いることが多いと思います。本稿では学習目的として net/http パッケージやフレームワークを使わずに、簡易的な HTTP サーバを実装することを試みます。車輪の再発明大好きです。
インクリメンタルに実装していきます。クライアントには curl を用いることにします。
HTTP サーバは何をするのか
HTTP サーバはシンプルにいうと以下のことを実施します。
- クライアントからの接続を待ち受ける
- クライアントから送信された HTTP リクエストをパースする
- HTTP リクエストに基づいて HTTP レスポンスを生成/返却する
クライアントからの接続を待ち受ける
HTTP は TCP/IP 上で動作するプロトコルです。まずはソケット通信を実装します。
main.gopackage main import ( "fmt" "net" "os" "github.com/pkg/errors" ) func main() { if err := run(); err != nil { fmt.Printf("%+v", err) } } func run() error { fmt.Println("start tcp listen...") // Listen ポートの生成をする listen, err := net.Listen("tcp", "localhost:12345") if err != nil { return errors.WithStack(err) } defer listen.Close() // コネクションを受け付ける conn, err := listen.Accept() if err != nil { return errors.WithStack(err) } defer conn.Close() fmt.Println(">>> start") buf := make([]byte, 1024) // Read メソッドの返り値が 0 byte ならすべて Read したとしておく for { n, err := conn.Read(buf) if n == 0 { break } if err != nil { return errors.WithStack(err) } fmt.Println(string(buf[:n])) } fmt.Println("<<< end") return nil }クライアントからcurlを送信curl -v http://localhost:12345
サーバー側出力start tcp listen... >>> start GET / HTTP/1.1 Host: localhost:12345 User-Agent: curl/7.55.1 Accept: */*ひとまずクライアントからの HTTP リクエストを読み込むことができたことが分かります。これはクライアントから
Ctrl + C
などで中断させないと処理が完了しませんが、ひとまず良いものとします。クライアントから送信された HTTP リクエストをパースする
HTTP のリクエストとレスポンスの構造
HTTP のリクエストとレスポンスは大きく次の 2 つから構成されます。
- ヘッダー
- リクエストボディ
ヘッダーとボディを区切るのは空行になります。クライアントからのリクエストを読み込んだときに、空行を読み込むまではヘッダー、それ以降がボディと判断することができます。
ヘッダーの取得
まずはヘッダーまでを取得することにします。
package main import ( "bufio" "fmt" "net" "os" "github.com/pkg/errors" ) func main() { if err := run(); err != nil { fmt.Printf("%+v", err) } } func run() error { fmt.Println("start tcp listen...") // Listen ポートの生成をする listen, err := net.Listen("tcp", "localhost:12345") if err != nil { return errors.WithStack(err) } defer listen.Close() // コネクションを受け付ける conn, err := listen.Accept() if err != nil { return errors.WithStack(err) } defer conn.Close() fmt.Println(">>> start") scanner := bufio.NewScanner(conn) // 一行ずつ処理する for scanner.Scan() { // つまりリクエストヘッダーを表示する // Text() からの返り値が空文字であれば空行と判断する if scanner.Text() == "" { break } fmt.Println(scanner.Text()) } // non-EOF error がある場合 if scanner.Err() != nil { return scanner.Err() } fmt.Println("<<< end") return nil }curl -v http://localhost:12345/ -X POST -d "Sample Message."start tcp listen... >>> start POST / HTTP/1.1 Host: localhost:12345 User-Agent: curl/7.55.1 Accept: */* Content-Length: 15 Content-Type: application/x-www-form-urlencoded <<< end想定どおり、リクエストのヘッダーを表示することができました。
ボディの取得
続いてメッセージのボディを取得します。リクエストボディの終端を判断は、リクエストヘッダーの
Content-Length
を使います。このヘッダーはリクエストボディのバイト数を表しています。Content-Length
のバイト数だけ文字を取得すればよいです。main.gopackage main import ( "bufio" "fmt" "io" "net" "strconv" "strings" "github.com/pkg/errors" ) func main() { if err := run(); err != nil { fmt.Printf("%+v", err) } } func run() error { fmt.Println("start tcp listen...") // Listen ポートの生成をする listen, err := net.Listen("tcp", "localhost:12345") if err != nil { return errors.WithStack(err) } defer listen.Close() // コネクションを受け付ける conn, err := listen.Accept() if err != nil { return errors.WithStack(err) } defer conn.Close() fmt.Println(">>> start") scanner := bufio.NewScanner(conn) var contentLength int // 一行ずつ処理する // リクエストヘッダー for scanner.Scan() { // Text() からの返り値が空文字であれば空行と判断する line := scanner.Text() if line == "" { break } if strings.HasPrefix(line, "Content-Length") { contentLength, err = strconv.Atoi(strings.TrimSpace(strings.Split(line, ":")[1])) if err != nil { return errors.WithStack(err) } } fmt.Println(line) } // non-EOF error がある場合 if scanner.Err() != nil { return scanner.Err() } // リクエストボディ buf := make([]byte, contentLength) _, err = io.ReadFull(conn, buf) if err != nil { return errors.WithStack(err) } fmt.Println("BODY:", string(buf)) // non-EOF error がある場合 if scanner.Err() != nil { return scanner.Err() } fmt.Println("<<< end") return nil }クライアントからcurlを送信curl -v http://localhost:12345/ -X POST -d "Sample Message."
サーバー側出力start tcp listen... >>> start POST / HTTP/1.1 Host: localhost:12345 User-Agent: curl/7.55.1 Accept: */* Content-Length: 14 Content-Type: application/x-www-form-urlencoded
Ctrl + C
で終了します。ここで問題なのが、実は上記の実装では、リクエストボディを読み込むことができません。具体的に言うと、
_, err = io.ReadFull(conn, buf)
でリクエストボディを読み込みたいのですが、Scanner がバッファリングですでにすべてのリクエストコンテンツを読んでしまっているため読み込むことができません。今回は Reader に
net/textproto
を用いることにします。net/textproto
は HTTP, NNTP, SMTPといったテキストベースのリクエスト/レスポンスプロトコルへの包括的なサポートを実装していて、自作 HTTP サーバの実装に役に立ちます。ということでいくつか修正すると以下のようになります。main.gopackage main import ( "bufio" "fmt" "io" "net" "net/textproto" "strconv" "strings" "github.com/pkg/errors" ) func main() { if err := run(); err != nil { fmt.Printf("%+v", err) } } func run() error { fmt.Println("start tcp listen...") // Listen ポートの生成をする listen, err := net.Listen("tcp", "localhost:12345") if err != nil { return errors.WithStack(err) } defer listen.Close() // コネクションを受け付ける conn, err := listen.Accept() if err != nil { return errors.WithStack(err) } defer conn.Close() fmt.Println(">>> start") reader := bufio.NewReader(conn) scanner := textproto.NewReader(reader) var contentLength int // 一行ずつ処理する // リクエストヘッダー for { line, err := scanner.ReadLine() if line == "" { break } if err != nil { return errors.WithStack(err) } if strings.HasPrefix(line, "Content-Length") { contentLength, err = strconv.Atoi(strings.TrimSpace(strings.Split(line, ":")[1])) if err != nil { return errors.WithStack(err) } } fmt.Println(line) } // リクエストボディ buf := make([]byte, contentLength) _, err = io.ReadFull(reader, buf) if err != nil { return errors.WithStack(err) } // in buf we will have the POST content fmt.Println("BODY:", string(buf)) fmt.Println("<<< end") return nil }クライアントからcurlを送信curl -v http://localhost:12345/ -X POST -d "Sample Message."
サーバー側出力start tcp listen... >>> start POST / HTTP/1.1 Host: localhost:12345 User-Agent: curl/7.55.1 Accept: */* Content-Length: 15 Content-Type: application/x-www-form-urlencoded BODY: Sample Message. <<< end想定どおりリクエストボディも処理することができました。
リファクタリング1
リクエストヘッダーの解析をキレイにしておきます。リクエストヘッダーの 1 行目はリクエストラインであって、 RFC7230 のとおり次の形式で定められるものでした。
A request-line begins with a method token, followed by a single space (SP), the request-target, another single space (SP), the protocol version, and ends with CRLF.
request-line = method SP request-target SP HTTP-version CRLF以下のようにして whiteSpace で split しておきます。
headerLine := strings.Fields(line)リクエストヘッダーの 2 行目以降から空行まではヘッダーフィールドでした。コロン(":")のあとの whitespace は任意ですが、今回はあるものとします。そうするとヘッダーフィールドの解析は以下のようになります。
headerFields := strings.SplitN(line, ": ", 2)ということで軽微なリファクタリングを加えました。
main.gopackage main import ( "bufio" "fmt" "io" "net" "net/textproto" "strconv" "strings" "github.com/pkg/errors" ) func main() { if err := run(); err != nil { fmt.Printf("%+v", err) } } func run() error { fmt.Println("start tcp listen...") // Listen ポートの生成をする listen, err := net.Listen("tcp", "localhost:12345") if err != nil { return errors.WithStack(err) } defer listen.Close() // コネクションを受け付ける conn, err := listen.Accept() if err != nil { return errors.WithStack(err) } defer conn.Close() fmt.Println(">>> start") reader := bufio.NewReader(conn) scanner := textproto.NewReader(reader) // 一行ずつ処理する // リクエストヘッダー var method, path string header := make(map[string]string) isFirst := true for { line, err := scanner.ReadLine() if line == "" { break } if err != nil { return errors.WithStack(err) } // Request Line if isFirst { isFirst = false headerLine := strings.Fields(line) header["Method"] = headerLine[0] header["Path"] = headerLine[1] fmt.Println(method, path) continue } // Header Fields headerFields := strings.SplitN(line, ": ", 2) fmt.Printf("%s: %s\n", headerFields[0], headerFields[1]) header[headerFields[0]] = headerFields[1] } // リクエストボディ method, ok := header["Method"] if !ok { return errors.New("no method found") } if method == "POST" || method == "PUT" { len, err := strconv.Atoi(header["Content-Length"]) if err != nil { return errors.WithStack(err) } buf := make([]byte, len) _, err = io.ReadFull(reader, buf) if err != nil { return errors.WithStack(err) } fmt.Println("BODY:", string(buf)) } // completed fmt.Println("<<< end") return nil }HTTP リクエストに基づいて HTTP レスポンスを生成/返却する
ステータスラインのみを返す
HTTP サーバからレスポンスを返却できるようにします。RFC 7230 のとおり、1 行目はステータスラインを返すことになっていて、以下の形式で定められています。
The first line of a response message is the status-line, consisting of the protocol version, a space (SP), the status code, another space, a possibly empty textual phrase describing the status code, and ending with CRLF.
status-line = HTTP-version SP status-code SP reason-phrase CRLF以下の実装を追加します。
// レスポンス io.WriteString(conn, "HTTP/1.1 200 OK\r\n")クライアントからcurlを送信$ curl -i http://localhost:12345/ HTTP/1.1 200 OKクライアントからのリクエストに対して、ステータスコードを返すことができるようになりました。
ヘッダーとボディも返す
続いて、レスポンスのヘッダーとボディを生成します。非常に簡単なレスポンスを返却します。
io.WriteString(conn, "Content-Type: text/html\r\n") io.WriteString(conn, "\r\n") io.WriteString(conn, "<h1>Hello World!!</h1>")クライアントからcurlを送信$ curl -i http://localhost:12345/ HTTP/1.1 200 OK Content-Type: text/html <h1>Hello World!!</h1>HTML をレスポンスとして受け取りました。ブラウザでも表示させてみます。
ブラウザからアクセスすることができました。
追加機能の実装
リクエストを受け取って、レスポンスを返すことができるようになりました。続いていくつかの機能を実装していきます。
- チャンク
- マルチバイト対応
- GET メソッドが来たらパスで指定されたファイルを返すようにする
- ファイルが存在しない場合は 404 を返すようにする
- 複数リクエストに対応する
チャンク
チャンクの仕様は Chunked Transfer Coding です。
チャンク形式転送エンコーディングとは、送信したいデータを任意のサイズのチャンクに分割し、各々のチャンクにサイズ情報を付与するエンコード方式です。
Content-Length
ではあらかじめ送信するバイト数を明記していましたが、チャンクの場合は、チャンクそれぞれのバイト数を 16 進数で明記して、チャンクサイズに 0 のときに終了になります。チャンクエンコーディングを扱う場合はヘッダーに "Transfer-Encoding: chunked" を指定します。Go で実装する前に、どのような挙動なのか確認してみます。適当に 100 KB のファイルを作成、サーバにアップロードする挙動を Netcat で表示させてみます。
$ dd if=/dev/zero of=100KB.txt bs=1K count=100$ nc -l 8888$ curl -T 100KB.txt -H "Transfer-Encoding: chunked" http://localhost:8888Netcatの出力$ nc -l 8888 PUT /100KB.txt HTTP/1.1 Host: localhost:8888 User-Agent: curl/7.55.1 Accept: */* Transfer-Encoding: chunked Expect: 100-continue 3ff4 3ff4 3ff4 3ff4 3ff4 3ff4 1048 016 進数の 3ff4 を 10 進数で表示すると 16372 Byte で 1048 が 4168 Byte ですから、16372 * 6 + 4168 = 102400 Byte = 100 KB になります。たしかにチャンクに分割して送信できていることが分かります。
// TODO: ちゃんと 16 進数のバイト数分の Read して処理する
transferEncoding, ok := header["Transfer-Encoding"] if !ok { return errors.New("no match operation") } if transferEncoding == "chunked" { for { line, err := scanner.ReadLine() if line == "0" { break } if err != nil { return errors.WithStack(err) } fmt.Println(line) } }マルチバイトに対応させる
Go では文字列は単なるバイトの slice でした。なので Linux 環境から以下のように curl したバイト数 Read して表示させればマルチバイトを扱えます。Windows で curl する場合はデフォルトで SJIS なので
chcp 65001
などで UTF-8 表示モードに変更し、マルチバイト文字を Unicode エンコーディングしておく必要があり、ちょっとだけ面倒です。$ curl -X POST -v http://localhost:12345 -d "サンプルメッセージ"サーバー側出力start tcp listen... >>> start Host: localhost:12345 User-Agent: curl/7.58.0 Accept: */* Content-Length: 27 Content-Type: application/x-www-form-urlencoded BODY: サンプルメッセージ <<< endGET メソッドが来たらパスで指定されたファイルを返すようにする
リクエストヘッダーのパスからローカルのファイルを参照して HTML を返却するようにします。Go での実装例は以下です。ファイルパスの扱いには
"path/filepath"
パッケージを使うとクロスプラットフォームに対応できてスマートです。var resp []byte if method == "GET" { path, ok := header["Path"] if !ok { return errors.New("no path found") } cwd, err := os.Getwd() if err != nil { return errors.WithStack(err) } p := filepath.Join(cwd, filepath.Clean(path)) if err != nil { return errors.WithStack(err) } resp, err = ioutil.ReadFile(p) }以下のような HTML を用意しておきます。
sample.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Simple HTTP Server</title> </head> <body> <h1>Hello Simple HTTP Server</h1> </body> </html>$ curl http://localhost:12345/sample.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Simple HTTP Server</title> </head> <body> <h1>Hello Simple HTTP Server</h1> </body> </html>start tcp listen... >>> start GET /sample.html Host: localhost:12345 User-Agent: curl/7.55.1 Accept: */* <<< endファイルが存在しない場合は 404 を返すようにする
"path/filepath"
を用いてファイルパスは取得できるようになったので、ファイルの有無を確認する実装が必要です。func run() error { // ... if !fileExists(p) { io.WriteString(conn, "HTTP/1.1 404 Not Found\r\n") io.WriteString(conn, "Content-Type: text/html\r\n") io.WriteString(conn, "\r\n") io.WriteString(conn, string("<h1>Error 404</h1>")) } else { resp, err := ioutil.ReadFile(p) if err != nil { return errors.WithStack(err) } io.WriteString(conn, "HTTP/1.1 200 OK\r\n") io.WriteString(conn, "Content-Type: text/html\r\n") io.WriteString(conn, "\r\n") io.WriteString(conn, string(resp)) } // ... } func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() }ブラウザから確認してみます。たしかに存在しないファイル名やディレクトリの場合には 404 がクライアントに返却できていることが分かります。
リファクタリング2
たいぶごちゃごちゃしてきたので、ファイルに分割してリファクタリングします。
こんな感じのディレクトリ構造にしました。
/ │ index.html │ main.go │ request.go │ response.go │ server.go │ utils.goそれぞれのファイルは https://github.com/d-tsuji/simple-http-server にコミットしておきました。
複数のリクエストに同時に対応する
最後に、複数のリクエストを同時に扱えるようにします。もともとの実装では、サーバが処理している間は他のクライアントはコネクションを確立することができませんでした。これは困るので、複数のクライアントから同時にリクエストが来た場合にレスポンスを返せるように修正します。
これは
listen.Accept()
したあとのサーバの処理を goroutine を用いて非同期で行うことで実現できます。エラーが返ってきた場合は Internal Server Error としておきましょう。func Run() error { // ... go func(conn net.Conn) { defer conn.Close() // エラーが発生した場合は Status Code 500 としてクライアントに返却する if err := service(conn); err != nil { fmt.Printf("%+v", err) InternalServerError(conn) } }(conn) // ... } func service(conn net.Conn) error { fmt.Println(">>> start") reader := bufio.NewReader(conn) scanner := textproto.NewReader(reader) // 一行ずつ処理する // リクエストヘッダー req, err := NewHttpRequest(scanner) if err != nil { return errors.WithStack(err) } // リクエストボディ switch req.headers["Method"] { case "GET": path, ok := req.headers["Path"] if !ok { return errors.New("no path found") } cwd, err := os.Getwd() if err != nil { return errors.WithStack(err) } p := filepath.Join(cwd, filepath.Clean(path)) // file not found if !fileExists(p) { NotFoundError(conn) } else { data, err := ioutil.ReadFile(p) if err != nil { return errors.WithStack(err) } GetOk(conn, data) } case "POST", "PUT": if err := req.GetRequestBody(reader, scanner); err != nil { return errors.WithStack(err) } PostOK(conn) return nil default: return errors.New("no match method") } // completed fmt.Println("<<< end") return nil }まとめ
シンプルな HTTP サーバを実装しました。一度は自作 HTTP サーバを作ってみたいと思っていたので、Go で実現できてよかったです。必然的に RFC も読むことになり、HTTP プロトコルの勉強にもなっておすすめです。
参考
- 投稿日:2019-11-24T02:18:42+09:00
unsupported Scan
こんなエラーが出たら
sql: Scan error on column index x, name "updated_at": unsupported Scan, storing driver.Value type <nil> into type *time.Timenull パッケージを使う
import ( "gopkg.in/guregu/null.v3" "time" ) type Hoge struct { Id int64 Hoge string CreatedAt time.Time UpdatedAt null.Time DeletedAt null.Time }代入は、キャストして
Hoge.UpdatedAt = null.TimeFrom(time.Now())