最近よく書く HTTP サーバ基礎部分

この記事は Go3 Advent Calendar 2019 13日目の記事です qiita.com

TL;DR

  • x/sync/errgroup はいいぞ

本編

最近よく書くサーバの起動部分のコードを紹介します
分かる人には見るのが一番早いと思うので、早速コード全体を載せます

やりたいことは

  • localhost:8888 で HTTP サーバを起動
  • SIGINT を受けると HTTP サーバを graceful shutdown
  • 各所での ctx.Done() のハンドリング

これらを混乱しないように記述したかったのが最初のモチベーションです

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "x/sync/errgroup"
)

func main() {
    os.Exit(run(context.Background()))
}

func run(ctx context.Context) int {
    var eg *errgroup.Group
    eg, ctx = errgroup.WithContext(ctx)

    eg.Go(func() error {
        return runServer(ctx)
    })
    eg.Go(func() error {
        return signal(ctx)
    })
    eg.Go(func() error {
        <-ctx.Done()
        return ctx.Err()
    })

    if err := eg.Wait(); err != nil {
        fmt.Println(err)
        return 1
    }
    return 0
}

func signal(ctx context.Context) error {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt)

    select {
    case <-ctx.Done():
        signal.Reset()
        return nil
    case sig := <-sigCh:
        return fmt.Errorf("signal received: %v", sig.String())
    }
}

func runServer(ctx context.Context) error {
    s := &http.Server{
        Addr:    ":8888",
        Handler: nil,
    }

    errCh := make(chan error)
    go func() {
        defer close(errCh)
        if err := s.ListenAndServe(":8888", nil); err != nil {
            errCh <- err
        }
    }()

    select {
    case <-ctx.Done():
        return s.Shutdown()
    case err := <-errCh:
        return err
    }
}

解説

x/sync/errgroup

godoc.org

単純に一つ HTTP サーバを立てるだけであれば必要ありませんが、今回のように HTTP サーバも起動したいし、シグナルハンドリングもしたい、なんなら他のサーバも起動したい、かつそれらの error 処理も纏めてやりたいというような要件を達成するために x/sync/errgroup は非常に便利なパッケージです
指定した goroutine をグループ化し、そのエラー処理を取り纏めてくれます

以下のように使います

  • errgroup.Group 型の値(0値が使用可能)を用意する
  • errgroup.Group.Go() で error を返す関数を goroutine として起動する
  • errgroup.Group.Wait()errgroup.Group.Go() で起動した goroutine の全てが終了するまで待機し、一番最初に返ってきた non-nil error を返す

一番最初に返ってきた non-nil error を強調しましたが、どうしても起動した goroutine の全ての error を補足したいときは x/sync/errgroup は使用できないことに注意してください
// この場合は sync.WaitGroup などで自力で頑張りましょう

加えて errgroup.WithContext() で初期化をしておくと、最初に non-nil error が返ってきた瞬間に context がキャンセルされます
今回のコードでは run() がこの処理をしている部分です

  • runServer() が error を返す
  • signal() が error を返す

のどちらかが起きると、ctx のキャンセル処理により他方も終了処理が出来る、という作りです

func run(ctx context.Context) int {
    var eg *errgroup.Group
    eg, ctx = errgroup.WithContext(ctx)

    eg.Go(func() error {
        return runServer(ctx)
    })
    eg.Go(func() error {
        return signal(ctx)
    })
    eg.Go(func() error {
        <-ctx.Done()
        return ctx.Err()
    })

    if err := eg.Wait(); err != nil {
        fmt.Println(err)
        return 1
    }
    return 0
}

runServer()/signal()

次は errgroup.Group.Go() で呼び出される側の関数です
単純に http.ListenAndServe() を実行するだけでもよいですが、ctx のキャンセル待受処理を両立するために http.ListenAndServe() の error を chan 経由でやりとりします

