「Go言語によるWebアプリケーション開発」の感想など
O'Reilly Japan - Go言語によるWebアプリケーション開発をのんびりと写経してきて、一通り終わりました。
全ての章のものではないですが、写経と読書中にメモした感想などを備忘録として残しておきたいと思います。
全体の感想
「Go言語によるWebアプリケーション開発」というタイトルですが、
本書に書かれているプログラムはWebアプリケーションとコンソールアプリケーションの両方になります。
どちらもGo言語での開発に即、実践可能なノウハウが書かれているので
「Webアプリケーション開発」に限らず、Go言語でのあらゆるアプリケーション開発に従事する人におススメできるかと思います。
(例え小さなツールを作るだけの場合などでも。)
また、ある程度Go言語が分かった状態で始めた方が良さそうだとも感じました。
goroutineやスライスなど、Go言語での開発でよく使う機能が取り上げられていますが、これらを知らないで本書を読むと、唐突に出来てたような印象を受けそうだと思います。
各章の「まとめ」には、書かれている内容が「まと」められているので、後日必要になった時に読み返す箇所を探すのに役立ちそうです。
3章
- インターフェースを使った実装の切り替え
- 型の埋め込み(type embedding)とインターフェース
について取り扱われており、大変参考になりました。
インターフェースの定義と具象の実装と切り替え、これに型の埋め込みを組み合わせて
どのように使うかの具体例が示されてていることが参考になります。
4章
一転してコンソールアプリで、小さなアプリを組み合わせ、それぞれのアウトプットを標準出力で繋ぐことで目的を達成している具体例です。
小さなアプリの組み合わせているところは、「UNIXという考え方」で書かれていた考えに近い感じがします。
この章も、ツールやバッチを作る時などにアプリ構成を考えるのに参考になるかと思います。
5章
分散システム。システムを複数のサブシステム(今でいうマイクロサービスに近い?)に分割することによって、サブシステムごとにスケールアウトできるメリットについて言及しています。
また複数のgoroutineの使用例、mapとlockを使って複数goroutineでmapを破壊しない方法、チャネルを使ったシグナルの送受信、Graceful Shutdownについても記載されています。
6章
5章で作ったシステムのデータをWebで公開する仕組みを作成します。
独立したAPIと、Webサイトをそれぞれ分ける仕組みです。
APIの方は実案件では何らかのフレームワーク使うかな、という感じはしました。
7章
WebAPIの作成。ここまでやってきたことの応用例という感じです。
8章
コンソールアプリでファイルのバックアップシステムを作成します。
これもここまでやってきたことの応用例という感じです。
以上です。
watermillを触ってみた
前回に引き続きメッセージを扱うライブラリを触ってみました。
github.com
「watermill」というもので、メッセージストリームを効率的に扱うためことを目的としているようです。
手始めにREADMEにある「Getting Started Guide」(https://watermill.io/docs/getting-started/)から始めてみたのですが、一番最初にあるサンプルを改変してみたので上げておきたいと思います。
ポイントとしては
- メッセージを受信するためのChannelを複数(2つ)最初に定義する(messagesA、messagesB)
- それぞれのChannel(messagesA、messagesB)にはトピック名を定義する
- メッセージを受信するreceiveMessages()をgoroutineとして実行する
- publishMessages()でトピック名を指定してメッセージを送信する
- 最後に終了のログ(finish)も出している
点です。ソースは以下のようになります。
GitHub - SrcHndWng/go-watermill-simple-sample
package main import ( "context" "log" "time" "github.com/ThreeDotsLabs/watermill" "github.com/ThreeDotsLabs/watermill/message" "github.com/ThreeDotsLabs/watermill/pubsub/gochannel" ) func main() { pubSub := gochannel.NewGoChannel( gochannel.Config{}, watermill.NewStdLogger(false, false), ) messagesA, err := pubSub.Subscribe(context.Background(), "example.topic.A") if err != nil { panic(err) } messagesB, err := pubSub.Subscribe(context.Background(), "example.topic.B") if err != nil { panic(err) } go receiveMessages(messagesA) go receiveMessages(messagesB) publishMessages(pubSub) log.Println("finish!") } func publishMessages(pubSub *gochannel.GoChannel) error { publishMessage := func(publisher message.Publisher, topic string, msgStr string) error { msg := message.NewMessage(watermill.NewUUID(), []byte(msgStr)) return publisher.Publish(topic, msg) } for i := 0; i < 5; i++ { if err := publishMessage(pubSub, "example.topic.A", "Hello, message A!"); err != nil { return err } if err := publishMessage(pubSub, "example.topic.B", "Hello, message B!"); err != nil { return err } time.Sleep(time.Second) } return nil } func receiveMessages(messages <-chan *message.Message) { for msg := range messages { log.Printf("received message: %s, payload: %s", msg.UUID, string(msg.Payload)) msg.Ack() // Ack sends message's acknowledgement. } }
動かしてみると分かりますが、A・Bのメッセージが受信される順番は、送信した順になるとは限らないようです。
メッセージを受信したら、「Ack()」で受信済である旨を通知することもポイントかと思います。
これ以外は、goroutineを使っていること、pushish~、subscribe~など一般的な用語がそのままメソッド名になっているため、分かりやすいライブラリかなと思います。
RxGoを触ってみた
RxGoというReactiveをGolangで実現するためのライブラリを触ってみました。
READMEのサンプルが分かりやすく、それを見るのがいいとは思いますが、自分の理解のために多少改変してみたりもしたのでサンプルとして上げておきます。
GitHub - SrcHndWng/go-rx-sample
helloworld
一番シンプルなサンプルで、READMEの"Simple Usage"を改修したものになります。
READMEのサンプルとの違いは、time.Sleepを使って1秒毎にメッセージを送信するようにした点です。
実案件ではtime.Sleepの他、何らかのイベントを切っ掛けにメッセージを送信することもあるかと思いますが、そのような使用を想定しました。
コメントに書いたように "not error raise. print 'Done!'"の行を実行すると全てを実行して"Done!"の表示まで実行します。
コメントアウトしてある"error sample"以下のif~else文を実行するようにし、 "not error raise.・・・"をコメントアウトするとobserverのErrHandlerにてエラーがhandleされます。
package main import ( "fmt" "time" "github.com/reactivex/rxgo/observable" "github.com/reactivex/rxgo/observer" ) func main() { watcher := observer.Observer{ NextHandler: func(item interface{}) { fmt.Printf("handled: %v\n", item) }, ErrHandler: func(err error) { fmt.Printf("error: %v\n", err) }, DoneHandler: func() { fmt.Println("Done!") }, } message := make(chan interface{}) source := observable.Observable(message) sub := source.Subscribe(watcher) go func() { cnt := 0 for { if cnt > 4 { break } // error sample // if cnt == 3 { // message <- errors.New("some error") // } else { // message <- fmt.Sprintf("Hello, cnt = %d", cnt) // } message <- fmt.Sprintf("Hello, cnt = %d", cnt) // not error raise. print 'Done!' cnt++ time.Sleep(1 * time.Second) } close(message) }() <-sub }
grouping
READMEの"Simple Usage"の次のサンプルを改修したものになり、handlerを observer.Newでグループ化したものです。
ただ処理概要は先の"helloworld"と同じにしてあるので、比較してみてください。
package main import ( "fmt" "time" "github.com/reactivex/rxgo/handlers" "github.com/reactivex/rxgo/observable" "github.com/reactivex/rxgo/observer" ) func main() { onNext := handlers.NextFunc(func(item interface{}) { fmt.Printf("handled: %v\n", item) }) onError := handlers.ErrFunc(func(err error) { fmt.Printf("error: %v\n", err) }) onDone := handlers.DoneFunc(func() { fmt.Println("Done!") }) watcher := observer.New(onNext, onError, onDone) message := make(chan interface{}) source := observable.Observable(message) sub := source.Subscribe(watcher) go func() { cnt := 0 for { if cnt > 4 { break } // error sample // if cnt == 3 { // message <- errors.New("some error") // } else { // message <- fmt.Sprintf("Hello, cnt = %d", cnt) // } message <- fmt.Sprintf("Hello, cnt = %d", cnt) // not error raise. print 'Done!' cnt++ time.Sleep(1 * time.Second) } close(message) }() <-sub }
observable
READMEの"Recap"欄に「In RxGo, it's useful to think of Observable and Connectable as channels with additional ability to Subscribe handlers.」とあります。
これもREADMEのサンプルを改修したものですが
- DBへの登録などの代わりに標準出力するようにした
- エラーについても実装した
などを変えています。
実行してみると"f1"と"f2"の出力順序が逆になりますが、このことからも先に引用した"Recap"欄の旨が分かるかと思います。
package main import ( "errors" "fmt" "time" "github.com/reactivex/rxgo/handlers" "github.com/reactivex/rxgo/observable" ) func main() { f1 := func() interface{} { // Simulate a blocking I/O time.Sleep(2 * time.Second) return 1 } f2 := func() interface{} { // Simulate a blocking I/O time.Sleep(time.Second) return 2 } f3 := func() interface{} { // Simulate a blocking I/O time.Sleep(3 * time.Second) return 3 } f4 := func() interface{} { // Simulate a blocking I/O time.Sleep(4 * time.Second) return errors.New("some error") } onNext := handlers.NextFunc(func(v interface{}) { fmt.Printf("handled, v = %v\n", v) }) wait := observable.Start(f1, f2, f3, f4).Subscribe(onNext) sub := <-wait if err := sub.Err(); err != nil { fmt.Printf("error raise. err = %v\n", err) } fmt.Println("finish") }
GolangでWebAssemblyの復習をした
GolangでWebAssemblyの復習するため、いくつかサイトを見て写経してみました。
一年以上ぶり、かつ実行環境が異なったので、改めて気づいたことがあったので、メモとして残しておきます。
実行環境について
- Windows10上のWindows Subsystem for Linux(Ubuntu)
- go1.12.6
- goenv 2.0.0beta11
- Visual Studio Code Remote DevelopmentでWindows Subsystem for Linuxのソースを編集
先に書いておくと、goenv、Visual Studio Code 固有のポイントに気づいた感じです。
気づいたポイント
ビルドコマンドはGOOS、GOARCHの指定も含めて一行で行う
https://github.com/golang/go/wiki/WebAssembly#getting-started
にあるように、以下のように一行のコマンドでビルドしないと、ブラウザからの実行時にエラーとなりました。
正しいwasm形式にビルドできないようです。
$ GOOS=js GOARCH=wasm go build -o main.wasm
wasm_exec.js、テンプレートのHTMLのコピー
サイトによってwasm_exec.jsのコピーのコマンドや、テンプレートのHTMLは自作するなどの違いがありましたが、
両方とも「GOROOT」環境変数を使ってコピーし、HTMLはファイル名を変更するのが効率がいいと思いました。
https://blog.gopheracademy.com/advent-2018/go-in-the-browser/
からの引用です。
$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . $ cp "$(go env GOROOT)/misc/wasm/wasm_exec.html" . $ mv wasm_exec.html index.html
goexecの実行について
ローカルでWebサーバを動かすのに「goexec」を使う例がありました。
https://github.com/shurcooL/goexec
「go get」で取得して「goexec」コマンドをそのまま実行しても、私の環境ではgoexecが見つからないでエラーとなりました。
goenvで複数のバージョンのgoを入れているためかと思われます。
以下のようにパスを含めたコマンドで実行できました。
$GOPATH/bin/goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'
could not import syscall/js
syscall/jsのimportでVisual Studio Code上で「could not import syscall/js」というエラーとなりました。
https://github.com/Microsoft/vscode-go/issues/1874
こちらのissueにあるように、Visual Studio CodeのWorkspaceのsettings.jsonを以下のようにしたら解決しました。
{ "go.toolsEnvVars": { "GOOS": "js", "GOARCH": "wasm", "GOROOT": "/usr/local/go", "GOPATH": "/home/corey/src/goprojects" } }
以上です。
go-linqを触ってみた
GolangでC#のLINQのようなデータ操作をできるgo-linqを触ってみました。
GitHub - ahmetb/go-linq: .NET LINQ capabilities in Go
使い方は公式のREADMEにそのまま書いてあるので改めてここには書きませんが
便利そうなので備忘録として残しておきます。
GoのSliceに対して、先に書いたようにLINQ likeな操作をすることができ
- From(slice)
- Where(predicate)
- Select(selector)
- Union(data)
- GroupBy(selector)
を使った抽出や、Query型を使用した独自Queryを定義しての抽出などができるようです。
DBから一度に多めのデータを取得 → go-linqで細かい条件を指定しての抽出、などは実案件で割と使えそうな気がします。
とりあえず便利そうなので、気になった方は一度、一番上に張ったリンクのREADMEを一読することをお勧めします。
ginとendressを使ってGraceful Restartを実装してみた
GolangのWeb Frameworkであるgin、"Zero downtime restarts"を行うためのendressを使って
タイトルにあるようにGraceful RestartするWebのサンプルを作ってみました。
https://github.com/gin-gonic/gin
https://github.com/fvbock/endless
サンプルソース
ソースは以下のようになります。
main.go
package main import ( "fmt" "log" "os/exec" "syscall" "time" "github.com/fvbock/endless" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/hello", func(c *gin.Context) { t := time.Now() const layout = "2006-01-02 15:04:05" const version = "V1" message := fmt.Sprintf("Hello! Now is %s. This version is %s.", t.Format(layout), version) c.JSON(200, gin.H{"message": message}) }) router.GET("/restart", func(c *gin.Context) { pid, err := readPid() if err != nil { log.Fatal(err) } err = exec.Command("kill", "-SIGHUP", pid).Start() if err != nil { log.Fatal(err) } message := "accept restart. pid = " + pid c.JSON(200, gin.H{"message": message}) }) server := endless.NewServer("localhost:4430", router) server.BeforeBegin = func(add string) { pid := syscall.Getpid() err := writePid(pid) if err != nil { log.Fatal(err) } log.Printf("Actual pid is %d", pid) } err := server.ListenAndServe() if err != nil { log.Fatal(err) } }
pid.go
package main import ( "io/ioutil" "log" "os" "path/filepath" "strconv" ) func getPidFilePath() (string, error) { exe, err := os.Executable() if err != nil { return "", err } return filepath.Dir(exe) + "/pid", nil } func writePid(pid int) error { path, err := getPidFilePath() if err != nil { log.Fatal(err) } file, err := os.Create(path) if err != nil { log.Fatal(err) } defer file.Close() _, err = file.Write(([]byte)(strconv.Itoa(pid))) if err != nil { return err } return nil } func readPid() (string, error) { path, err := getPidFilePath() if err != nil { log.Fatal(err) } data, err := ioutil.ReadFile(path) if err != nil { return "", err } return string(data), nil }
挨拶と現在日時を返す/hello、(セキュリティ的には微妙ですが)Graceful Restartを行う/restartの2つのGetメソッドを持ちます。
やっていることはソースを見てもらえれば分かりやすいかとは思いますが、ざっと書くと
- ginでrouterを作り、Getメソッドを実装する
- そのrouterをendressに渡してserverとして起動する
ことを行っています。
起動時にはpid.go内のwritePid()にてプロセスIDをファイルに出力しておき、/restartが呼ばれた時にはreadPid()で読み込んで使用します。
動作確認
ビルドしてバイナリを起動すると以下のように出力されます。
[GIN-debug] GET /hello --> main.main.func1 (3 handlers) [GIN-debug] GET /restart --> main.main.func2 (3 handlers) 2019/06/03 22:03:51 Actual pid is 97
同時にpidファイルにプロセスIDが出力されるはずです。
別のターミナルを起動し、動作確認のためにいくつかのコマンドを実行します。
$ curl http://localhost:4430/hello {"message":"Hello! Now is 2019-06-03 22:05:02. This version is V1."} $ curl http://localhost:4430/restart {"message":"accept restart. pid = 97"} $ curl http://localhost:4430/hello {"message":"Hello! Now is 2019-06-03 22:06:15. This version is V1."} $ kill -SIGHUP 202 # 最初にバイナリを起動したターミナルにrestart後のプロセスIDが出力される $ kill -SIGINT 212
「kill -SIGHUP プロセスID」でGraceful Restartができます。ここで指定するプロセスIDは、バイナリの起動時にターミナルに出力された値です。
URLの/restartでもRestartできますが、実際は「kill -SIGHUP」を内部で呼び出しているだけです。
終了は「kill -SIGINT プロセスID」で行います。
Microsoft TypeScript Vue Starter を vue-loader 15系でやってみた
3カ月くらい前ですが、TypeScriptの雰囲気を知るため、こちらにあるMicrosoftのTypeScript Vue Starterを途中までやってみました。
チュートリアル形式でREADMEが書かれているので、そのまま進めたのですが、途中で躓いたところがあったのでメモ代わりに書いておきます。
なおタイトルにあるようにvue-loader 15系での話なので、今後はどうなるかは分からないです。
やってみた範囲について
TypeScript Vue StarterのREADMEの「Single File Components」まで以下の修正をしつつ進めました。
webpack.config.jsの修正
npm run build をすると以下のようなエラーとなりました。
vue-loader was used without the corresponding plugin. Make sure to include VueLoaderPlugin in your webpack config.
どうもvue-loaderの14→15系に上がった時に変更があったようです。
webpack.config.js にVueLoaderPluginを明示的に追加してあげることで上手くいきました。
const { VueLoaderPlugin } = require("vue-loader"); module.exports = { (中略) mode: "development", module: { rules: [ { test: /\.vue$/, (中略) }, (中略) ] }, plugins: [ new VueLoaderPlugin() ],
もう一つ、やはりnpm run build時に以下のエラーにもなりました。
You may need an appropriate loader to handle this file type.
要は適切なloaderを定義しろ、ってことです。
今回はstyleタグを読めなかったので、webpack.config.js に以下を追加しました。
var path = require('path') var webpack = require('webpack') const { VueLoaderPlugin } = require("vue-loader"); module.exports = { entry: './src/index.ts', (中略) module: { rules: [ (中略) { test: /\.css$/, loader: 'style-loader!css-loader' }, ] },