20191124のGoに関する記事は11件です。

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 に記載されている手法なのでとりあえずこうしておくのが良さそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go使ってAWS Lambdaでラムダ計算した

tl; dr

ソースコードはこちら
エンドポイントは
https://nd88j25ztg.execute-api.ap-northeast-1.amazonaws.com/dev/lambda
でヘッダにContent-type: application/jsonX-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.go
package 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.go
func 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.go
func 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言語のような原初の風景も感じますが、エディタの補完が強力なので気持ちよく書けます。今度は非同期処理とかもやってみたいですね。

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

[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.txt
1.こんにちは。ゆっくり実況始めるよーーー

2, 設定ファイルに音声ファイルや、画像サイズ、キャラクター画像を設定する
3, go run main.goで実行する

出力結果

0_こんにちは.png
+音声ファイル

得られた知見

・他オープンソースツール公開者への感謝
・modの使い方
・基本的な画像処理
・viperの使い方
・tomlの扱い方

その他

一応参考までにgit hubにあげておきますが、ソースコードを信用してはいけません。個人ツールです。
https://github.com/hiromichi-n/text2talk

そもそも素材収録してないので、結局何がしたかったのか忘れたorz。
次は使い始めてからのバグ修正編を書きたい(願望)

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

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
}

ここでの問題はvalueinterface{}として扱う必要があり、型による保証を受けられず型安全ではありません

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を構成する元の型、IntStringは以下のように定義します。
この定義により、それぞれ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インターフェイスの実装

IntString共に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.Intnilの場合ランタイムエラーが起きるので、以下のような実装のほうがより安全でしょう。

type Int int

func (i Int) Match(c Cases) {
    if c.Int != nil {
        c.Int(i)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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がなんだか遅いな、っていう人は試してみるといいんじゃないでしょうか。

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

テンプレートのパイプライン

パイプラインとは

テンプレート上で引数や関数、メソッドを直列につなげたもの

下記のように書くことで 12.3456printf "%.2f" の引数に送れる

{{ 12.3456 | printf "%.2f" }}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 変更なし 変更なし 変更なし

参考リンク

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

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型をコピーしただけでは、長さとスライスのポインタアドレスがコピーされるだけのようでした。メモリ全コピーのような高コストなことはなさそうでした。

ポインタ気軽に使えすぎてぬるぽへの恐怖が薄れてきた

ちょっと郷に従いすぎてしまったんでしょうか。

  • ドット演算子が有能すぎてポインタであるかどうかを意識しない。
  • あまりにも気軽にポインタ型を作れて返却して引き回してしまう。
  • ポインタの所有権や寿命に関して意識することがない。
  • レシーバーも大抵はポインタで書いてしまうことが多いし。
  • 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の正規表現なんとかならないの。

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

サードパッケージを使用したGoのアプリケーションをHerokuにデプロイする

Portfolioを作るに当たってGoでHerokuにデプロイしようとしたらハマった
結論からいうとパッケージのコミット漏れ

Herokuで公式HPにデプロイ方法は載っていたが、サードパッケージを使用した際はハマるので今回の事象を押さえておく

Goでアプリケーションを作成

今回はginでプロジェクトを作成する
Heroku用のフォルダを作成し、以下のファイルを配置

main.go
package 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/govendor

govendorについては下記の記事がとても理解できたので参考にさせて頂きました。
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.json

Herokuのアプリを作成

$ 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 master

Goのアプリケーションのデプロイ完了

ちなみに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 failed

HerokuのGithubにサードパッケージをコミットできていないためビルドエラー起きてしまうとのことでした。
公式のやり方だけでは静的なアプリケーションをデプロイできてもサードパッケージを使用した際にできないため、少し工夫が必要でした。
GoPathの考え方に慣れていかないとですかね。

アプリケーション名の変更

Heroku公式の通りに進めていくとアプリケーション名が適当な文字列で作成されてしまい
URLもそのままの文字列になってしまうため、変更したい場合はHerokuのマイページ→設定から変更しましょう。
その場合はgitで作成したリモートのパスを合わせて変更する事を忘れずに

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

GoでシンプルなHTTPサーバを自作する

Go アドベントカレンダーその 6 の 5 日目のエントリーです。

はじめに

HTTP サーバを自作してみよう!という試みです。もちろん実践的には net/http パッケージや Echo や Gin といったフレームワークを用いることが多いと思います。本稿では学習目的として net/http パッケージやフレームワークを使わずに、簡易的な HTTP サーバを実装することを試みます。車輪の再発明大好きです。

インクリメンタルに実装していきます。クライアントには curl を用いることにします。

HTTP サーバは何をするのか

HTTP サーバはシンプルにいうと以下のことを実施します。

  • クライアントからの接続を待ち受ける
  • クライアントから送信された HTTP リクエストをパースする
  • HTTP リクエストに基づいて HTTP レスポンスを生成/返却する

クライアントからの接続を待ち受ける

HTTP は TCP/IP 上で動作するプロトコルです。まずはソケット通信を実装します。

main.go
package 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.go
package 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.go
package 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.go
package 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 をレスポンスとして受け取りました。ブラウザでも表示させてみます。

image.png

ブラウザからアクセスすることができました。

追加機能の実装

リクエストを受け取って、レスポンスを返すことができるようになりました。続いていくつかの機能を実装していきます。

  • チャンク
  • マルチバイト対応
  • 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:8888
Netcatの出力
$ 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

0

16 進数の 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: サンプルメッセージ
<<< end

GET メソッドが来たらパスで指定されたファイルを返すようにする

リクエストヘッダーのパスからローカルのファイルを参照して 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 がクライアントに返却できていることが分かります。

image.png
image.png

リファクタリング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 プロトコルの勉強にもなっておすすめです。

参考

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

unsupported Scan

こんなエラーが出たら

sql: Scan error on column index x, name "updated_at": unsupported Scan, storing driver.Value type <nil> into type *time.Time

null パッケージを使う

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