func runServer(ctx context.Context) error {
    // サーバ初期化
    s := &http.Server{Addr: ":8888", Handler: nil}
    // error 伝達用 chan
    errCh := make(chan error)
    go func() {
        defer close(errCh)
        if err := s.ListenAndServe(":8888", nil); err != nil {
            // error を chan 経由で伝える
            errCh <- err
        }
    }()
    select {
    case <-ctx.Done(): // 上位で ctx のキャンセルされた場合は graceful shutdown
        return s.Shutdown()
    case err := <-errCh: // `s.ListenAndServe()` が error を返した場合、そのまま error を返す
        return err
    }
}

シグナル待受処理も同様です シグナルを受ける chanctx.Done()select 文で待ち受けるように書きます

2019/12/13 追記: 取りこぼす可能性があるので、chan にバッファを付けた

func signal(ctx context.Context) error {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt)

    select {
    case <-ctx.Done():
        signal.Reset()
        return nil
    case sig := <-sigCh:
        return fmt.Errorf("signal received: %v", sig.String())
    }
}

やってみて

シグナルハンドリングも x/sync/errgroup にのせることで、制御フローをシンプルにできてるのではないかなぁと感じています

私が実際に導入した要件では、HTTP サーバ、gRPC サーバが一つのコードで動いている中、追加で別ポートに gRPC サーバを立てる必要が出てきました
その際もコードの拡張は eg.Go() 部分と runServer() 部分の追加でよく、gRPC サーバ自身の後処理も runServer() で完結できているので苦労なく追加することができました

x/sync/errgroup 便利

mbsync/imapnotify で(ある程度)快適なメール受信環境を手に入れる

この記事は dotfiles Advent Calendar 2019 2日目の記事です qiita.com

みなさん、電子メールと呼ばれるものをご存知でしょうか
現代を生きておられる皆様におかれましては想像もつかないでしょうが、太古の昔平成という時代では、人間同士のコミュニケーションに電子メールと呼ばれるものが使われていたそうです
この電子メールというロストテクノロジーを現代に蘇えらせるため、筆を取らせていただきました

はい、というわけで CLI のメール環境の話をします
なお以下の内容とも被ってますが、その詳細版ということで...

tennashi.hatenablog.com

メールシステムの責務は多く、手元部分のみに着目しても以下のような処理が必要です

  • メールを受信する
    • メールの受信をトリガとして他の処理をする
  • メールを読む
    • メールの検索を早くするためにインデックスを張る
  • メールを作成する
  • メールを送信する

これらの要素を満たすように色々なツール群を組み合わせてメール環境を構築する必要があります
ここではメールの受信部分に注目して、どのようなツールで処理を実現するのかを見ていきます
// 本当は全部書くつもりでしたが、予想以上に長くなってしまって疲れたので...

参考までに私がどのツールを使用しているかを書いておきます

  • メール受信: isync(mbsync)/imapnotify/systemd
    • systemd は imapnotify の起動と timer で 5m 毎に mbsync を実行するため
  • メールを読む: neomutt*1
  • メールを作成する: vim*2
  • メールを送信する: msmtp*3

メールの受信

IMAP/POP クライアントが必要です
私は POP は使用していませんので IMAP に限定します

私は isync(mbsync)*4 を使ってメールを IMAP サーバから手元にダウンロードしています

IMAP はネットワーク越しにメールを読むためのプロトコルですが、サーバへの到達性が無いとメールが読めないことや一つのメールを読むにも通信が必要なため(ネットワーク環境にもよりますが)それなりに遅いという問題点があります
isync(mbsync) はこれを解消するために、IMAP サーバからメールをダウンロードして、ローカルに保存しておけばよい、という発想のツールです
類似のツールとしては offlineimap*5 もありますが、2016年に試した限りでは速度面で mbsync が圧倒していたため、そちらに移行しました
現在でも開発は続いているようなので、2019年12月現在でも同様かは不明です

ローカルにメールを保存すると簡単に書きましたが、メールの保存形式にもいくつか標準があり、大きく以下の二つに分かれます

  • mbox 形式*6
  • Maildir 形式*7

