- 投稿日:2019-08-16T17:43:24+09:00
golangで始めるTDD
【この記事の目的】
golang x TDDは「あ、こんな風にやればええんやな」という感覚を掴んでもらう為の記事です。
【対象とする人】
- golangこれから学ぶぞ
- golangちょっとだけしってる
- テストコードってどう書けばいいんだろう
- TDDやったことない
という人を対象としてます
【やってみよう】
< golang基礎編 >
□ Hello World
言語学習の事始めと言ったらやっぱり「Hello World」ですよね!
golangでも例にもれず、まずはHello Worldでどんな感じなのかを確認することができます。ご存知の方も多いとお思いますが、環境準備をしなくてもPlaygroundで試すことができるので
簡単な挙動の確認くらいだったら検証できます。
早速Playgroundに行って、「Hello World」を実行してみましょう!hello.gopackage main import "fmt" func main() { fmt.Println("Hello, 世界") }□ 環境準備
まずはgoのコードを入れましょう。
↓の公式サイトにアクセスをしてβ版ではない最新のバージョンをダウンロードしてセットアップします。
https://golang.org/dl/golangをセットアップし終わったら、次は「エディタ」を準備します。
個人的なオススメは「VS Code」です。
https://code.visualstudio.com/軽量かつ様々なプラグインがあるので、デバッグがしやすく扱いやすいです。
今回はこの「VS Code」と「プラグイン」を用いることを前提で話を進めていきます。プラグインは↓の2つをインストールします。
プラグイン1: Go
用途:vs codeのgolangサポート
プラグイン2: Go Text Explorer
用途:テストに利用
□ 基礎構文
環境も準備できたので、あとは実際に手を動かすばかりではありますが、その前にgolangの基本構文を理解しましょう。
公式に動かして理解する素晴らしい資料があるので、ここでは割愛します。https://go-tour-jp.appspot.com/welcome/1
□ golangでのテストコード
ここでは詳しいgo testの仕様などは説明しませんが、
とりあえずテストコードが書けるように↓の3つだけざっくりと説明したいと思います。
- ファイルの名前は「xxx_test.go」で対応させtestingパッケージを利用する
- 標準関数で評価は十分できる
- Table Driven Testが推奨されている
※ go testの詳しい概要については↓のような素晴らしい記事を参考にしてください。
https://golang.org/pkg/testing/
https://budougumi0617.github.io/2018/08/19/go-testing2018/
https://swet.dena.com/entry/2018/01/16/211035ファイルの名前は「xxx_test.go」で対応させtestingパッケージを利用する
大原則となりますが、テストコードを記述するファイルは
xxx_test.goという(xxxはテスト対象のコードがあるファイル名が一般的)名前をつけなくてはなりません。
この名前にすることで「go test」を実行する時にテストコードとして認識してくれます。
xxxはテスト対象のコードがあるファイル名と関係ないものでも動きますが、同じ名前をつけるのが一般的なので敢えて変える必要はないかと思います。buildOK.sh$ ls main.go main_test.go $ go test PASS ok _/home/debug/test 0.007s $ go build $ ls main.go main_test.go testbuildNG.sh$ ls aaa.go main.go $ go test ? _/home/debug/test [no test files] $ go build $ ls aaa.go main.go testまた、テストを実行するのに「 testing 」というパッケージを利用します。
テストを実行して色々確認したりするのに便利なメソッドがまとまってますので、どんなことができるかはReferenceを確認しておくとよいでしょう。
ちなみにtestingパッケージを使う場合は他にも注意点があります。
テストとして実行するメソッドは必ずTestXxxという命名規則で書かなくてはなりません。
なので記述するとしたらこんな感じになります。※ Test以降の文字列もキャメルケースである必要があります
example.gopackage main import ( "testing" ) func TestXxx(t *testing.T) { ... }標準関数で評価は十分できる
javaなどではassert()を利用して、期待との評価を行うのが一般的かと思いますが
golangでは以下のように評価するケースが多いです。
assert()を利用しなくても十分評価できるので、goに入ってはgoに従えということでこの形式でテストは評価します。https://play.golang.org/p/jUjXNAsmkxD
plus_test.gopackage main import ( "testing" ) func Plus(x, y int) int { return x + y } func TestPlus(t *testing.T) { x, y := 1, 1 expectation := 3 res := Plus(x, y) if res != expectation { t.Errorf("Not equal value %v", res) } }↑を実行するとこんな結果が得られます。
どこで失敗したかなどわかりやすいですね。=== RUN TestPlus --- FAIL: TestPlus (0.00s) prog.go:17: Not equal value 2 FAILTable Driven Testが推奨されている
すごくざっくりいうと、「 テストケースをJSONっぽい見た目のテーブルで書くとわかりやすくない? 」という感じの書き方です。
ちゃんとした説明や詳しい内容は参考資料や公式を見てください。
ここではざっくりこんな感じの書き方をするんだなーというところだけ掴んでくれればいいので、実例を載せます。https://play.golang.org/p/sc7HkbaxA4D
plus_test.gopackage main import ( "testing" ) func Plus(x, y int) int { return x + y } func TestPlus(t *testing.T) { data := map[string]struct { X int Y int Expectation int }{ "正の整数+正の整数": { X: 1, Y: 1, Expectation: 2, }, "正の整数+負の整数": { X: 1, Y: -1, Expectation: 0, }, } for name, d := range data { t.Run(name, func(t *testing.T) { res := Plus(d.X, d.Y) if res != d.Expectation { t.Errorf("Not equal value %v", res) } }) } }↑を実行するとこんな結果が得られます。
=== RUN TestPlus === RUN TestPlus/正の整数+正の整数 === RUN TestPlus/正の整数+負の整数 --- PASS: TestPlus (0.00s) --- PASS: TestPlus/正の整数+正の整数 (0.00s) --- PASS: TestPlus/正の整数+負の整数 (0.00s) PASSさらっとサブテストの実行までやってしまってますが、この書き方をするとどのケースでどういうデータがセットされているかがひと目でわかりやすいので複数ケースを行う場合はおすすめです。
< TDD基礎編 >
□ TDDの基本
さて、ようやくこれでTDDに入っていきます。
TDDも色々わかりやすい説明をしている記事やt_wadaさんというTDDの現人神のような方がいらっしゃるので、詳しくはそちらを見てください。なので、ここではほんとに入り口だけを紹介します。
TDDは「 まずは作ろう! 」ではなく「 これはどんなことができる子かな? 」という完成品が叶えるべき要件をベースにしてテストケースから考えていく開発手法です。
なので、個人的にはこの「 テストケースを作る 」という段階が重要になってくると考えています。そして、テストケースが作り終わったら「 3つのサイクルを回して 」開発していきます。
-Red(失敗させて)
-Green(エラーが出ないようにして)
-リファクタリング(正しい形に直していく)このサイクルを回していくと要件を満たしたことが担保された動くコードが出来上がっていきます。
< golang x TDD >
基本的なやり方は以前書いたエアペアプロにある通りになります。
https://qiita.com/i-dach/items/20b3460f736fb93c53c4#%E3%82%A8%E3%82%A2%E3%83%9A%E3%82%A2%E3%83%97%E3%83%AD%E3%81%AE%E3%82%84%E3%82%8A%E3%81%8B%E3%81%9FTDDのサイクル通りに
-「テストケース」を作り、
-「golangで空のテストを実行してエラーにさせて」
-「3つのサイクルをぐるぐると回していく」をひたすら行うだけです。
【まとめ】
golangでTDDをやるとしても特別なことは特にありません。
golangでのテストの書き方とTDDのススメ方さえ把握してれば怖くないので、是非トライしてみましょう!
- 投稿日:2019-08-16T17:28:43+09:00
golangとjavascriptで日めくりカレンダー的なのを作った。
やりたかったこと
1・1か月ごとのカレンダーを表示
2・スケジュールの入力ができる(todoアプリ的な)
3・タスクがすべて完了したらマスの色を変える。
4・一か月前、一か月後まではボタンで切り替えて表示ここまでを約1週間で行いたかった。
実際は?
現在の年月日、曜日を表示、時刻はデジタル時計のように、リアルタイムで更新するようにしました。
日めくりカレンダーのようにしました。使用言語
golang,javascript,html,cssフォルダ構成
calender |_resources | |_index.html | | | |_styles.css |_hello.go苦戦したところ
goで定義したもの(現在日時など)をどうやってHTMLに埋め込むか
CSSファイルを読み取ってくれなかった。
この二つが特に悩まされた。後はgoにこだわらないでほかの言語なら行けたんじゃね?と思う
(Goを必ず使わなくてはいけなかったのでそこのさじ加減が掴めませんでした)どうやったか
hello.gopackage main import ( "fmt" "html/template" "net/http" "time" ) func main() { fmt.Println("The Server runs with http://localhost:3000/") http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/")))) http.HandleFunc("/", Handler) http.ListenAndServe(":3000", nil) } type Time struct { Year int Month string Day1 int Weekday string Hour int Minute int Second int } func Handler(w http.ResponseWriter, r *http.Request) { t := Time{ Year: time.Now().Year(), Month: time.Now().Month().String(), Day1: time.Now().Day(), Hour: time.Now().Hour(), Minute: time.Now().Minute(), Second: time.Now().Second(), Weekday: time.Now().Weekday().String(), } tmpl := template.Must(template.ParseFiles("./resources/index.html")) tmpl.Execute(w, t) }苦戦した箇所である、html、cssを読み込む処理は、
tmpl := template.Must(template.ParseFiles("./resources/index.html"))
tmpl.Execute(w, t)
読み込みたいファイルのパスを指定してあげて、
上で定義した変数を引数にして一緒に実行するようにしました。
これでhtml上にgoを埋め込むことに成功。cssの読み込みは、
http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
この一文でresourcesフォルダ以下の静的ファイルを探してきてくれ、読み込みができるようになりました。index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>テスト</title> <link rel="stylesheet" type="text/css" href="../resources/styles.css"> <script> function digitalClock(){ // 現在日時を取得 var now = new Date(); // 「時」を取得 var hour = now.getHours(); // 「分」を取得 var minute = now.getMinutes(); // 「秒」を取得 var second = now.getSeconds(); // 0埋めで2桁表示にする 例)08:45:09 if(hour < 10) hour = "0" + hour; if(minute < 10) minute = "0" + minute; if(second < 10) second = "0" + second; // 現在時刻を表示する var elem = document.getElementById("clock"); elem.innerHTML = hour + ":" + minute + ":" + second; // 500ミリ秒後に再帰呼び出し setTimeout(digitalClock, 500); } window.onload = function(){ digitalClock(); } </script> </head> <body> <div class="calenderbox"> <pre><h1 class="month_now"><span>{{.Year }}</span></h1></pre> <div id ="box1"> <div id="box2"> <p class="Date">{{.Month }}</p> <p class="Date">{{.Day1 }}</p> <p class="Date">{{.Weekday }}</p> </div> <div id="box3"> <p id="clock"></p> </div> </div> </div> </body>hello.goで処理したので、goの変数を以下のような形で受け取ることができます。
{{.変数名 }}
また、内で時刻を取得し、デジタル時計の見た目に設計したものをhtmlで返しています。
cssファイルは特に変わったことをしていないので割愛させて頂きます。
まとめ
触ったことがないものばかりだったのでここまでできたのも奇跡に等しいです。
ただ、まだ追加したい機能やより良い記述があるはずなのでしばらくこれを継続して取り組みます。
Goはとりあえずチュートリアルを完走します。
あとはjavascriptはじめてまともに使ってマスターしたらかなり便利だと感じました。
こちらもしっかり取り組んでいきます。
- 投稿日:2019-08-16T17:06:38+09:00
mongo-go-driverがうまく入らなかったので、手動でいれた。
概要
mongo-go-driverをDockerfileなり、自動で入れようとしたら上手くいかず、手動でいれるとうまくいったので、その共有。
誰かが同じ轍を踏まないように。mongo-go-driver
Go Runtimeコンテナ
base image => golang:1.12.8 ※2019/08/15時点でのstable versionアタッチして下記を実行
パッケージ管理ツール: depのインストール
$ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | shProjectの初期化
$ dep init ※project rootでMongo Driverのインストール
$ dep ensure --add go.mongodb.org/mongo-driver/mongo go.mongodb.org/mongo-driver/bson go.mongodb.org/mongo-driver/mongo/options※goファイルでimportしていない場合、「これはまだ仮のインストールでっせ」という警告が出るかもしれませんが、
importすれば、消えるので無視でOKあとはdocker commitなりなんなり。
参考
- Github: mongo-go-driver
- MongoDB公式ドキュメント
- 投稿日:2019-08-16T16:49:14+09:00
Go言語のバージョンを切り替え方法 macOS HomeBrew
モチベーション
go言語でビルドしていて、バージョンがマッチしていないとエラーになった。macOSのコンパイル環境で、go言語のバージョンを簡単に変更する方法は無いだろうか?
# math/bits compile: version "go1.12.8" does not match go tool version "go1.12.9" # errors compile: version "go1.12.8" does not match go tool version "go1.12.9" # runtime/internal/sys compile: version "go1.12.8" does not match go tool version "go1.12.9" # unicode/utf8 compile: version "go1.12.8" does not match go tool version "go1.12.9" # internal/cpu compile: version "go1.12.8" does not match go tool version "go1.12.9"HomeBrew を使ってバージョンを確認
go言語のパッケージの詳細を見ると、どうも、切り替えられそうだ...
$ brew info go go: stable 1.12.9 (bottled), HEAD Open source programming language to build simple/reliable/efficient software https://golang.org /usr/local/Cellar/go/1.12.8 (9,816 files, 452.7MB) Poured from bottle on 2019-08-15 at 22:49:03 /usr/local/Cellar/go/1.12.9 (9,819 files, 452.8MB) * Poured from bottle on 2019-08-16 at 16:23:19 From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/go.rb ==> Requirements Required: macOS >= 10.10 ✔ ==> Options --HEAD Install HEAD version ==> Analytics install: 84,842 (30 days), 275,543 (90 days), 1,163,983 (365 days) install_on_request: 65,340 (30 days), 211,527 (90 days), 863,599 (365 days) build_error: 0 (30 days)brew switch で切り替え
調べたところ、brew switch という機能があるらしい。
そこで、先ほど、リストされていたバージョンを指定してみる。$ brew switch go 1.12.8 Cleaning /usr/local/Cellar/go/1.12.8 Cleaning /usr/local/Cellar/go/1.12.9 3 links created for /usr/local/Cellar/go/1.12.8どうやら、切り替わったみたいだ
$ brew info go go: stable 1.12.9 (bottled), HEAD Open source programming language to build simple/reliable/efficient software https://golang.org /usr/local/Cellar/go/1.12.8 (9,816 files, 452.7MB) * Poured from bottle on 2019-08-15 at 22:49:03 /usr/local/Cellar/go/1.12.9 (9,819 files, 452.8MB) Poured from bottle on 2019-08-16 at 16:23:19 From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/go.rb ==> Requirements Required: macOS >= 10.10 ✔ ==> Options --HEAD Install HEAD version ==> Analytics install: 84,842 (30 days), 275,543 (90 days), 1,163,983 (365 days) install_on_request: 65,340 (30 days), 211,527 (90 days), 863,599 (365 days) build_error: 0 (30 days)確認結果
確かに、go言語のバージョンが切り替わった
$ go version go version go1.12.8 darwin/amd64
- 投稿日:2019-08-16T16:49:14+09:00
Go言語のバージョンを切り替える方法 macOS HomeBrew
モチベーション
go言語でビルドしていて、バージョンがマッチしていないとエラーになった。macOSのコンパイル環境で、go言語のバージョンを簡単に変更する方法は無いだろうか?
# math/bits compile: version "go1.12.8" does not match go tool version "go1.12.9" # errors compile: version "go1.12.8" does not match go tool version "go1.12.9" # runtime/internal/sys compile: version "go1.12.8" does not match go tool version "go1.12.9" # unicode/utf8 compile: version "go1.12.8" does not match go tool version "go1.12.9" # internal/cpu compile: version "go1.12.8" does not match go tool version "go1.12.9"HomeBrew を使ってバージョンを確認
go言語のパッケージの詳細を見ると、どうも、切り替えられそうだ...
$ brew info go go: stable 1.12.9 (bottled), HEAD Open source programming language to build simple/reliable/efficient software https://golang.org /usr/local/Cellar/go/1.12.8 (9,816 files, 452.7MB) Poured from bottle on 2019-08-15 at 22:49:03 /usr/local/Cellar/go/1.12.9 (9,819 files, 452.8MB) * Poured from bottle on 2019-08-16 at 16:23:19 From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/go.rb ==> Requirements Required: macOS >= 10.10 ✔ ==> Options --HEAD Install HEAD version ==> Analytics install: 84,842 (30 days), 275,543 (90 days), 1,163,983 (365 days) install_on_request: 65,340 (30 days), 211,527 (90 days), 863,599 (365 days) build_error: 0 (30 days)brew switch で切り替え
調べたところ、brew switch という機能があるらしい。
そこで、先ほど、リストされていたバージョンを指定してみる。$ brew switch go 1.12.8 Cleaning /usr/local/Cellar/go/1.12.8 Cleaning /usr/local/Cellar/go/1.12.9 3 links created for /usr/local/Cellar/go/1.12.8どうやら、切り替わったみたいだ
$ brew info go go: stable 1.12.9 (bottled), HEAD Open source programming language to build simple/reliable/efficient software https://golang.org /usr/local/Cellar/go/1.12.8 (9,816 files, 452.7MB) * Poured from bottle on 2019-08-15 at 22:49:03 /usr/local/Cellar/go/1.12.9 (9,819 files, 452.8MB) Poured from bottle on 2019-08-16 at 16:23:19 From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/go.rb ==> Requirements Required: macOS >= 10.10 ✔ ==> Options --HEAD Install HEAD version ==> Analytics install: 84,842 (30 days), 275,543 (90 days), 1,163,983 (365 days) install_on_request: 65,340 (30 days), 211,527 (90 days), 863,599 (365 days) build_error: 0 (30 days)確認結果
確かに、go言語のバージョンが切り替わった
$ go version go version go1.12.8 darwin/amd64
- 投稿日:2019-08-16T15:25:10+09:00
Windows環境 Goのrealizeでハマった話
いきなりDocker化すると、動きがわからず、
ハマる可能性大なので一回ローカルで試すのが吉と思うコンパイルの出力先ファイル名になんでもいいから拡張子つけないとダメ。
windowsなので、exeをつけた。commands: install: status: true method: go build -o app.exe run: status: true method: ./app.exeそうしないと。このエラー
$ realize start [15:10:53][GO_DEMO2] : Watching 2 file/s 0 folder/s [15:10:53][GO_DEMO2] : Install started [15:10:57][GO_DEMO2] : Install completed in 3.978 s [15:10:57][GO_DEMO2] : Running.. panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x0 addr=0x8 pc=0x4d006d] goroutine 52 [running]: os.(*Process).signal(0x0, 0xa58520, 0xd9a4c0, 0xc000214020, 0xc00007af70) c:/go/src/os/exec_windows.go:59 +0x2d os.(*Process).Signal(...) c:/go/src/os/exec.go:131 github.com/oxequa/realize/realize.(*Project).run.func1(0xc00007b6a8) C:/Users/N10W78/go/src/github.com/oxequa/realize/realize/projects.go:581 +0x64 github.com/oxequa/realize/realize.(*Project).run(0xc00005e300, 0xc000019650, 0x5, 0xc00020a180, 0xc000058420, 0xa53d40, 0xc0002020c0) C:/Users/N10W78/go/src/github.com/oxequa/realize/realize/projects.go:646 +0xc62 github.com/oxequa/realize/realize.(*Project).Reload.func3(0xc00005e300, 0xc00020a180, 0xc000058420) C:/Users/N10W78/go/src/github.com/oxequa/realize/realize/projects.go:262 +0x14e created by github.com/oxequa/realize/realize.(*Project).Reload C:/Users/N10W78/go/src/github.com/oxequa/realize/realize/projects.go:260 +0x29eまた、Goでwebサーバしている場合、プロセスを殺してやらないと、実行ファイルが上書きされず
ホットリロードされなかった。
macやlinuxならいけるのだろうか。。。今後調査scripts: - type: before command: taskkill /F /im app.exerealize.yml サンプル
settings: legacy: force: true interval: 0s schema: - name: realize path: . args: - main.go commands: install: status: true method: go build -o app.exe run: status: true method: ./app.exe watcher: extensions: - go paths: - / scripts: - type: before command: taskkill /F /im app.exe ignored_paths: - .git - .realize - vendor
- 投稿日:2019-08-16T15:18:46+09:00
【Golang】フィボナッチ数列の値を積み上げ式で計算すると早い話
leetcodeでアルゴリズムを勉強し始めたので初投稿です。
問題
n番目のフィボナッチ数列の値を算出する
直感的に解く
fibonacci.gofunc Fibo(n int) int { if n < 2 { return n } return Fibo(n-2) + Fibo(n-1) }n=4だったら、
Fibo(4) = Fibo(2) + Fibo(3)
Fibo(4) = Fibo(0) + Fibo(1) + Fibo(1) + Fibo(2)
Fibo(4) = 0 + 1 + 1 + Fibo(0) + Fibo(1)
Fibo(4) = 0 + 1 + 1 + 0 + 1 = 3
って感じで計算が進む。計算が重複してるので遅い。積み上げ式
fibonacci.gofunc Fibo2(n int) int { x, y := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x }n=4なら、
x,y = 1,1
x,y = 1,2
x,y = 2,3
x,y = 3,5
で 3が返される。計算重複しないので早い。どのくらい早いのか
benchmarkで計測。
n=0..40まで計算させて、時間を比較してみた。
テストはてきとーにこんな感じで書いてみるfibonacci_test.gofunc BenchmarkFibo(b *testing.B) { for i := 0; i < 40; i++ { Fibo(i) } } func BenchmarkFibo2(b *testing.B) { for i := 0; i < 40; i++ { Fibo2(i) } }結果は
Fibo(再帰のやつ) => 1.356s
Fibo2(積み上げ式) => 0.008s早すぎて草。真面目にアルゴリズム勉強しようと思った。
行列を使うともっと早くなるらしい。
もっと早い方法知っていたら教えてください。
- 投稿日:2019-08-16T15:18:46+09:00
【Golang】フィボナッチ数列をメモ化や積み上げ式で解く
leetcodeでアルゴリズムを勉強し始めたので初投稿です。
問題
n番目のフィボナッチ数列の値を算出する
直感的に解く
fibonacci.gofunc Fibo(n int) int { if n < 2 { return n } return Fibo(n-2) + Fibo(n-1) }n=4だったら、
Fibo(4) = Fibo(2) + Fibo(3)
Fibo(4) = Fibo(0) + Fibo(1) + Fibo(1) + Fibo(2)
Fibo(4) = 0 + 1 + 1 + Fibo(0) + Fibo(1)
Fibo(4) = 0 + 1 + 1 + 0 + 1 = 3
って感じで計算が進む。計算が重複してるので遅い。積み上げ式
fibonacci.gofunc Fibo2(n int) int { x, y := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x }n=4なら、
x,y = 1,1
x,y = 1,2
x,y = 2,3
x,y = 3,5
で 3が返される。計算重複しないので早い。メモ化で解く
fibonacci.gofunc Memorize(n int, memo map[int]int) int { if n < 2 { return n } if _, ok := memo[n]; !ok { memo[n] = Memorize(n-2, memo) + Memorize(n-1, memo) } return memo[n] }マップに保存して、すでに計算してる場合はマップから取り出して使うようにする。
どのくらい早いのか
benchmarkで計測。
n=0..40まで計算させて、時間を比較してみた。
テストはてきとーにこんな感じで書いてみるfibonacci_test.gofunc BenchmarkFibo(b *testing.B) { for i := 0; i < 40; i++ { Fibo(i) } } func BenchmarkFibo2(b *testing.B) { for i := 0; i < 40; i++ { Fibo2(i) } } func BenchmarkFibo3(b *testing.B) { memo := make(map[int]int) for i := 0; i < 40; i++ { Memorize(i, memo) } }結果は
Fibo(ふつうのやつ) => 1.356s
Fibo2(積み上げ式) => 0.008s
Fibo3(メモ化) => 0.008s早すぎて草。真面目にアルゴリズム勉強しようと思った。
行列を使うともっと早くなるらしい。
もっと早い方法知っていたら教えてください。
- 投稿日:2019-08-16T13:50:09+09:00
超訳Effective Goその1
重要だからEffective Go読めよ。
でもダラダラ書いてあるから超訳してみた。とりあえずControl structures(制御構造)まで。
イントロダクション
Goで実装するなら他の言語のことは一旦忘れろ。既存コードはそのまま移植しようとしても上手くいかない。
Goに入ってはGoに従えサンプル
Goのパッケージのソースは良サンプルだからそのまま手本にしろ。
フォーマッティング
gofmtしろ。
コメント
C++スタイルだ。
/* comment */// comment
トップレベルの宣言の直前行に書いたコメントはそのままgodocでドキュメントになるぞ。
最初の行は宣言名で始まる1行サマリーを書くんだ。
英語のプレーンテキストで書くのがベストだ。余計な書式タグとかはいらないぞ。名前
Go命名規則は重要な意味がある(例えば頭大文字でパッケージ外に公開)から手間を惜しむなよ。
ちなみに名前は短いほうがいい。超絶長い名前付けるくらいならドキュメントを書くんだ。パッケージ名
パッケージ名はそのソースファイルのディレクトリ名だ。
名前は小文字の1単語にしろ。
万が一インポートするパッケージ名が衝突しても使うときに選択可能だし、実際混乱することなんて稀だ。
src/encoding/base64ににあるソースのパッケージ名はbase64でimport ( "encoding/base64" "fmt" ) func main() { str := base64.StdEncoding.EncodeToString([]byte("data")) fmt.Println(str) }ってな感じで使うぞ。
ゲッター
Goにゲッター、セッター的な規約はないが実装しても構わない。
ゲッターはエクスポートされていないownerフィールドに対して大文字で始まるOwner()って書いてエクスポートされたフィールドっぽくアクセスできるようにするんだ。
セッターのほうはSetOwner()でいいぞ。owner := obj.Owner() if owner != user { obj.SetOwner(user) }インターフェース名
one-method interfaces(メソッドが1個しないインターフェース)は、メソッド名に接尾辞「er」つけてインターフェース名にするんだ。
Reader、Writer、Formatter、CloseNotifierこういうやつ。
Read、Write、Close、Flush、Stringみたいなメソッドは上記のような既知のone-method interfaceがあるから、そのインターフェースを使えるように命名することも大事だ。例えば文字列として出力するメソッド名はtoString()じゃなくてString()にしてStringerとして扱えるようにするんだ。
既知のインターフェースと別の用途で使うなら被らない別の名前にしてくれ。大文字混合
Goではスネークケース
snake_casing的なやつじゃなくて、いわゆるキャメルケースcamelCasingとかパスカルケースPascalCasingってやつを使うんだ。セミコロン
いらん。
正確には改行直前のトークン後にコンパイラが自動的にセミコロンを入れているんだ。
だから、{の前とかに改行入れると不要なセミコロン扱いでコンパイルエラーになるぞ。if i < f() // wrong! ; { // wrong! g() }制御構造
Cに似てるがいろいろ違うぞ。
doやwhileはないifとswitchはいろいろ初期化できるbreakとcontinueはラベルを使って抜ける場所を決められるselectっていうのが増えたif
カッコつけんじゃない。
if x > 0 { return y }もちろん
returnやbreakで制御ブロックから抜けられる。
forみたいにローカル変数として初期化できる。if err := file.Chmod(0664); err != nil { log.Print(err) return err }
break、continue、goto、returnで終了するんだったらelseは省略して次の処理を書いていい。f, err := os.Open(name) // err宣言 if err != nil { return err } d, err := f.Stat() // d宣言、err再割当て(代入) if err != nil { f.Close() return err } codeUsing(f, d)再宣言と再割り当て
余談だが、前項の例で
errが2回宣言されているように見えるが問題ない。2回目はスコープそのままに普通の代入だ。
:=で複数宣言するときは、新しく宣言する変数が最低1つあればいい。for
Goの
forループはCと似て非なるものだ。Cのwhileとかもforに統合されていて、3つの形式があるぞ。// ふつうのCっぽいfor for 初期化; 条件; ループ後 { } // whileっぽいやつ for 条件 { } // for(;;)っぽいやつ 無条件ループ for { }インデックス変数はループ初期化で簡単に宣言できるぞ。
sum := 0 for i := 0; i < 10; i++ { sum += i }
array、slice、string、map、またはchanから読取る場合はrangeを使ってループするぞ。for key, value := range oldMap { newMap[key] = value }
rangeでキーだけが必要なら代入する変数1個だけにしたらいいぞ。for key := range m { if key.expired() { delete(m, key) } }値の方だけ必要なら、1番目の変数を
_(blank identifier)にしてキーを無視すればいい。
_(blank identifier)はよく使うので後で説明する。sum := 0 for _, value := range array { sum += value }文字列の場合は
range使うといろいろ(UTF-8を解析して個々のUnicodeコードポイントに分割、誤ったエンコードはU+FFFDに置換)やってruneを生成してくれるぞ。
(runeの詳細は言語仕様をみてくれ)for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding fmt.Printf("character %#U starts at byte position %d\n", char, pos) } prints character U+65E5 '日' starts at byte position 0 character U+672C '本' starts at byte position 3 character U+FFFD '�' starts at byte position 6 character U+8A9E '語' starts at byte position 7forで複数の式は使えないから、複数の変数を実行する場合は1つの式で複数割当るんだ。
// Reverse a for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] }switch
switchの式はCのと違って定数か整数じゃなくてもいいぞ。
caseは上から順にswitchの式と一致するまで評価されるが、switchに式が無いときは上から順にcaseがtrueになるまで評価される。
これはif-else-if-elseチェーンをswitchで書けるってことだ。func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 }複数caseをフォールスルーはしないんだけど、1つの
caseに複数の条件を書けるぞ。func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
breakステートメントを使ってswitchを抜けることができるけど、スイッチじゃなくて外のforループから抜け出したいときは、forループにラベル付けをしておいて、そのラベルをbreakしてやればいい。
もちろんcontinueステートメントもラベルが使えるぞ。Loop: for n := 0; n < len(src); n += size { switch { case src[n] < sizeOne: if validateOnly { break // switchを抜ける } size = 1 update(src[n]) case src[n] < sizeTwo: if n+1 >= len(src) { err = errShortInput break Loop // forを抜ける } if validateOnly { break // switchを抜ける } size = 2 update(src[n] + src[n+1]<<shift) } }最後に2つのswitchステートメントを使用する
bytesliceの比較ルーチンを書いておく。// Compare returns an integer comparing the two byte slices, // lexicographically. // The result will be 0 if a == b, -1 if a < b, and +1 if a > b func Compare(a, b []byte) int { for i := 0; i < len(a) && i < len(b); i++ { switch { case a[i] > b[i]: return 1 case a[i] < b[i]: return -1 } } switch { case len(a) > len(b): return 1 case len(a) < len(b): return -1 } return 0 }type switch
switchをつかうことでinterface{}の動的型を検出できるぞ。
switchの式で、type asertion構文を使って変数を宣言すると、caseでその型が評価できる**んだ。
このとき宣言する変数名は、元のinterface{}型の変数と同じ名前にするといい。その名前の変数が評価された型で使えるぞ。var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int }
- 投稿日:2019-08-16T13:16:02+09:00
Google Calendar API を使って、サンプルをもとに重複イベントの削除アプリを実装してみる
カレンダーの同期アプリや連携サービスを試していた関係上、イベントが100個ちかく量産されてしまっていました…
あまりにもうっとおしいので、重複したイベントを削除するアプリを作ろうと思い至りました。その過程でサンプルをもとにカスタマイズする箇所も結構あったりして、どうせなのでちゃんとまとめておこうと思います。
まずは、サンプルを動かす
https://developers.google.com/calendar/quickstart/go に動作するサンプルがあります。
まずは、これをもとにして、自分のカレンダーのイベントを一覧表示するところまでやってみます。Step1 ~APIの有効化~
なにはともあれ、Google のサービスにプログラムからアクセスするための情報を取得します。
Step1 でその処理が簡単に行なえます。本来であれば Google Developer Console で有効化するところを、簡略化してくれているようです。
「ENABLE THE GOOGLE CALENDAR API」ボタンを押します。
これで有効化されました。
ここで、「DOWNLOAD CLIENT CONFIGURATION」ボタンを押して、credentials.json という名前でプロジェクトディレクトリーに配置します。
ファイル名は複数形です。この名前のファイルを読み取るようなプログラムになっていますので、間違いのないように。Step2 ~go get~
プロジェクトディレクトリーで、必要なパッケージを取得します。
過去に同パッケージを取得している場合でも、Go のバージョンを上げたり、パッケージの更新をしていないと、「undefined: proto.ProtoPackageIsVersion3」というエラーが出たりするので、ちゃんと実行しておきましょう。
go get -u google.golang.org/api/calendar/v3 go get -u golang.org/x/oauth2/googleStep3 ~サンプルをコピペ~
変なアレンジはせず、まずは動かすことを優先します。
サンプルのページにある内容を quickstart.go として保存します。
この時点で、プロジェクトディレクトリーには、credentials.json と quickstart.go があるはずです。
Step4 ~動かす~
go run quickstart.go最初にコンソールに URL が出力されるので、それをブラウザのアドレスバーにコピペして、アプリのアクセスを承認します。
承認後、ブラウザにアクセスコードが表示されるので、今度はコンソールに貼り付けます。
すると、実行した日付以降の10個までのイベントが列挙されます。
その後
プロジェクトディレクトリーを見てみてください。
token.json というファイルが作成されているはずです。これは、承認済みアプリに対して Google Calendar API にアクセスするための承認情報を記録したものです。
これがあれば、次回以降はアクセス承認をせずに API を実行できるようになります。サンプルを理解する
quickstart.go を参照して、取得可能なイベントの属性と、処理の流れを確認します。
(実際にソースコードを片手に読んでもらえると理解が用意になるかもしれません)承認の処理は複雑なので、その部分をカスタマイズするまでお預けです。(token.json がある現状、あまり触れなくても不便はありませんし)
イベント取得の基本形
まずは、アプリの骨子であるイベント取得の流れを理解します。
イベント取得の基本形は以下のようになります。
main()の中の、イベント取得の部分に注目しています。
- なにはともあれ、
calendar.Newで*calendar.Serviceを取得する。- イベントのリストは、
(*calendar.Service).List().(メソッドチェーン).Do()で取得する。events.Items(型[]*calendar.Event)で取得したイベントの情報を得る。イベントに対してできることは、API ドキュメントの https://godoc.org/google.golang.org/api/calendar/v3#EventsService を参照すると、大体どんなものがあるかわかると思います。
いずれの操作でも、Delete()やList()の後に条件をメソッドチェーンの形で指定し、最後にDo()を呼び出すと、その操作が実行できるようです。また、
List()に渡している"primary"という文字列ですが、これは承認したアカウントのデフォルトのカレンダーを意味する ID のようです。
複数のカレンダーを管理している場合など、別のカレンダーを指定したい場合は、ここを変更します。
カレンダーIDは、カレンダーごとの設定から確認できます。(概ね、hogehoge@group.calendar.google.comのような形式になっています)取得できるイベントの属性
上記の API ドキュメントをたどっていくと、取得できるイベントの属性が分かります。
これです→ https://godoc.org/google.golang.org/api/calendar/v3#Eventsただ、属性値自体が struct だったりして、たどっていくとキリがありません。
まあ、大体どんなものがあるかを把握するだけでいいと思います。以下余談
ここらへんは、アプリの形に応じてどの程度詳細なところまで取得するべきか考えます。
今回つくりたいもので言えば、重複のチェックのためにイベントの属性を使いたいので…
- 複数の属性を連結できる形に加工したい (連結した単一のキーすると扱いが楽)
- 本当に重複しているか、デバッグ出力したい
- 反対に、「特定の属性値を持つイベントを抽出する」ことは不要なので、人間が入力する手間は考えなくて良い
というのを満たせれば良いです。
というわけで、今回は文字列に変換することを念頭に置きます。
カスタマイズする
quickstart.go をカスタマイズして、今回やりたいことである重複したイベントを削除する機能を追加していきます。
ついでに、アプリとしての体を成すようにしていきます。重複したイベントを除去する
イベントから、キーとする属性値を文字列化する
こんな関数を用意して、
*calendar.Eventを文字列にするようにしました。↓引数
fieldsには、キーとする属性の名前を複数指定します。func UniqKey(e *calendar.Event, fields ...string) string { k := "" for _, f := range fields { switch strings.ToLower(f) { case "created": k += e.Created case "description": k += e.Description case "end": t := e.End.DateTime if t != "" { k += t } else { k += e.End.Date } case "etag": k += e.Etag case "hangoutLink": k += e.HangoutLink case "htmllink": k += e.HtmlLink case "icaluid": k += e.ICalUID case "id": k += e.Id case "location": k += e.Location case "start": t := e.Start.DateTime if t != "" { k += t } else { k += e.Start.Date } case "summary": k += e.Summary case "updated": k += e.Updated default: } } return k }重複の検出をする
重複検知の簡単な実装としては、map があります。
今回キーは文字列ですので、
map[string]struct{}を使います。uniqs := make(map[string]struct{}) : key := UniqKey(item, "Description", "Summary", "Start", "End") if _, found := uniqs[key]; found { fmt.Printf("[DEL] %v (%v)\n", item.Summary, date) // イベントを削除する } else { fmt.Printf("* %v (%v)\n", item.Summary, date) uniqs[key] = struct{}{} }重複した場合でも優先して残すべき条件がある場合は、
map[string]([]*calendar.Event)のようにしておくと、後で処理がしやすいでしょう。イベントを削除する
ここまでで
srv.Events.List()を使ってきましたが、削除はsrv.Events.Delete()を使います。
List()の場合と同様、Delete()で条件をしていして、最後にDo()を呼び出します。uniqs := make(map[string]struct{}) : key := UniqKey(item, "Description", "Summary", "Start", "End") if _, found := uniqs[key]; found { fmt.Printf("[DEL] %v (%v)\n", item.Summary, date) delevent := srv.Events.Delete("primary", item.Id) err = delevent.Do() if err != nil { fmt.Printf(" failed to delete: %v", err) } } else { fmt.Printf("* %v (%v)\n", item.Summary, date) uniqs[key] = struct{}{} }ただし、これだけでは削除できません。
ソースコード上でのスコープの変更が必要です。スコープを変更する
スコープは、いわば、承認時に申請しておく権限のようなものです。
ここになって少し承認の部分を変更します。
サンプルのコードでは、以下のようになっています。
config, err = google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)あからさまに削除等の操作ができないことがわかるスコープになっていますね。
API ドキュメントを
CalendarReadonlyScopeで検索するとわかりますが、イベントのみ操作可能なスコープや全体を変更可能なスコープがあります。
https://godoc.org/google.golang.org/api/calendar/v3#pkg-constantsこのスコープに応じて、承認時のメッセージが変わってきます。
いちいち考えるのが面倒なので何でもできそうなCalendarScopeにします。
アプリの用途を満たす最低限のスコープを指定するようにしてください。config, err = google.ConfigFromJSON(b, calendar.CalendarScope /*CalendarReadonlyScope*/)なお、上記のコード変更で承認時に必要な情報が変わっています。
今までのトークンは使えませんので、ファイル token.json は削除し、再度承認をします。承認をちょっとだけ自動化する
コピペが2回も必要なのは使い勝手が悪いので、できる範囲で自動化します。
サンプル quickstart.go の
getTokenFromWeb()の実装を変更します。承認URLをブラウザで表示する
これは非常に簡単で、外部のパッケージ
github.com/pkg/browserを使うだけでした。承認ページのURLは変数
authURLに格納されていますので、それを引数に上記パッケージの関数を呼び出します。authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) browser.OpenURL(authURL)これで、最初のコピペが不要になります。
次に、アクセスコードをアプリ(コンソール)にコピペする部分を自動化します。
アプリ内にHTTPサーバーを実装してアクセスコードを受け取る
承認の流れで(OAuth2)、アクセスコードを含むリクエストを受け取るためのリダイレクトURLを指定することができます。
今回はこの仕組みを使い、Google の承認ページからアプリがサーブするサイトにリダイレクトさせます。
まずは、
authURLを取得する前に、リダイレクト先を指定します。config.RedirectURL = fmt.Sprintf("http://localhost:%d/", 7878) //これ。ポートは暫定的に7878にする。 authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) browser.OpenURL(authURL)当然ながら、これだけでは動作しません。
アプリ内にHTTPサーバーの機能を実装しないといけません。HTTPサーバーを動作させつつ、承認の手続きをすすめるため、goroutine を利用します。
HTTPサーバーで受け取ったアクセスコードは、チャネル経由で承認手続きをしているコードに繋げます。// 簡易HTTPサーバー // 変数 code, codeChan に注目してください。他はお飾りです。 func launchRedirectionServer(port uint16, codeChan chan string) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { code := r.FormValue("code") codeChan <- code var color string var icon string var result string if code != "" { //success color = "green" icon = "✓" result = "Successfully authenticated!!" } else { //fail color = "red" icon = "✘" result = "FAILED!" } disp := fmt.Sprintf(`<div><span style="font-size:xx-large; color:%s; border:solid thin %s;">%s</span> %s</div>`, color, color, icon, result) fmt.Fprintf(w, ` <html> <head><title>%s pomi</title></head> <body onload="open(location, '_self').close();"> <!-- Chrome won't let me close! --> %s <hr /> <p>This is a temporal page.<br />Please close it.</p> </body> </html> `, icon, disp) }) http.ListenAndServe(fmt.Sprintf(":%d", port), nil) } // 承認手続き func getTokenFromWeb(config *oauth2.Config) (*oauth2.Token, error) { var codeChan chan string config.RedirectURL = fmt.Sprintf("http://localhost:%d/", 7878) codeChan = make(chan string) go launchRedirectionServer(7878, codeChan) authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) browser.OpenURL(authURL) var authCode string authCode = <-codeChan // 以上がカスタマイズ箇所 tok, err := config.Exchange(context.TODO(), authCode) : }承認手続きの中で変数
authCodeをfmt.Scanしていた箇所がなくなり、HTTPサーバーから来るチャネルの読み取りに変わっています。
そして、最後のconfig.Exchange()で、アクセスコードをもとにトークンを取得していますね。なお、割り切りポイントとして、HTTPサーバーの停止は実装していません。
考えるのが面倒だったんで常駐せず実行後は直ぐに終了するアプリですので、そこまでする必要はないかな、と。
ただ、そのために、main() で正常終了時もos.Exit(0)を呼ぶ必要がありました。ちゃんとした CLI アプリにする
以下の対応をしました。
- キーとする項目を指定できるようにする。
- 重複チェックの開始日と取得件数、カレンダーIDを指定できるようにする。
- Dry Runモードをつける。(削除をせずコンソールへの出力のみを行う)
- 承認時のローカルポートを指定できるようにする。
- ヘルプメッセージをつける。
ここらへんは、好きな CLI フレームワークを使えばいいと思います。
個人的には、自分の作った gli というパッケージが死ぬほど使いやすいので使っています。// オプションの定義 (サブコマンドも定義可能ですが、今回はナシ) type globalCmd struct { Start gli.Date `cli:"start,s=DATE" help:"defaults to today"` Items int64 `cli:"items,n=NUMBER" default:"10" help:"the number of events from --start"` Keys gli.StrList `cli:"keys,k=LIST_OF_STRINGS" default:"Description,Summary,Start,End" help:"comman-separated keys to test uniquity of events"` CalendarID string `cli:"calendar-id,id" default:"primary"` Credential string `cli:"credentials,c=FILE_NAME" default:"./credentials.json" help:"your client configuration file from Google Developer Console"` Token string `cli:"token,t=FILE_NAME" default:"./token.json" help:"file path to read/write retrieved token"` AuthPort uint16 `cli:"auth-port=NUMBER" default:"7878"` DryRun bool `cli:"dry-run,dry" help:"do not exec"` } // 文字列で表現できないデフォルト値は *cmd.Init() の中で代入。gli が勝手に呼び出す。 func (c *globalCmd) Init() { c.Start = gli.Date(time.Now()) } // main() はコマンドラインの初期化だけになる。 func main() { app := gli.NewWith(&globalCmd{}) app.Name = "uniqal" app.Desc = "make each event be unique" //英語自信ない... app.Version = "0.1.0" app.Usage = `uniqal --credential=./my_credentials.json --items=100 --start=` + time.Now().AddDate(0, 0, 7).Format("2006-01-02") app.Copyright = "(C) 2019 Shuhei Kubota" err := app.Run(os.Args) if err != nil { os.Exit(1) } os.Exit(0) //ワークアラウンド: HTTPサーバーを停止させたい。 } // メインのルーチンはこちらに移動。gli が勝手に呼び出す。 func (c globalCmd) Run() error { uniqs := make(map[string]struct{}) var config *oauth2.Config var err error if _, err := os.Stat(c.Credential); err != nil { // c.オプション名 でコマンドライン引数を参照 : }出来上がったものがこちらになります
ビルドした上で、自前の credentials.json を配置すれば使えるようになります。
カレンダー同期・連携サービスを使って自爆した人にオススメです。
- 投稿日:2019-08-16T12:05:55+09:00
Go言語のtimeパッケージで夏時間が扱えるか確認
Go言語の標準パッケージtimeで夏時間が扱えるか確認しました。
https://ja.wikipedia.org/wiki/英国夏時間 によると英国の2018年の夏時間は3月25日の1amから10月28日の1amまでだったようなので、その前後を表示してみて確認。package main import ( "fmt" "time" ) func main() { london, _ := time.LoadLocation("Europe/London") fmt.Println(london) t1 := time.Date(2018, time.March, 25, 0, 0, 0, 0, london) for i := 0; i < 5; i++ { fmt.Println(t1) t1 = t1.Add(time.Hour) } t2 := time.Date(2018, time.October, 28, 0, 0, 0, 0, london) for i := 0; i < 5; i++ { fmt.Println(t2) t2 = t2.Add(time.Hour) } }Europe/London 2018-03-25 00:00:00 +0000 GMT 2018-03-25 02:00:00 +0100 BST 2018-03-25 03:00:00 +0100 BST 2018-03-25 04:00:00 +0100 BST 2018-03-25 05:00:00 +0100 BST 2018-10-28 00:00:00 +0100 BST 2018-10-28 01:00:00 +0100 BST 2018-10-28 01:00:00 +0000 GMT 2018-10-28 02:00:00 +0000 GMT 2018-10-28 03:00:00 +0000 GMT扱えるようです。







