- 投稿日:2019-11-12T20:29:18+09:00
初見でGo言語Webアプリケーションをやってみる(Hello World編)
はじめに
Go初心者が、初見でWebアプリケーションを作るまでの過程をつづっていきます。
環境
macOS:Catalina
goバージョン:go1.12.9Go言語のWebフレームワークは何か?
私は普段
Kotlin
を使っているので、WebフレームワークはSpringFrameworkを使用しています。SpringFramework的な、Go言語におけるWebフレームワークのスタンダードは何なのでしょうか?
https://qiita.com/loftkun/items/1a9951d1864bebdc51e1
上記の記事を発見し、ginがよく使用されているようなので、そちらを使っていきたいと思います。ginフレームワークをいれる
前提としてGoがインストールされている必要があります。
そのうえで、ginのREADMEの通りに、インストールをします。$ go get -u github.com/gin-gonic/ginJSONを返すサンプルコードを動かす
ginのREADMEのサンプルコードを実行してみます。
example.gopackage main import "github.com/gin-gonic/gin" func main() { //LoggerとRecoveryがセットになったEngineインスタンスを返してくれる r := gin.Default() //SpringにおけるGetMapping r.GET("/ping", func(c *gin.Context) { //JSON形式のbodyを返す c.JSON(200, gin.H{ "message": "pong", }) }) //http.Serverを起動してリクエストを受け付ける r.Run() }そして、サンプルコードを実行します。
$ go run example.go実行コマンドを打つと、「ネットワーク接続への許可」をきいてくるので、「許可」します。
ブラウザで
localhost:8080/ping
にアクセスすると
{"message":"pong"}
とかえってきました。HTMLを返してみる
JSONだと見た目が楽しくないですね。
先ほどのサンプルを改良して、HTMLを返すサンプルを作ってみます。html_example.gopackage main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.LoadHTMLGlob("templates/*.html") r.GET("/hello", func(c *gin.Context) { c.HTML(200,"hello.html", none) }) r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") }templates/hello.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>HelloGo</title> </head> <body> <h1>Hello Go</h1> <img src="https://golang.org/lib/godoc/images/home-gopher.png"> </body> </html>コードを書いたら、以下のようにターミナルで実行コマンドをうちます。
$ go run html_example.goブラウザで
localhost:8080/hello
にアクセスすると、
HTMLが表示されましたね。
次回
HTMLに変数を渡してみる。
- 投稿日:2019-11-12T19:22:16+09:00
Go Compilerのインライン化についてまとめた
はじめに
Go は末尾再帰が最適化されない。というかあえて最適化しない、という選択を採用しているようです。
そもそも私はあまりコンパイラの気持ちになったことがなく、関数呼び出しのときにインライン展開されるということがどういうことかわかりませんでした。
本記事では以下の 2 点についてまとめてみます。
- 関数のインライン展開とはどういうことか
- インライン展開されることでどのような効果があるのか
関数のインライン展開とはどういうことか
インライン展開とは
インライン展開(インラインてんかい、英: inline expansion または 英: inlining)とは、コンパイラによる最適化手法の1つで、関数を呼び出す側に呼び出される関数のコードを展開し、関数への制御転送をしないようにする手法。これにより関数呼び出しに伴うオーバーヘッドを削減する。特に小さくて頻繁に呼ばれる関数では効果的であり、呼び出し側にそのコードを展開することで定数畳み込みなどのさらなる最適化を施せる可能性が生じる。問題点はバイナリコードが一般に肥大化する結果を招く点であり、参照の局所性を損なうほどだったり、リソースの限界を超えると性能がかえって悪化することになる。
「インライン展開」より引用
端的に言って、関数呼び出しのコードを呼び出し元に展開することで、関数呼び出しのオーバヘッドを削減し高速化する、という最適化手法のようです。
Go で関数がインライン化されたときとそうでないときで、コンパイラ結果にどのような違いがあるのか確認してみます。
Go では
go:noinline
という pragma を用いることができ、この pragma を用いることで関数をインライン化しないようにコンパイルします。1go:noinline
はテストを目的とした pragma です。2pragma を用いて、インライン化するコードとそうでないコードで、それぞれコンパイル後の実行ファイルのダンプを取得して比較してみます。
サンプル実装
- インライン化する場合
hello.gopackage main func main() { Hello() } func Hello() interface{} { return struct{}{} }go build main.go go tool objdump main.exe > dump.txt
- インライン化しない場合
main.gopackage main func main() { HelloNoInline() } //go:noinline func HelloNoInline() interface{} { return struct{}{} }go build main.go go tool objdump main.exe > dump_noinline.txt出力結果の比較
ダンプした結果の main 関数のテキストセグメントを抜粋します。
- インライン化する場合
... TEXT main.main(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:4 0x44f5c0 90 NOPL main.go:4 0x44f5c1 c3 RET :-1 0x44f5c2 cc INT $0x3 :-1 0x44f5c3 cc INT $0x3 :-1 0x44f5c4 cc INT $0x3 :-1 0x44f5c5 cc INT $0x3 :-1 0x44f5c6 cc INT $0x3 :-1 0x44f5c7 cc INT $0x3 :-1 0x44f5c8 cc INT $0x3 :-1 0x44f5c9 cc INT $0x3 :-1 0x44f5ca cc INT $0x3 :-1 0x44f5cb cc INT $0x3 :-1 0x44f5cc cc INT $0x3 :-1 0x44f5cd cc INT $0x3 :-1 0x44f5ce cc INT $0x3 :-1 0x44f5cf cc INT $0x3 ...
- インライン化しない場合
... TEXT main.main(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:3 0x44f5c0 65488b0c2528000000 MOVQ GS:0x28, CX main.go:3 0x44f5c9 488b8900000000 MOVQ 0(CX), CX main.go:3 0x44f5d0 483b6110 CMPQ 0x10(CX), SP main.go:3 0x44f5d4 761d JBE 0x44f5f3 main.go:3 0x44f5d6 4883ec18 SUBQ $0x18, SP main.go:3 0x44f5da 48896c2410 MOVQ BP, 0x10(SP) main.go:3 0x44f5df 488d6c2410 LEAQ 0x10(SP), BP main.go:4 0x44f5e4 e817000000 CALL main.HelloNoInline(SB) main.go:5 0x44f5e9 488b6c2410 MOVQ 0x10(SP), BP main.go:5 0x44f5ee 4883c418 ADDQ $0x18, SP main.go:5 0x44f5f2 c3 RET main.go:3 0x44f5f3 e81882ffff CALL runtime.morestack_noctxt(SB) main.go:3 0x44f5f8 ebc6 JMP main.main(SB) :-1 0x44f5fa cc INT $0x3 :-1 0x44f5fb cc INT $0x3 :-1 0x44f5fc cc INT $0x3 :-1 0x44f5fd cc INT $0x3 :-1 0x44f5fe cc INT $0x3 :-1 0x44f5ff cc INT $0x3 TEXT main.HelloNoInline(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:9 0x44f600 488d05f9ec0000 LEAQ runtime.rodata+58112(SB), AX main.go:9 0x44f607 4889442408 MOVQ AX, 0x8(SP) main.go:9 0x44f60c 488d05cd730800 LEAQ runtime.zerobase(SB), AX main.go:9 0x44f613 4889442410 MOVQ AX, 0x10(SP) main.go:9 0x44f618 c3 RET :-1 0x44f619 cc INT $0x3 :-1 0x44f61a cc INT $0x3 :-1 0x44f61b cc INT $0x3 :-1 0x44f61c cc INT $0x3 :-1 0x44f61d cc INT $0x3 :-1 0x44f61e cc INT $0x3 :-1 0x44f61f cc INT $0x3 ...インライン化した場合は、実行ファイルから Hello 関数の呼び出しが消えています。インライン化された結果です。一方でインライン化しない場合は、実行ファイルに HelloNoInline 関数が存在し、main 関数から HelloNoInline 関数を CALL していることが分かります。
サンプルの実装例は、何もしない関数を定義しているため極端ですが、インライン化される場合とそうでない場合でコンパイルされた結果がどのように異なるか確認できました。インライン展開されることでどのような効果があるのか
実行時間にどの程度影響があるのか、ベンチマークをとってみます。サンプルの関数の実装は先ほどの例の実装と同様です。
hello.gopackage hello //go:noinline func HelloNoInline() interface{} { return struct{}{} } func Hello() interface{} { return struct{}{} }hello_test.gopackage hello import "testing" func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { Hello() } } func BenchmarkHelloNoInline(b *testing.B) { for i := 0; i < b.N; i++ { HelloNoInline() } }> go test -bench . -benchmem goos: windows goarch: amd64 pkg: github.com/d-tsuji/go-sandbox/opt/hello BenchmarkHello-8 2000000000 0.34 ns/op 0 B/op 0 allocs/op BenchmarkHelloNoInline-8 2000000000 1.51 ns/op 0 B/op 0 allocs/op PASS ok github.com/d-tsuji/go-sandbox/opt/hello 4.295s関数は空構造体を返しているだけで、メモリはアロケートしておらず、どちらも 0 B/op で想定通りです。一方で実行時間はインライン化している場合は、そうでない場合と比較して約 1/5 倍の実行時間になっている結果が得られました3。
まとめ
インライン化する場合とそうでない場合で Go のコンパイラがどのようなコードを取得するか確認しました。またインライン化の高速化の効果を確認しました。
Go のコンパイラがどのような最適化を実施するかは Compiler And Runtime Optimizations のページが参考になります。
上記のよると、関数のインライン化の要件は結構厳しくて、以下を満たす必要があります。
- 関数に含まれる式が 40 個未満
- 関数呼出し・ループ・クロージャー・
panic
・recover
・select
・switch
といった複雑なものを含まないOnly short and simple functions are inlined. To be inlined a function must contain less than ~40 expressions and does not contain complex things like function calls, loops, labels, closures, panic's, recover's, select's, switch'es, etc
関数の中身が小さい(今回の場合は、ない)ために、より効果的な結果が得られていると考えられます。 ↩
- 投稿日:2019-11-12T19:22:16+09:00
Go Compilerのインライン展開についてまとめた
はじめに
Go は末尾再帰が最適化されない。というかあえて最適化しない、という選択を採用しているようです。
そもそも私はあまりコンパイラの気持ちになったことがなく、関数呼び出しのときにインライン展開されるということがどういうことかわかりませんでした。
本記事では以下の 2 点についてまとめてみます。
- 関数のインライン展開とはどういうことか
- インライン展開されることでどのような効果があるのか
関数のインライン展開とはどういうことか
インライン展開とは
インライン展開(インラインてんかい、英: inline expansion または 英: inlining)とは、コンパイラによる最適化手法の1つで、関数を呼び出す側に呼び出される関数のコードを展開し、関数への制御転送をしないようにする手法。これにより関数呼び出しに伴うオーバーヘッドを削減する。特に小さくて頻繁に呼ばれる関数では効果的であり、呼び出し側にそのコードを展開することで定数畳み込みなどのさらなる最適化を施せる可能性が生じる。問題点はバイナリコードが一般に肥大化する結果を招く点であり、参照の局所性を損なうほどだったり、リソースの限界を超えると性能がかえって悪化することになる。
「インライン展開」より引用
端的に言って、関数呼び出しのコードを呼び出し元に展開することで、関数呼び出しのオーバヘッドを削減し高速化する、という最適化手法のようです。
Go で関数がインライン化されたときとそうでないときで、コンパイラ結果にどのような違いがあるのか確認してみます。
Go では
go:noinline
という pragma を用いることができ、この pragma を用いることで関数をインライン化しないようにコンパイルします。1go:noinline
はテストを目的とした pragma です。2pragma を用いて、インライン化するコードとそうでないコードで、それぞれコンパイル後の実行ファイルを逆アセンブリして確認します。Go に標準で備わっている objdump を用いて実行ファイルを逆アセンブリすることができます。
サンプル実装
- インライン化する場合
hello.gopackage main func main() { Hello() } func Hello() interface{} { return struct{}{} }go build main.go go tool objdump main.exe > dump.txt
- インライン化しない場合
main.gopackage main func main() { HelloNoInline() } //go:noinline func HelloNoInline() interface{} { return struct{}{} }go build main.go go tool objdump main.exe > dump_noinline.txt出力結果の比較
ダンプした結果の main 関数のテキストセグメントを抜粋します。
- インライン化する場合
... TEXT main.main(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:4 0x44f5c0 90 NOPL main.go:4 0x44f5c1 c3 RET :-1 0x44f5c2 cc INT $0x3 :-1 0x44f5c3 cc INT $0x3 :-1 0x44f5c4 cc INT $0x3 :-1 0x44f5c5 cc INT $0x3 :-1 0x44f5c6 cc INT $0x3 :-1 0x44f5c7 cc INT $0x3 :-1 0x44f5c8 cc INT $0x3 :-1 0x44f5c9 cc INT $0x3 :-1 0x44f5ca cc INT $0x3 :-1 0x44f5cb cc INT $0x3 :-1 0x44f5cc cc INT $0x3 :-1 0x44f5cd cc INT $0x3 :-1 0x44f5ce cc INT $0x3 :-1 0x44f5cf cc INT $0x3 ...
- インライン化しない場合
... TEXT main.main(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:3 0x44f5c0 65488b0c2528000000 MOVQ GS:0x28, CX main.go:3 0x44f5c9 488b8900000000 MOVQ 0(CX), CX main.go:3 0x44f5d0 483b6110 CMPQ 0x10(CX), SP main.go:3 0x44f5d4 761d JBE 0x44f5f3 main.go:3 0x44f5d6 4883ec18 SUBQ $0x18, SP main.go:3 0x44f5da 48896c2410 MOVQ BP, 0x10(SP) main.go:3 0x44f5df 488d6c2410 LEAQ 0x10(SP), BP main.go:4 0x44f5e4 e817000000 CALL main.HelloNoInline(SB) main.go:5 0x44f5e9 488b6c2410 MOVQ 0x10(SP), BP main.go:5 0x44f5ee 4883c418 ADDQ $0x18, SP main.go:5 0x44f5f2 c3 RET main.go:3 0x44f5f3 e81882ffff CALL runtime.morestack_noctxt(SB) main.go:3 0x44f5f8 ebc6 JMP main.main(SB) :-1 0x44f5fa cc INT $0x3 :-1 0x44f5fb cc INT $0x3 :-1 0x44f5fc cc INT $0x3 :-1 0x44f5fd cc INT $0x3 :-1 0x44f5fe cc INT $0x3 :-1 0x44f5ff cc INT $0x3 TEXT main.HelloNoInline(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:9 0x44f600 488d05f9ec0000 LEAQ runtime.rodata+58112(SB), AX main.go:9 0x44f607 4889442408 MOVQ AX, 0x8(SP) main.go:9 0x44f60c 488d05cd730800 LEAQ runtime.zerobase(SB), AX main.go:9 0x44f613 4889442410 MOVQ AX, 0x10(SP) main.go:9 0x44f618 c3 RET :-1 0x44f619 cc INT $0x3 :-1 0x44f61a cc INT $0x3 :-1 0x44f61b cc INT $0x3 :-1 0x44f61c cc INT $0x3 :-1 0x44f61d cc INT $0x3 :-1 0x44f61e cc INT $0x3 :-1 0x44f61f cc INT $0x3 ...インライン化した場合は、実行ファイルから Hello 関数の呼び出しが消えています。インライン化された結果です。一方でインライン化しない場合は、実行ファイルに HelloNoInline 関数が存在し、main 関数から HelloNoInline 関数を CALL していることが分かります。
サンプルの実装例は、何もしない関数を定義しているため極端ですが、インライン化される場合とそうでない場合でコンパイルされた結果がどのように異なるか確認できました。インライン展開されることでどのような効果があるのか
実行時間にどの程度影響があるのか、ベンチマークをとってみます。サンプルの関数の実装は先ほどの例の実装と同様です。
hello.gopackage hello //go:noinline func HelloNoInline() interface{} { return struct{}{} } func Hello() interface{} { return struct{}{} }hello_test.gopackage hello import "testing" func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { Hello() } } func BenchmarkHelloNoInline(b *testing.B) { for i := 0; i < b.N; i++ { HelloNoInline() } }> go test -bench . -benchmem goos: windows goarch: amd64 pkg: github.com/d-tsuji/go-sandbox/opt/hello BenchmarkHello-8 2000000000 0.34 ns/op 0 B/op 0 allocs/op BenchmarkHelloNoInline-8 2000000000 1.51 ns/op 0 B/op 0 allocs/op PASS ok github.com/d-tsuji/go-sandbox/opt/hello 4.295s関数は空構造体を返しているだけで、メモリはアロケートしておらず、どちらも 0 B/op で想定通りです。一方で実行時間はインライン化している場合は、そうでない場合と比較して約 1/5 倍の実行時間になっている結果が得られました3。
まとめ
インライン化する場合とそうでない場合で Go のコンパイラがどのようなコードを取得するか確認しました。またインライン化の高速化の効果を確認しました。
Go のコンパイラがどのような最適化を実施するかは Compiler And Runtime Optimizations のページが参考になります。
上記のよると、関数のインライン化の要件は結構厳しくて、以下を満たす必要があります。
- 関数に含まれる式が 40 個未満
- 関数呼出し・ループ・クロージャー・
panic
・recover
・select
・switch
といった複雑なものを含まないOnly short and simple functions are inlined. To be inlined a function must contain less than ~40 expressions and does not contain complex things like function calls, loops, labels, closures, panic's, recover's, select's, switch'es, etc
関数の中身が小さい(今回の場合は、ない)ために、より効果的な結果が得られていると考えられます。 ↩
- 投稿日:2019-11-12T18:40:04+09:00
Goで自分のqiita投稿記事一覧を見れるコマンドラインツールを作ってみた。
Goで自分のqiita投稿記事一覧を見れるコマンドラインツールを作ってみた。
Go言語の勉強をしたので、Cliアプリケーションを作成してみます。
何番煎じ目か知れませんが、QiitaのAPIを叩いて自分のqiitaの投稿記事一覧を見れるコマンドラインツールを作ってみます。
これが出来た暁には、qiitaを書く意欲がもりもりと湧いてくる筈である。作ったもの
$ qi myqi
と打つと、
と出力されるというコマンドラインツール。
githubはこちらです。:https://github.com/yujiteshima/qi使用するフレームワーク
urfave/cli
というCLIアプリケーションフレームワークを使用してみる。
urfaveの使い方は公式のDocumentが一番分かりやすかったです。https://github.com/urfave/cli/blob/master/docs/v1/manual.mdディレクトリきったり、
go mod init
したりする。Go 1.11から採用されている
modules
を使用する。gopathの外でも開発が可能であるので、
home
直下にworkspace_golang
というディレクトリを作って、その中に今回のプロジェクトのディレクリを作成しました。
golangはbuildした時の実行ファイル名がデフォルトではディレクトリ名になるので、
コマンドの名前を付けると思って考えます。
「qiitaの記事を取得するコマンドだから········
qi
だ。」
というような感じで考えました。悩みますが、勉強が全然進まなくなりますので、さっと考えて進めていきます。$ mkdir workspace_golang $ cd workspace_golang $ mkdir qi <- 好きな名前を付ける、オプションを付けずに`build`するとフォルダ名がコマンド名になる次にモジュールの初期化を行います。
$ cd qi $ go mod init github.com/username/qiこれでプロジェクトフォルダに
go.mod
ファイルが作成されます。
このファイルがnpm
のpackage.json
みたいなもののようだ。buildすると新たに、
go.sum
ファイルが作成される。
このファイルはbuildした実行ファイルの依存パッケージのlockする為にバージョンが記載されている。
npm
のpackage-lock.json
みたいなもののようだ。次にCLIフレームワーク
urfave/cli
をgo get
しておきます。$ go get github.com/urfave/cli
ディレクトリ構成
今回は
myqi
コマンドだけしか実装しないが、他にも好きなキーワードで最新記事や、人気記事を取ってきたり、様々なコマンド実装をしていきたいので、cmdパッケージへ分けていきたいと思います。qi ├── cmd │ └── myqi.go ├── go.mod ├── go.sum └── main.go全体のコード
まず全体のコードを載せ、続いてコードの説明をしていきたいと思います。
main.gopackage main import ( "os" "github.com/username/qi/cmd" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "qiitasearch" app.Usage = "search qiita articles" app.Version = "0.1.0" app.Commands = []cli.Command{ { Name: "myqi", Usage: "qiita + mine : you get yours articles", Action: func(c *cli.Context) error { qiitaToken := os.Getenv("QIITA_TOKEN") datas := cmd.FetchQiitaData(qiitaToken) cmd.OutputQiitaData(datas) return nil }, }, } app.Run(os.Args) }myqi.gopackage cmd import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) // jsonをパースする為の構造体を定義する type Data struct { ID string `json:id` Url string `json:"url"` Title string `json:"title"` LikesCount int `json:"likes_count"` PageViewsCount int `json:"page_views_count"` } func FetchQiitaData(accessToken string) []Data { baseUrl := "https://qiita.com/api/v2/items?query=user:yujiteshima" // 様々な検索条件をかけるときはbaseUrlをv2/までにして他を変数で定義してurl.Parseで合体させる endpointURL, err := url.Parse(baseUrl) if err != nil { panic(err) } b, err := json.Marshal(Data{}) if err != nil { panic(err) } var resp = &http.Response{} // qiitaのアクセストークンがない場合はAuthorizationを付与しない // 2パターン作っておく。 // accessトークンは環境変数に入れておく。自分の場合は.bash_profileにexport文を書いている。 if len(accessToken) > 0 { // QiitaAPIにリクエストを送ってレスポンスをrespに格納する。 resp, err = http.DefaultClient.Do(&http.Request{ URL: endpointURL, Method: "GET", Header: http.Header{ "Content-Type": {"application/json"}, "Authorization": {"Bearer " + accessToken}, }, }) } else { fmt.Println("***** Access Token 無しでQiitaAPIを叩いています アクセス制限に注意して下さい*****") resp, err = http.DefaultClient.Do(&http.Request{ URL: endpointURL, Method: "GET", Header: http.Header{ "Content-Type": {"application/json"}, }, }) } defer resp.Body.Close() if err != nil { panic(err) } b, err = ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var datas []Data if err := json.Unmarshal(b, &datas); err != nil { fmt.Println("JSON Unmarshal error:", err) return nil } /*********一覧取得では、ページビューがnilになるので個別で取りに行ってデータを得る*****************/ for i, val := range datas { article_id := val.ID baseUrl := "https://qiita.com/api/v2/items/" endpointURL2, err := url.Parse(baseUrl + article_id) if err != nil { panic(err) } b, err := json.Marshal(Data{}) if err != nil { panic(err) } resp, err = http.DefaultClient.Do(&http.Request{ URL: endpointURL2, Method: "GET", Header: http.Header{ "Content-Type": {"application/json"}, "Authorization": {"Bearer " + accessToken}, }, }) if err != nil { panic(err) } b, err = ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var m map[string]interface{} if err := json.Unmarshal(b, &m); err != nil { fmt.Println("JSON Unmarshal error:", err) return nil } datas[i].PageViewsCount = int(m["page_views_count"].(float64)) } return datas } // データの出力 func OutputQiitaData(datas []Data) { fmt.Println("************************自分のQiita投稿一覧******************************") for _, val := range datas { fmt.Printf("%-15v%v%v\n", "ID", ": ", val.ID) fmt.Printf("%-15v%v%v\n", "Title", ": ", val.Title) fmt.Printf("%-12v%v%v\n", "いいね", ": ", val.LikesCount) fmt.Printf("%-9v%v%v\n", "ページビュー", ": ", val.PageViewsCount) fmt.Printf("%-15v%v%v\n", "URL", ": ", val.Url) fmt.Println("-------------------------------------------------------------------------") } }main.goでしている事
urfave/cli
を使用して、コマンドラインツールのインターフェイスを定義する。
urfave/cli
では、複雑なインターフェイスも定義できるが、今回は、$ qi myqiとうてば自身の
qiita
記事が出力されるようにしたいと思います。
他のコマンドもゆくゆくは作りたいので最低限、コマンドを1つとるツールとしていきます。Qiitaのトークンの取得
qiitaのトークンを取得しておき、環境変数に入れておきます。qiitaのトークンの取得の仕方は、この記事を書いている2019/11時点では、Qiitaのページ右上の自分のアカウントのアイコンから、
設定 -> 左側のメニューのアプリケーション -> 新しくトークンを発行するとなっています。
自分の環境の場合は、.bash_profile
に取得したトークンを以下のように記述しています。.bash_profile# qiita_api export QIITA_TOKEN='取得したqiitaのアクセストークンを入れてください'別パッケージのimportは絶対パス指定で取得する
別のcmdディレクトリに作成した、cmdパッケージのimportは絶対パス指定にしないとならない。
modules
を使用した時と,していない時で、記述方法が違い、古い情報のまま相対パスで記述するとパスを見つけられずエラーがでる。https://qiita.com/yujiteshima/items/8dc2f782f27f147a1e3emain.go·抜粋import ( "os" "github.com/yujiteshima/qiita/cmd" // 絶対パスで指定する。 "github.com/urfave/cli" )初期化する
go
には、オブジェクト指向プログラミングに見られる「コンストラクタ」機能は無いですが、
「型のコンストラクタ」というパターンの利用が多く使われている。
型のコンストラクタはNew + 型名
とする事が多い。また、型のコンストラクタは対象の型のポインタ型を返すように定義するのが望ましく、urfave/cli
においてもそのようになっている。main.go·抜粋func main(){ app := cli.NewApp() }app.Commandsにコマンド
myqi
を登録する。main.go·抜粋app.Commands = []cli.Command{ { Name: "myqi", Usage: "qiita + mine : you get yours articles", Action: func(c *cli.Context) error { qiitaToken := os.Getenv("QIITA_TOKEN") datas := cmd.FetchQiitaData(qiitaToken) cmd.OutputQiitaData(datas) return nil }, }, }Nameに登録するコマンドを文字列で指定する。
Usageには使い方を登録する。helpで使われる。
Actionに実際の処理を定義する。
Action内で、
1. FectchMyQiitaData()を実行して、データを取得する。
2. OutputQiitaData()で出力する。以上が
main.go
での処理です。続いて、cmdパッケージの説明をします。cmdパッケージ
cmdパッケージには、構造体
Data
の定義とFetchMyQiitaData関数とOutputQiitaData関数をを定義している。importしているパッケージ
まず各種パッケージを
import
する。
- "encoding/json" : QiitaAPIから取得したjsonを
go
のデータにパースする時に使用する。- "fmt" : 標準出力に使用する。
- "io.ioutil" : QiitaAPIから取得したデータを読み込む時に使用する。
- "net/http" : QiitaAPIを叩きに行く時に使用する。
- "net/url" : 文字列からurlへパースする時に使用する。
cmd.go·抜粋package cmd import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" )jsonをパースする為に構造体を定義する
encoding/json
を用いて構造体のデータにパースする。
encoding/json
は構造体にTag
でjson:キー名
を定義しておくと、キー名を出力するjsonのキー名として使ってくれます。
書籍のスターティングGOのChapter5の構造体とインターフェイス
が分かりやすかったです。cmd.go·抜粋type Data struct { ID string `json:id` Url string `json:"url"` Title string `json:"title"` LikesCount int `json:"likes_count"` PageViewsCount int `json:"page_views_count"` }
FetchMyQiitaData()
を定義する。
FetchMyQiitaData
は引数としてaccessToken
を受け取ってHeader
にセットしている。この他にもサブコマンドに引数を受け取ってここで使用することも出来る。キーワードを引数で受け取って検索するコマンドを作るときに使える。
accessToken無しでリクエストを送った時には、accessToken無しでアクセスしているとメッセージを出すようにしておきました。
json
で受け取ったデータをパースする為に、標準パッケージのencoding/json
を使いました。Unmarshal
メソッドを使って、jsonをGoの構造体データのスライスにパースする。とても簡単に出来ます。
OutputQiitaData()
を定義するあとは出力する為の
OutputQiitaData
を定義するだけです。cmd.go·抜粋func OutputQiitaData(datas []Data) { fmt.Println("************************自分のQiita投稿一覧******************************") for _, val := range datas { fmt.Printf("%-15v%v%v\n", "ID", ": ", val.ID) fmt.Printf("%-15v%v%v\n", "Title", ": ", val.Title) fmt.Printf("%-12v%v%v\n", "いいね", ": ", val.LikesCount) fmt.Printf("%-9v%v%v\n", "ページビュー", ": ", val.PageViewsCount) fmt.Printf("%-15v%v%v\n", "URL", ": ", val.Url) fmt.Println("-------------------------------------------------------------------------") } }forループで出力する。
見やすくフォーマットしておく。ページビューが取れない問題発生
しかし、ここで一度出力して確認すると、ページビューが0となってしまっている。
アクセストークンが外れてしまっているのか等様々調べた結果、"https://qiita.com/api/v2/items?query=user:yujiteshima"
このように一覧を取得する際には、ページビューは
null
になって返ってくる。つまり、個別の記事検索でしか、ページビュ0は取得できないという事でした。個別のページからページビューを取得する
ページビューの表示を諦めそうになりますが、ページビュー欲しいです。個別のページから全て取得していきます。
for i, val := range datas { //fmt.Println("id:", val.ID) article_id := val.ID baseUrl := "https://qiita.com/api/v2/items/" endpointURL2, err := url.Parse(baseUrl + article_id) if err != nil { panic(err) } b, err := json.Marshal(Data{}) if err != nil { panic(err) } resp, err = http.DefaultClient.Do(&http.Request{ URL: endpointURL2, Method: "GET", Header: http.Header{ "Content-Type": {"application/json"}, "Authorization": {"Bearer " + accessToken}, }, }) if err != nil { panic(err) } b, err = ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var m map[string]interface{} if err := json.Unmarshal(b, &m); err != nil { fmt.Println("JSON Unmarshal error:", err) return nil } datas[i].PageViewsCount = int(m["page_views_count"].(float64)) }取得した一覧で得た
id
を使ってループで回してpage_view_count
を取得していき、0が入ってしまっているpage_view_count
を更新していきます。これで、出力してみて完成です。
qiディレクトリで、go install
します。$ go installこれで、qiディレクリで無くても、コマンド
qi
が使えるようになった。まとめ
やはり、何か作って見ると、理解が深まると感じた。
今回のアプリケーションの今後の拡張としては、
- さらに検索コマンドを増やしすこと。
- テストを書く。
- ページビューを取得する為に何回もAPIにアクセスする事になる事の改良。
3のページビューを取得する為に何回もAPIにアクセスする事になる事の改良については、
FaaS
とCloudStorage
等のサービスを使って定期実行してページビューも取得済のデータをキャッシュさせたものを叩きに行くか、コマンドラインツールから、コマンドでデータをリフレッシュさせれて、そのデータを叩きにいったり出来るようにしてみたい。たくさん
Go
を使って何か作って理解を深めていきたいと思います。リファクタリングした方が良い所や、まずい書き方あれば指摘お願いします。
- 投稿日:2019-11-12T17:41:29+09:00
mongoDB公式のGoのDriverを使ってみた
概要
mongoDB公式のGo用のDriver触ったので、そのまとめ
環境
go 1.12.5
go.mongodb.org/mongo-driver v1.1.2
mongoDB 3.1用語
- table → collection
- row → document
- column → field
接続
公式ドキュメントのUsage見た方が早い気がしやすが一応
import ( "context" "log" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://username:password@localhost:27017")) if err != nil { log.Fatalln(err) return } }
ApplyURI("mongodb://username:password@localhost:27017")
ここで接続先指定してる感じFindOne
1ドキュメントの取得
検索条件を構造体で作成した場合import ( "go.mongodb.org/mongo-driver/bson/primitive" // NOTE: 他に必要なパッケージは省略 ) type FindOneRequest struct { // NOTE: 検索したいfieldとvalueを適宜定義する TargetField string `json:"targetField" bson:"targetField"` } type FindOneResponse struct { ID primitive.ObjectID `json:"id" bson:"_id"` Hoge string `json:"hoge" bson:"hoge"` Fuga string `json:"fuga" bson:"fuga"` } func main() { // NOTE: 接続処理は省きやす 上の見てくだされ collection := client.Database("db_name").Collection("collection_name") request := FindOneRequest{ TargetField: "検索内容", } var response FindOneResponse err = collection.FindOne(context.Background(), request).Decode(&response) if err == mongo.ErrNoDocuments { log.Println("Documents not found") } else if err != nil { log.Fatalln(err) } log.Println(res) }構造体使う場合は
bson
タグ使ってドキュメントで使用している項目名を指定すればいい感じにマッピングしてくれる
ドキュメント作ると勝手に生成してくれるObjectIDはprimitive.ObjectID
型を使えばよきFind
複数ドキュメントの取得
今回は検索条件をbson.Dで定義し、レスポンスにはbson.Mを使用import ( "go.mongodb.org/mongo-driver/bson" // NOTE: 他に必要なパッケージは省略 ) func main() { // NOTE: 接続処理は省きやす collection := client.Database("db_name").Collection("collection_name") // NOTE: 検索したいfield名とvalueを定義 cur, err := collection.Find(ctx, bson.D{{Key: "field_name", Value: "value"}}) if err != nil { log.Fatal(err) } defer cur.Close(ctx) for cur.Next(ctx) { // NOTE: 1ドキュメントずつdecode // mapが返ってくる var result bson.M err := cur.Decode(&result) if err != nil { log.Fatal(err) } fmt.Println(result) } if err := cur.Err(); err != nil { log.Fatal(err) } }Find使った時に返り値には単純な配列とかが返ってくるわけでなく、
*Cursor
型が返ってくるのでここらへんの扱いが独特だと感じた次第InsertOne
1ドキュメントの追加
import ( "go.mongodb.org/mongo-driver/bson/primitive" // NOTE: 他に必要なパッケージは省略 ) type InsertOneRequest struct { ID primitive.ObjectID `json:"id" bson:"_id"` Hoge string `json:"hoge" bson:"hoge"` Fuga string `json:"fuga" bson:"fuga"` } func main() { // NOTE: 接続処理は省きやす collection := client.Database("db_name").Collection("collection_name") request := InsertOneRequest{ ID: primitive.NewObjectID(), Hoge: "hoge", Fuga: "fuga", } // NOTE: InsertOneの返り値には作成したドキュメントのObjectIDが返ってくる response, err := collection.InsertOne(context.Background(), request) if err != nil { log.Fatalln(err) } }作成したドキュメント使って何かしたいときは
request.ID
使ってFindOneした構造体を使う感じになりますかねInsertMany
複数ドキュメントの登録
import ( "go.mongodb.org/mongo-driver/bson/primitive" // NOTE: 他に必要なパッケージは省略 ) type Request struct { ID primitive.ObjectID `json:"id" bson:"_id"` Hoge string `json:"hoge" bson:"hoge"` Fuga string `json:"fuga" bson:"fuga"` } func main() { // NOTE: 接続処理は省きやす collection := client.Database("db_name").Collection("collection_name") var insertManyRequest []interface{} for index := 0; index < 10; index++ { indexString := strconv.Itoa(index) request := Request{ ID: primitive.NewObjectID(), Hoge: "hoge", Fuga: "fuga", } insertManyRequest = append(insertManyRequest, question) } // NOTE: 今回の場合、一括で10件登録してくれる // 返り値はinsertしたドキュメントのObjectIDの配列 response, err := collection.InsertMany(context.Background(), insertManyRequest) if err != nil { log.Fatalln(err) } }InsertManyメソッドの第二引数は
[]interface{}
型Aggregate
集計はAggregateってやつでできるらしい
mongoDBにはOperatorsってのがあってCollectionに対して、このOperatorsを使ってく感じですねOperatorsの中でもAggregateで使うのがPipeline
公式ドキュメントに比較的Exampleが書いてるのでそれとにらめっこするといつの間にか実装できるimport ( "go.mongodb.org/mongo-driver/bson" // NOTE: 他に必要なパッケージは省略 ) func main() { // NOTE: 接続処理は省きやす collection := client.Database("db_name").Collection("collection_name") pipeline := []bson.M{ bson.M{ "$match": bson.M{ "targetField": "value", }, }, bson.M{ "$group": bson.M{ "_id": "$targetFieldID", "count": bson.M{ "$sum": 1, }, }, }, } // NOTE: AggregateはFindと同様に`*Cursor`型を返すので、Cursor型用の扱い方が必要 hogeAggre, err := collection.Aggregate(ctx, pipeline) if err != nil { log.Fatalln(err) } defer hogeAggre.Close(ctx) for hogeAggre.Next(ctx) { var result bson.M err := answerAggre.Decode(&result) if err != nil { log.Fatal(err) } fmt.Println(result) } if err := answerAggre.Err(); err != nil { log.Fatal(err) } }出力結果は下記のような感じのを返してくれるはず
厳密には違うかもしれないので悪しからず[ { "_id": "hogehoge", "count": 15, }, { "_id": "fugafuga", "count": 13, }, ]
$
がついてるのがOperatorですね
今回Pipelineで指定したのはcollection_name
ってコレクションのtargetField
フィールドの値がvalue
のドキュメントたちをtargetFieldID
フィールドでGroupByしてtargetFieldID
ごとのドキュメント数をcount
フィールドに算出させた感じです
多分・・・OperatorとかPiplineのStageとかはもっとドキュメント読み込む必要がありやす
所感
InsertやFindするときの引数や返り値は構造体でしっかり型定義してたけども、結局ドキュメントの構造は同一のコレクションでも異なる可能性が出てくるのでbson.Mとかみたくしっかり型定義しないほうがいい気もした
- 投稿日:2019-11-12T17:13:15+09:00
アドベントカレンダーに向けてMarkdown に埋め込まれた Go のソースコードに gofmt をかけてくれるツールを作った
こんにちは pon です。みなさんアドベントカレンダーは参加しますか?僕も今年は Go や Rust、Elasticsearch のアドベントカレンダーに参加する予定です。
僕はQiitaの記事を書く際には手元でMarkdownファイルを作ってそこに下書きするのですが、gofmtをかけるのが正直面倒でした。もちろん、既存コードからコピペする分には良いかもしれませんが、記事向けにコードを修正したり補足を入れた時にgofmtがかかっていない状態になることがありました。
そこで来たるアドベントカレンダーに向けて Markdown に埋め込まれた Go のソースコードに gofmt をかけてくれるツールを作ったので、ツールの紹介と作り方を記事にしました。
gofmtmd
作ったツールはこちら
Markdown に埋め込まれた Go のソースコードに gofmt をかけてくれるツールです。例えば下記のようにGoのコードブロックを検知してその中身だけにgofmtがかかります。
もちろんQiitaのようなMarkdownをサポートしているブログサービスだけでなく、
README.md
を書く時なんかも便利かもしれません。インストールはこちら
$ go get github.com/po3rin/gofmtmd/cmd/gofmtmd
使い方としてはなるべく gofmt に寄せるようにしました。
-r
をつけると引数で指定したファイルをそのまま上書きフォーマット。-w
を使うと保存するファイルを指定できます。何もつけないとformatした結果が標準出力に流れます。# replace Go code with formated code $ gofmtmd testdata/testdata.md -r # write result to file instead of stdout $ gofmtmd testdata/testdata.md -w formatted.mdInternal
Markdown のASTを取得する
内部の実装を紹介します。主に使ったのはGo製の Markdown Parser である
gopkg.in/russross/blackfriday.v2
を使いました。これを使ってMarkdownのバイト列をASTに変換しています。
func FmtGoCodeInMarkdown(md []byte) ([]byte, error) { // ... n := blackfriday.New(blackfriday.WithExtensions(blackfriday.FencedCode)).Parse(md) // ... }実は fenced block の記法は拡張機能としてのみサポートされているので引数にオプションとして
blackfriday.WithExtensions(blackfriday.FencedCode)
を渡しています。これで fenced block として Parse してくれるようになります。ASTをWalkする
MarkdownのASTに対して何かしらの処理を帰納的に処理(トラバース)を実行するには。
Walk
という*blackfriday.Node
から生えているメソッドを使います。Walk
にノードごとに実行したい関数を渡してあげます。Walk
に渡せる関数は下記の型を持っています。type NodeVisitor func(node *Node, entering bool) WalkStatus今回作ったツールでは
genFmtWalker
という自作関数をWalk
に渡しています。MarkdownのASTをトラバースしていき、Goのコードブロックを見つけたらGoが標準で用意してくれているフォーマット用関数のformat.Source
に渡しています。func genFmtWalker(md *[]byte, fmterr *error) blackfriday.NodeVisitor { return func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if isGoCodeBlock(node) { fmted, err := format.Source(node.Literal) if err != nil { *fmterr = err return blackfriday.Terminate } *md = bytes.ReplaceAll( *md, bytes.TrimRight(node.Literal, "\n"), bytes.TrimRight(fmted, "\n"), ) } return blackfriday.GoToNext } } func isGoCodeBlock(node *blackfriday.Node) bool { return node.Type == blackfriday.CodeBlock && string(node.CodeBlockData.Info) == "go" }
genFmtWalker
はクロージャになっておりNodeVisitor
の型を満たす関数を返しています。なぜこんな複雑な形になったかというとgopkg.in/russross/blackfriday.v2
は Markdown の AST から Markdown に戻す機能がないからです。genFmtWalker
で Markdown のバイト列のポインタを受け取って、ポインタの指す先をWalk
内で Replace するようにしています。
Walk
はエラーを返さないためエラーも同様に処理しています。error があった場合はblackfriday.Terminate
というステータスを返してトラバースを中断します。もっといい実装があるかも。。ここまで紹介した関数を使って下記のように Markdown内のGoのコードをフォーマットした結果を返す関数が完成しました。主な実装はこれだけです。
// FmtGoCodeInMarkdown formats go code in Markdown. // returns error if code has syntax error. func FmtGoCodeInMarkdown(md []byte) ([]byte, error) { var err error n := blackfriday.New(blackfriday.WithExtensions(blackfriday.FencedCode)).Parse(md) // Walk内でformatしていく為、元のMarkdwonのポインタを渡してあげる。 n.Walk(genFmtWalker(&md, &err)) if err != nil { return nil, err } return md, nil }これを main から読んであげれば Markdown に埋め込まれた Go のソースコードに gofmt をかけてくれる CLI の完成です。
まとめ
Go の AST をトラバースする感覚で Markdown を扱えました。この要領で Markdown 内の Dockerfile のフォーマットなんかも実装できるかもしれません。何かバグがあったら教えてください!!
- 投稿日:2019-11-12T11:51:33+09:00
aws-lambda-go/cmd/build-lambda-zipをgo get時のエラー
何をしていたか
此方の記事を参考に、sam cliにてgolang, API gateway を試用していたところ、エラーに遭遇しました。
環境
- windows10 Pro
>sam --version SAM CLI, version 0.31.0詳細
ローカル実行は問題無し。API gatewayからRequestしたところ、502エラーが出ました。
もろもろの記事を見ると、
build-lambda-zip
をbuild時に実行し、zipファイルをS3へuploadすればよさそうです。build: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o hello-world/hello-world ./hello-world ${GOPATH}\bin\build-lambda-zip.exe -o hello-world/hello-world.zip hello-world/hello-worldこちらの
build-lambda-zip.exe
をinstall。
- https://github.com/aws/aws-lambda-go#for-developers-on-windows
go.exe get -u github.com/aws/aws-lambda-go/cmd/build-lambda-zip
エラー。
>go.exe get -u github.com/aws/aws-lambda-go/cmd/build-lambda-zip # github.com/aws/aws-lambda-go/cmd/build-lambda-zip src\github.com\aws\aws-lambda-go\cmd\build-lambda-zip\main.go:19:17: cannot use cli.StringFlag literal (type cli.StringFlag) as type cli.Flag in array or slice literal: cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver)解決策
- v2ブランチである
github.com/urfave/cli
が不安定なので、v1をinstallすればよいとのこと。- https://github.com/urfave/cli/issues/459
go get gopkg.in/urfave/cli.v1
src\github.com\aws\aws-lambda-go\cmd\build-lambda-zip\main.go
を下記のように変更go.exe get -u github.com/aws/aws-lambda-go/cmd/build-lambda-zip
import ( "archive/zip" "errors" "fmt" "io/ioutil" "os" "path/filepath" // "github.com/urfave/cli" "gopkg.in/urfave/cli.v1" )これで問題なくexeファイルが作成されました。
補遺
最後に、makeファイルを変更し、zipファイルをuploadしてあげます。
build: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o hello-world/hello-world ./hello-world ${GOPATH}\bin\build-lambda-zip.exe -o hello-world/hello-world.zip hello-world/hello-world package: sam package --template-file template.yaml --output-template-file output-template.yaml --s3-bucket sam-template-store-dev-kubosuke --profile ***** deploy: sam deploy --template-file output-template.yaml --stack-name joyfru-news-dev-kubosuke --capabilities CAPABILITY_IAM --profile *****
- 投稿日:2019-11-12T09:23:12+09:00
【Go】コーディングTips
はじめに
Goを書いていて便利だと思ったコーディング方法を溜めていきます。
構造体をフィールド名付きでPrintする
ただ
fmt.Println
するとフィールド名が付かないので見づらいときに。fmt.Printf("%+v\n", data){name:山田太郎 age:23}長さがNの文字列を生成する
文字数制限のメソッドをユニットテストしたいときに。
「1000文字も手打ちできないよ〜」みたいなときに。
ちなみに長さは指定した分になるけど、値は空っぽ。str := string(make([]rune, 1000))ある文字をN個生成したい
デバック中、見たいログを目立たせるときに。
str := strings.Repeat("+", 100)nilを代入しても元の型情報は保持される
型も値も同じなのに何故かユニットテストでFAILするときみたいなときはコレを疑って。
結論、 関数の返り値は型情報を持たせたままnilを返さない方が良い。s := make([]string, 0) fmt.Printf("type = %T | value = %v\n", s, s) s = nil fmt.Printf("type = %T | value = %v\n", s, s) // type = []string | value = []nilはキャストできる
わざわざnilをキャストしてnilかどうかなんで判定したくないはず。
関数の返り値は型情報を持たせたままnilを返さない方が良い。
i := new(int) i = nil fmt.Println(reflect.TypeOf(i) == nil) // false fmt.Println(i == (*int)(nil)) // true明示的にnilを返す。
// bad func do() something { // something something = nil return something } // good func do() something { // something return nil }簡単にバイトを割り当て
有名なやつ。
いちいち手打ちしなくても簡単に値をいれてくれる。
iota
の初期値は0
なので+1
。バイトじゃなくてもステータスを列挙して値に数字を入れたいときに。
type byteSize uint64 const ( KB byteSize = 1 << (10 * (iota + 1)) MB GB )