ここでは詳細に触れませんが、特徴の一つとして mbox 形式は全てのメールを一つのファイルに保存し、Maildir 形式では 1 メール 1ファイルとして保存することが挙げられます
mbsync は Maildir 形式で保存するためのツールです

設定

では設定を見ていきます

  • ~/.mbsync.rc
IMAPAccount gmail
Host imap.gmail.com
User hogehoge@example.com
Pass awesomepassword
SSLType IMAPS
AuthMechs LOGIN

IMAPStore gmail-remote
Account gmail

MaildirStore gmail-local
Path ~/.mail/gmail
Inbox ~/.mail/gmail/Inbox

Channel gmail
master :gmail-remote:
Slave :gmail-local:
Patterns * ![Gmail]* "[Gmail/Sent Mail]" "[Gmail]/Starred" "[Gmail]/All mail"
Create Both
SyncState *

設定の詳細には触れませんが、大まかには以下のようなことが設定されています

  • Gmail(IMAP サーバ) のログイン情報を宣言
  • リモート側(IMAP サーバ) として Gmail アカウントを指定
  • ローカル側(Maildir) として ~/.mail/gmail を指定(ここにメールが保存される)
  • 同期の挙動を設定する

詳細は https://wiki.archlinux.jp/index.php/Isynchttps://uwabami.github.io/cc-env/DebianMail.html を参考にされるとよいかと思います

メールの受信をトリガとして他の処理をする

先の mbsync は非常に便利ですが、コマンド自体は常駐するわけではなく、タイマー機能などは持っていないため、cron や systemd timer などを用いて定期的に mbsync を実行する必要があります
私はそれに加え、IMAP の NOTIFY 拡張を利用して、IMAP サーバにメールが届いたことをトリガにして mbsync を叩けるようにしています
これを実現するために imapnotify*8 を使用します

設定

  • ~/.config/imapnotify/config.json
{
  "host": "imap.gmail.com",
  "port": 993,
  "tls": true,
  "tlsOptions": { "rejectUnauthorized": false },
  "username": "hogehoge",
  "password": "awesomepassword",
  "onNotify": "/usr/bin/mbsync -a",
  "boxes":
    [
      "Inbox",
    ]
}
  • IMAP サーバの接続情報
  • Inbox のみを監視
  • Inbox にメールが配送されると onNotify に設定したコマンドを実行する
    • ここでは mbsync -a で mbsync に設定した全てのアカウントのメールを受信する

ほかにも

メールの受信時にやりたいことはいくつもありますが、例えば以下のようなことは今回は触れていません

今後ちょっとずつ整備していくのでまた知見が溜ったら公開します
neomutt 周り、気力があったら空いているところに入って書くかなぁ...

吉祥寺.pm #20 で LT してきた

してきた

資料

kichijojipm_20.md · GitHub

吉祥寺.pm

kichijojipm.connpass.com .pm と言いつつ任意の話題が提供される楽しい会
もちろん Perl の話題もある
句会: poem meeting でもあるという説がある[要出典]

LT した経緯

kichijojipm.connpass.com

前回吉祥寺.pm #19 で初参加し、あまりにごった煮な話題の幅広さに驚愕しました
その中でも

19:50〜20:05(15分) Talk2:TBD(Yoshitaka Kawashima)

この枠は kawasima さんによる Clojure の話題でした
トークでは Emacs というプレゼンツールが利用されていました
// 余談ですが Emacs というのは主にテキストエディタとして知られているようです

テキストエディタたるものプレゼンも出来るというのは当然のことですが、当時の tweet を見ると Emacs でプレゼンをするなんて、という言及が多く見られたように思います

ところで話は変わりますが、ここに vim というテキストエディタがあります
先に述べた通り、テキストエディタたるものプレゼンをできなければなりません

それを実証するために今回の LT を申し込みました

資料の読み方

基本的には Markdown ファイルなので、Gist 上でそのまま読めるはずです
プレゼン形式で表示するためには以下のプラグインを利用します github.com

今回のプレゼン資料を上記プラグインをインストールした gVim で開き、:ShowtimeStart と入力するとプレゼン形式で表示されます
表示時のフォントは Cica を指定しているため、フォントが無くてデフォルトのフォントで表示されてしまい辛い、という方は

<!--! {
  'font': 'Cica 50',
  'colorscheme': 'default',
}
-->

この部分の Cica という記述を自身の表示したいフォントに書き換えてください

感想など

と、いうわけで...
いやー、画面表示のトラブルなどご迷惑をおかけしました...
最後のネタバレまで到達できなかったのでネタバレ用に記事を書くはめになるという非常にダサい結果に...

Vim でプレゼンする、というのが当初の目的だったので、内容何にするかギリギリまで悩んでました
あのプレゼンが vim だったことに当日気付かれた方はどれだけいたでしょうか...??

また機会があれば登壇できたらな、と思います

VimConf 2019 に行ってきた話

VimConf 2019

vimconf.org

VimConf は、世界初かつ世界で唯一のコミュニティによって定期運営されているVimの国際カンファレンスです。

Vim の国際カンファレンス
何を言っているかさっぱりわかんねーと思うが(ry
開催は 11/03 今日は 11/23、20日ごしの感想記事です

各セッションの概要は色々な人がブログに纏めてくださっているし、トーク自体も YouTube で公開されてます
https://vimconf.org/2019/blogs/ www.youtube.com

感想など...

個人的に一番よかったセッションは m-nishi さんの "make test" でした
https://vimconf.org/2019/slides/m-nishi.pdf www.youtube.com

そもそも vim のテストが Vim script で記述されているとは知らなかったので学びがあった、という面もありますが、実際自身がテストを追加されたときの流れも丁寧に説明されており、私もコントリビュートしたいな/できる部分がありそうだな、という思いを持つことができました

やっていきの機運

キーノートの一つ目 Prabir Shrestha さんの "Vim Renaissance" では vim-lsp をどうやって作ってきたか、という話を聞けましたし、以下のセッションでも自身が何を作ってきたのか、何にコントリビュートしてきたのかと言った話題を聞き、やっていきをしたい意欲の高まりを感じました

  • IK さん: "Grown up from Vim User to Vim plugin developer side"
  • gorilla0513 さん: "My Vim life"
  • Shougo さん: "My dark plugins development history ~ over 10 years ~"

Neovim と vim

Vim に :terminal が入った理由は私には分からないですが、はたから見ている限りでは neovim に terminal 機能が入り、それがユーザに使われていることが確かめられたことも要因の一つとしてあるのではないかなぁと思ったりします
terminal の例のように vim と neovim は互いに影響を与え合って(ように見えて)おり neovim のこれからの機能、というのは vim にどういう機能が入っていくかを予想するにも意味があると思っています
キーノート二つ目 Justin M. Keyes さんの "We can have nice things" では neovim のこれまでとこれからの話を聞くことができました
私はまだどういうものか理解できていませんが、decoupled UI などこれからも面白そうな機能が続々と計画されているようで、使ってみたい気持ちが強くあります
// つい最近も LSP client 機能が merge されてましたね

how to be more productive with Vim?

今回 VimConf のテーマである “how to be more productive with Vim?” を実践できるような話題も聞くことができました 特に daisuzu さんの解説された tag 機能はこれまであまり使ってませんでしたが、セッションを聞きながらちょっと試しただけで生産性の高まりを感じれるほど便利な機能だったので、これからも積極的に使用していきたい所存です

  • mopp さん: "Your Vim is Only for You"
  • daisuzu さん: "Usage and manipulation of the tag stack"
  • Danish Prakash さん: "Using Vim at Work!"
  • Hezby Muhammad さん: "Let's Play with Vanilla Vim"
  • Tatsuhiro Ujihisa さん: "13 Vim plugins I use every day"

懇親会

懇親会 LT 飛び込んできました
特に資料はありませんでしたが、vim でメールが読みたかったのでそのためのプラグインを作っている話でした
// とりあえず Advent Calendar までには完成させたいものである...

その後...

会場1F の Hub でお酒を飲んでいました
ujihisa さんや mattn さん、スピーカの方々 Rubist の方々などなどとお話しすることができ、とても良い体験でした
コントリビュートの方法として OSS 作者としては Issue より Pull Request の方がいい、と言っていたのが印象的でした
// この間 mattn/efm-langserver を go get で取得しようとしたら依存解決でエラーが出たので Issue を上げようと思ったときにこの話題を思い出して人生初 OSS への Pull Request を達成しました
// PR 提出から merge までの時間より PR の本文の英語考えてる時間の方が長かった... github.com

golang.tokyo #25 参加レポ

golang.tokyo #25 に参加してきました
補欠だったんですが、ブログ枠が空いてたので急遽そっちで参加することに

でいい機会なのでブログをはじめてみることにしました

というわけで本題

今改めて読みなおしたい Go 基礎情報 その1

docs.google.com

概要

Go の郷に入るというテーマで "言語思想" と "Go Way な設計実装" が分かるような記事/書籍を紹介する話
それらの記事/書籍から Go の Simplicity がどのように実現されているかを学んでいく

感想など

私自身もなんとなくでしか Go らしさを理解していなかったので、紹介された記事を改めてよく読も
丁度、Go の研修用資料も書いてたので個人的にとてもタイムリーな発表でした

今改めて読みなおしたい Go 基礎情報 その2

概要

Go の郷に入る Part 2
こちらはより実装よりで、"言語仕様" と "内部処理" についての記事/書籍を紹介

質問

Q.util とか common とかはよくダメなネーミングだと言うが、標準パッケージにも ioutil とか httputil とかあるの、あれはよいのか??

A.ああいうのは io とか http とかコンテキストが制限されているからよさそう

Q.どもったような名前はダメだと言われるが、context.Context とかは許されるのはなんで??

A.context パッケージは Context 型しかないから仕方無いのでは

感想など

const では float64 + int みたいな計算が許されることとか知らないことが多かった
内部実装とかはパフォーマンスチューニングしたいときとかに役立つはずなので、少しづつ読み進めよ

golang binary hacks

golang binary hacks (golang.tokyo #25)

概要

Hugo Extended の docker image を Alpine Linux で作ったら Segmentation fault で動かなかったので、バイナリにパッチをあてて直した話
musl がスレッドに対して割り当てるスタックサイズが小さかったのが原因で、それは ELF のヘッダをいじることで変更できた github.com

感想など

debug/elf パッケージ知らなかった、めっちゃ便利そう
現行の alpine:edge image だとスレッドのスタックサイズが大きくなっているのでこの問題の発生頻度が低くなっているらしい

Consider a test of image processing in Go

概要

画像処理のテスト手法を色々なパッケージから調べてきて紹介していく話
計算コストを抑えるとテスト失敗時の詳細が失なわれるという傾向があるので、その辺りも考慮する必要がある また、テストデータを準備するのも難しく、goldenfile 方式で用意するのがよさそう

感想など

画像となると情報量が膨大になるのでやっぱりテストするのもテストデータを用意するのも大変そう
会の後、統計的な手法でテストしてるパッケージの例などがあったか聞いてみた
-> そういう手法でやってるパッケージ自体は無かったらしい

package名と変数名がかぶっているのをとにかく検出したい

概要

パッケージ名と変数名とかが被っているミスをよくやるので静的解析して検出できるようにした話 x/tools/analysis 便利 あとテスト作るのがとても簡単

github.com

感想など

静的解析、ネタはあって手が空いたらやりたかったので参考にしたい

github.com

も便利そうなので使ってみよ

おわり

こうやって纏めると資料を読み返すことになるのでやっぱりよいですね
今後もブログ枠空いてたら積極的に取っていこうと思います