├── 2016 ├── .gitkeep ├── treasure-go │ ├── basic │ │ └── basic.go │ ├── db │ │ └── db.go │ ├── fib │ │ ├── fib.go │ │ └── fib_test.go │ ├── gen │ │ └── gen.go │ ├── intro.slide │ ├── report │ │ └── report.go │ ├── scraper │ │ └── scraper.go │ ├── stack │ │ ├── stack.go │ │ └── stack_test.go │ └── template │ │ └── template.go └── treasure │ ├── infra_ci.slide │ └── pre.slide ├── 2017 └── treasure-go │ ├── basic │ └── basic.go │ ├── concurrency │ ├── basic.go │ ├── basic2.go │ ├── crawl.go │ ├── du │ │ └── du.go │ ├── echoserver.go │ ├── race.go │ ├── race │ │ ├── race.go │ │ └── race_test.go │ ├── race_test.go │ ├── waitgroup.go │ └── waitgroup_primer.go │ ├── db │ └── db.go │ ├── fib │ ├── fib.go │ └── fib_test.go │ ├── gen │ └── gen.go │ ├── http │ ├── curl.go │ └── example.go │ ├── img │ ├── legacybook.jpg │ └── tddcycle.png │ ├── intro.slide │ ├── report │ └── report.go │ ├── scraper │ └── scraper.go │ ├── stack │ ├── stack.go │ └── stack_test.go │ └── template │ └── template.go ├── 2018 └── treasure-go │ ├── basic │ └── basic.go │ ├── db │ └── db.go │ ├── fib │ ├── fib.go │ └── fib_test.go │ ├── gen │ └── gen.go │ ├── http │ ├── curl.go │ └── example.go │ ├── img │ ├── legacybook.jpg │ └── tddcycle.png │ ├── intro.slide │ ├── report │ └── report.go │ ├── scraper │ └── scraper.go │ └── stack │ ├── stack.go │ └── stack_test.go ├── 2019 ├── treasure-go-day2 │ ├── img │ │ └── mysql_viewer.png │ └── intro.slide ├── treasure-go │ ├── basic │ │ └── basic.go │ ├── db │ │ └── db.go │ ├── fib │ │ ├── fib.go │ │ └── fib_test.go │ ├── gen │ │ └── gen.go │ ├── http │ │ ├── curl.go │ │ └── example.go │ ├── img │ │ ├── legacybook.jpg │ │ └── tddcycle.png │ ├── intro.slide │ ├── report │ │ └── report.go │ ├── scraper │ │ └── scraper.go │ └── stack │ │ ├── stack.go │ │ └── stack_test.go └── treasure-javascript │ ├── .gitignore │ ├── deck.mdx │ ├── package-lock.json │ └── package.json ├── 2021 └── treasure-go │ ├── Makefile │ ├── README.md │ ├── assets │ ├── arch.png │ └── endpoint.png │ ├── go.mod │ ├── go.slide │ └── go.sum ├── 2022 └── treasure-go │ ├── Makefile │ ├── README.md │ ├── fat_controller.go │ ├── go.mod │ ├── go.slide │ ├── go.sum │ └── img │ ├── arch.png │ ├── comfort_zone.png │ ├── endpoint.png │ ├── me.JPG │ ├── migration_naming_convention.drawio │ ├── migration_naming_convention.png │ └── sequence_diagram.png ├── LICENSE └── README.md /2016/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2016/.gitkeep -------------------------------------------------------------------------------- /2016/treasure-go/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("go go go") 7 | } 8 | -------------------------------------------------------------------------------- /2016/treasure-go/db/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func UserByAge(db *sql.DB, age int) (names []string, err error) { 13 | rows, err := db.Query( 14 | `select name from users where age = ?`, 15 | age) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer rows.Close() 20 | 21 | for rows.Next() { 22 | var name string 23 | if err := rows.Scan(&name); err != nil { 24 | return nil, err 25 | } 26 | names = append(names, name) 27 | } 28 | return names, nil 29 | } 30 | 31 | func main() { 32 | var ( 33 | age = flag.Int("age", 10, "user's age") 34 | ) 35 | db, err := sql.Open("mysql", "") 36 | if err != nil { 37 | log.Fatalf("open db failed: %s", err) 38 | } 39 | users, err := UserByAge(db, *age) 40 | if err != nil { 41 | log.Fatalf("query failed: %s", err) 42 | } 43 | fmt.Printf("%v", users) 44 | } 45 | -------------------------------------------------------------------------------- /2016/treasure-go/fib/fib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func fib(n int) int { 10 | if n == 0 || n == 1 { 11 | return n 12 | } 13 | return fib(n-1) + fib(n-2) 14 | } 15 | 16 | func main() { 17 | if len(os.Args) < 1 { 18 | fmt.Print("usage: ./fib [integer]") 19 | os.Exit(1) 20 | } 21 | i, err := strconv.Atoi(os.Args[1]) 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "argument must be integer: %s\n", err) 24 | os.Exit(1) 25 | } 26 | fmt.Printf("%d", fib(i)) 27 | } 28 | -------------------------------------------------------------------------------- /2016/treasure-go/fib/fib_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFib(t *testing.T) { 8 | type Case struct { 9 | n, out int 10 | } 11 | cases := []Case{ 12 | {0, 0}, 13 | {1, 1}, 14 | {10, 55}, 15 | } 16 | for i, c := range cases { 17 | if got := fib(c.n); got != c.out { 18 | t.Errorf("#%d: fib(%d) want %d, got %d\n", i, c.n, c.out, got) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /2016/treasure-go/gen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/build" 5 | "os" 6 | "strings" 7 | "text/template" 8 | ) 9 | 10 | var tmpl = template.Must(template.New("").Parse(`// Don't edit! generated by gen.go 11 | 12 | package {{.Package}} 13 | 14 | {{range .Params}}func Handle{{title .Name}}(w http.ResponseWriter, r *http.Request) { 15 | p := r.FormValue("{{.Name}}") 16 | if p == "" { 17 | http.Error(w, "{{.Name}} not specified", http.StatusBadRequest) 18 | return 19 | } 20 | fmt.Fprintf(w, "%s is god.", p) 21 | } 22 | {{end}} 23 | `)) 24 | 25 | func Generate(name string...) error { 26 | pkg, err := build.Default.Import(".", 0) 27 | if err != nil { 28 | return err 29 | } 30 | f, err := os.Create(fmt.Sprintf("%s_gen.go"), strings.ToLower(name)) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | return tmpl.Execute(f, map[string]string{}) 36 | } 37 | -------------------------------------------------------------------------------- /2016/treasure-go/intro.slide: -------------------------------------------------------------------------------- 1 | Go入門 2 | Treasure2016版 3 | 00:00 15 Aug 2016 4 | Tags: golang 5 | 6 | Kenta SUZUKI 7 | Software Engineer at VOYAGE GROUP 8 | @suzu_v 9 | https://github.com/suzuken 10 | 11 | * この資料について 12 | 13 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2016年度版講義資料です。Goの講義は2日間です。 14 | 15 | .link https://voyagegroup.com/internship/treasure/ 16 | 17 | GitHubで公開されています。 18 | 19 | .link https://github.com/voyagegroup/talks 20 | 21 | * はじめに 22 | 23 | * Go 1.7がリリースされました! 24 | 25 | .link https://blog.golang.org/go1.7 26 | 27 | 今すぐインストールしましょう。 28 | 29 | * 2日間で学んでほしいこと 30 | 31 | - Goで小さなコマンドラインツールを書けるようになること 32 | - 一通り小さなWebアプリケーションを書けるようになること 33 | - Goのコードの書き方に慣れること 34 | 35 | とにかくどんどんコードを書きましょう。 36 | 37 | * アウトライン 38 | 39 | - なぜGoなのか? 40 | - 事前課題解説 41 | - testing 42 | - net/http 43 | - encoding/json 44 | - テンプレート 45 | - Goとデータベース 46 | - データベースマイグレーション 47 | 48 | .image https://golang.org/doc/gopher/project.png 49 | 50 | * なぜGoを使うのか? 51 | 52 | いろいろな良い点があります。 53 | 54 | - 言語仕様がfixしている 55 | - 軽快なbuild / 高速な動作 / concurrencyサポート 56 | - ツール (go tool, gofmt ...) 57 | - ほどよい標準ライブラリ 58 | 59 | Goの一番の特徴はSimplicityだと思います。 60 | 61 | .link https://talks.golang.org/2015/simplicity-is-complicated.slide#1 Simplicity is Complicated 62 | 63 | * 準備: Tour of Go 64 | 65 | .link https://tour.golang.org 66 | 67 | - まずTour of Goおすすめ 68 | - 言語仕様が簡単に学べる 69 | - ほどよい演習 70 | 71 | 演習やってないひとはやりましょう 72 | 73 | .link https://gist.github.com/suzuken/d892c42e56c986bca813 74 | 75 | * 準備: How to write Go code 76 | 77 | .link https://golang.org/doc/code.html 78 | 79 | - `$GOPATH/bin` をPATHに追加しましょう 80 | - これから書いてもらうコードもGoのワークスペースに則って配置して貰う必要があります。 81 | - 例えば `$GOPATH/src/github.com/suzuken/wiki` などに配置します。go getをつかうとこのルールに従って配置されます。 82 | 83 | tips: ghqをつかうとgit cloneするリポジトリの配置が楽になります。 84 | 85 | .link https://github.com/motemen/ghq 86 | 87 | tips: pecoをつかうと配置しているコードへの移動が楽になります。 88 | 89 | .link https://github.com/peco/peco 90 | 91 | * 手続き、コマンドラインツール、そしてテスト 92 | 93 | * 事前課題その1 94 | 95 | n番目のフィボナッチ数を返す手続きを実装してください。 96 | 97 | f(0) = 0 98 | f(1) = 1 99 | f(n+2) = f(n) + f(n+1) 100 | 101 | .link https://gist.github.com/suzuken/a8ec8fc95cd0c0d18d8d5584fdd4f3ab 102 | 103 | * 事前課題その1 フィボナッチ数 解答 104 | 105 | func fib(n int) int { 106 | if n == 0 { 107 | return 0 108 | } 109 | if n == 1 { 110 | return 1 111 | } 112 | return fib(n-1) + fib(n-2) 113 | } 114 | 115 | ちょっと冗長? 116 | 117 | OK、リファクタリングしましょう。テスト書いてから。 118 | 119 | * テスト 120 | 121 | テスト大事 122 | 123 | .image https://connpass-tokyo.s3.amazonaws.com/event/27540/41d84cf0e6494e2e91e51ad8e9c85310.png 124 | 125 | .link https://golang.org/pkg/testing/ 126 | 127 | * フィボナッチのテスト (発展課題 その1) 128 | 129 | .code fib/fib_test.go /func TestFib/,/^}/ 130 | 131 | .link https://github.com/golang/go/wiki/TableDrivenTests 132 | 133 | $ go test -v ./... 134 | === RUN TestFib 135 | --- PASS: TestFib (0.00s) 136 | PASS 137 | ok github.com/voyagegroup/talks/2016/treasure-go/fib 0.006s 138 | 139 | 140 | * リファクタリング 141 | 142 | .code fib/fib.go /func fib/,/^}/ 143 | 144 | 書き換えたらテストをします。 145 | 146 | $ go test -v ./... 147 | === RUN TestFib 148 | --- PASS: TestFib (0.00s) 149 | PASS 150 | ok github.com/voyagegroup/talks/2016/treasure-go/fib 0.006s 151 | 152 | リファクタリング前にはテストを書きましょう。 153 | 154 | * コマンドラインツールをつくろう(発展課題 その2) 155 | 156 | Goで `go run` させられるのは `main` パッケージのみ。 157 | 158 | .play basic/basic.go 159 | 160 | `fib` をするコマンドラインツールをつくってみましょう。 161 | 162 | * os.Args をつかう 163 | 164 | .play fib/fib.go /func main/,/^}/ 165 | 166 | ほとんどの場合は `flag` をつかったほうが楽です。 167 | 168 | .link https://golang.org/pkg/flag/ 169 | 170 | * よくしたレビューコメント 171 | 172 | 可読性大事。可読性のあるコードは信頼できるコードです。 173 | 174 | - `gofmt` おねがいします 175 | - `main` 以外で `os.Exit` や `panic` するのは大抵好ましくないです 176 | - `error` はよっぽど自明じゃないかぎり無視しないように 177 | - 変数や手続き名は `snake_case` ではなくて `camelCase` 178 | - 変数名は短く 179 | - 余分なelse: インデントを最小にしましょう 180 | 181 | .link https://golang.org/s/style 182 | .link https://blog.golang.org/go-fmt-your-code 183 | 184 | * TDD実習 & ペアプログラミング 185 | 186 | * TDD実習の説明 187 | 188 | - これからのとなりの人と一緒にプログラムを書いてもらいます 189 | - どちらか1台のPCをつかいます 190 | - どちらか一方がテストを、どちらか一方が実装側を書きます 191 | 192 | デモします。 193 | 194 | * TDD実習 その1: スタック 30分 195 | 196 | - スタックを作成してください 197 | 198 | .image https://upload.wikimedia.org/wikipedia/commons/b/b4/Lifo_stack.png _ 350 199 | 200 | // 簡単のため、stringしかいれられないようにしています 201 | type Stack struct{} 202 | func (s *Stack) Pop() string {} 203 | func (s *Stack) Push(s string) {} 204 | 205 | 内部のデータ構造はsliceをつかうといいでしょう。 206 | 207 | .link https://blog.golang.org/go-slices-usage-and-internals 208 | 209 | * 動作例 210 | 211 | s := &Stack{} 212 | s.Push("dataA") 213 | s.Push("dataB") 214 | s.Push("dataC") 215 | s.Pop() // -> "dataC" 216 | s.Pop() // -> "dataB" 217 | s.Push("dataD") 218 | s.Pop() // -> "dataD" 219 | s.Pop() // -> "dataA" 220 | s.Pop() // -> "" 221 | 222 | * TDD実習 その2: 制限付きスタック 10分 223 | 224 | - スタックに最大長を設定できるようにしてください 225 | - 最大長を超えるPushについては、古いものから削除されるようにしましょう。 226 | 227 | 動作例 228 | 229 | s := &Stack{limit: 2} 230 | s.Push("dataA") 231 | s.Push("dataB") 232 | s.Push("dataC") 233 | s.Pop() // -> "dataC" 234 | s.Pop() // -> "dataB" 235 | s.Pop() // -> "" 236 | 237 | 238 | インタフェースについては自分たちで相談して決めてみましょう。 239 | 240 | 発展 241 | 242 | - あとから制限長を変更できるようにしましょう 243 | 244 | * testing振り返り 245 | 246 | - どんなテストをかけばいいかを考えるのは最初は難しいかもしれません。 247 | - `testing` パッケージではふつうのGoコードでテストを書くことができます。テストを書くのに特別な知識は必要ありません。 248 | - ユニットテストを書くことで既存の機能が正常に動いていることを担保しつつ、新しい機能の追加を手助けしてくれます 249 | - デバッグプリントするのではなくテストを書く、ということも視野にいれてみましょう 250 | 251 | * net/http 入門 252 | 253 | * 事前課題その2: net/http 演習 254 | 255 | - 指定したURLにあるコンテンツについて、タイトルとdescriptionを抜き出すツールを書きましょう 256 | - HTTPサーバとして実装してこの機能をつかえるようにしましょう 257 | 258 | .link https://gist.github.com/suzuken/b456e0f4679f86da572839d6d86f159e 259 | 260 | $ go run scraper/scraper.go& 261 | $ curl -D - "http://localhost:8080?url=https://golang.org" 262 | HTTP/1.1 200 OK 263 | Content-Type: application/json 264 | Date: Fri, 05 Aug 2016 07:45:23 GMT 265 | Content-Length: 57 266 | 267 | {"title":"The Go Programming Language","description":""} 268 | 269 | この課題の狙いは、ある仕事をHTTPサーバに組み込むことを試してもらうことです。適切に仕事をわけて実装すれば、楽に組み込むことができます。 270 | 271 | * net/http.(*Server) 272 | 273 | Goの標準ライブラリでは簡単にHTTPサーバを立ち上げることができます。 274 | 275 | func main() { 276 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 277 | // wはio.Writerなので書き込むことができます 278 | fmt.Fprint(w, "hello world") 279 | }) 280 | http.ListenAndServe(":8080", nil) 281 | } 282 | 283 | and 284 | 285 | $ go run server.go& 286 | $ curl http://localhost:8080 287 | hello world 288 | 289 | .link https://golang.org/pkg/fmt/#Fprint 290 | .link https://golang.org/pkg/net/http/ 291 | 292 | * encoding/json 293 | 294 | .code scraper/scraper.go /type Page/,/^}/ 295 | 296 | and 297 | 298 | enc := json.NewEncoder(w) 299 | if err := enc.Encode(p); err != nil { 300 | // ... 301 | } 302 | 303 | すると io.Writer である `w` にJSONが出力されます。 304 | 305 | .link https://golang.org/pkg/encoding/json/ 306 | 307 | 308 | * 2の解答例 309 | 310 | .code scraper/scraper.go /START OMIT/,/END OMIT/ 311 | 312 | .link https://gist.github.com/suzuken/e211f01171e2a841ddf6417e479004e0 313 | 314 | * 2の解答例 (cont.) 315 | 316 | .code scraper/scraper.go /func handler/,/^}/ 317 | 318 | .link https://gist.github.com/suzuken/e211f01171e2a841ddf6417e479004e0 319 | 320 | * io.Writer またはインタフェース 321 | 322 | type Writer interface { 323 | Write(p []byte) (n int, err error) 324 | } 325 | 326 | .link https://golang.org/pkg/io/#Writer 327 | 328 | `io.Writer` を出力先としてサポートすると、Goの標準的なインタフェースを利用した機能に出力する機能を提供することができます。例えば `os.Stdout` なども `io.Writer` を実装しています。 329 | 330 | .link http://mattn.kaoriya.net/software/lang/go/20140501172821.htm Big Sky :: Golang のオフィシャルが提供するインタフェースまとめ 331 | .link http://spf13.com/presentation/7-biggest-mistakes-in-go/ 7 Common mistakes in Go and when to avoid them 332 | 333 | * 実習 net/http: 複数のページ取得 45分 334 | 335 | 事前課題その2でつくったHTTPサーバに以下の機能を追加しましょう。 336 | 337 | - URLを複数受け取り、複数ページのtitleとdescriptionを取ってこれるようにしましょう。パラメータの渡し方はお好みで。(カンマ区切りなど) 338 | - 結果はJSONの配列で返すようにしましょう。その際、どのURLからどの結果が取得できたかをわかるようにしてください。 339 | 340 | 発展課題 341 | 342 | - 各ページへのHTTPリクエストのタイムアウトを設定し、長く時間のかかったURLは無視できるようにしましょう。正常に取得できた結果のみ返すようにします。 343 | - URLごとにページの取得を並列化しましょう。すべてのページの結果が揃ったら結果を返すようにしてみましょう。 344 | - `net/http/httptest` をつかってこの機能のテストを書いてみましょう。 345 | 346 | * 小さなWebアプリケーションを実装しよう 347 | 348 | * ところで 349 | 350 | Webアプリケーションってなんでしょう? 351 | 352 | - HTTPリクエスト受け付ければWebアプリケーション? 353 | - フォームがあるもの? 354 | - たくさんページがあればWebアプリ? 355 | - ユーザが使えばWebアプリ? 356 | - 入出力によってページ内容がかわればWebアプリ? 357 | - TwitterやGoogleはWebアプリ? 358 | 359 | Webアプリとひとえにいってもいろいろありますね。 360 | 361 | * WAF: Web Application Framework 362 | 363 | - Webアプリケーションをつくるために便利な機能をまとめたもの 364 | - Goにおいては `net/http` のラッパーであるものが多い 365 | - 今回は `gin-gonic/gin` をとりあげます 366 | 367 | .link https://github.com/gin-gonic/gin 368 | 369 | `net/http` と標準ライブラリを組み合わせてできないことはGoではできないと思って良いので、楽に書くためにWAFを使う、くらいなスタンスがいいと思います。 370 | 371 | * 実アプリケーションの例 372 | 373 | .link https://github.com/suzuken/wiki 374 | 375 | デモしつつ解説します。MySQLがあれば手元でも動かせるので、やってみてください。 376 | 機能は以下のもののみです。 377 | 378 | - ユーザ登録とログイン・ログアウト 379 | - 記事の投稿・編集・削除 380 | 381 | このあとの実習でこのアプリケーションに機能を追加してもらいます。 382 | 383 | * よくあるWebアプリケーションの構成 384 | 385 | 今回はいわゆるMVCの構成にしてみました。といっても `gin-gonic/gin` がこのルールで動くわけではなくて、Goのpackageの機能をつかってディレクトリをわけているだけです。 386 | 387 | コードをみつつ、1つ1つ中身を解説します。 388 | 389 | .image https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/MVC-Process.svg/1000px-MVC-Process.svg.png 300 _ 390 | 391 | * なぜこういう構成なの? 392 | 393 | Webアプリケーションによくある仕事をわけると、下の3つの層に分けることができます。 394 | 395 | - データソースからの参照、あるいは更新をする層 396 | - ユーザへの見せ方をいい感じに整える層 397 | - HTTPリクエストをうけて、仕事を制御して振り分ける層 398 | 399 | .link http://www.slideshare.net/brtriver/php-14295877 フラットなPHPからフレームワークへ 400 | 401 | 仕事をわけることで、各実装をシンプルにすることができます。ただし分割しすぎるのはよくありません。またGo特有ですが、適切にpackageを分割することによるメリットもあります。 402 | 403 | - 構造体やインタフェースの名前をシンプルにできる。 404 | - 依存関係が明瞭になり、package単位のテストを書きやすくなる。 405 | 406 | * テンプレート 407 | 408 | * WebアプリケーションにおけるHTMLテンプレート 409 | 410 | - 構造化してviewをつくる 411 | - 適切にエスケープをしてHTMLを出力する 412 | 413 | スマートフォンアプリだったりクライアントサイドでテンプレートの機能をもっているものをつかうと、この仕事はサーバサイドではやらないことになります。 414 | 415 | .link http://www.slideshare.net/ockeghem/xssreintroduction 416 | 417 | * html/template の利用例 418 | 419 | .play template/template.go /func main/,/^}/ 420 | 421 | * 実習: コメント欄の追加 422 | 423 | - 記事にコメント欄をつけてみましょう 424 | 425 | テンプレートを追加して、フォーム作って、送信、あたりを作る必要があります。エンドポイントも新しくつくりましょう。ただ、ちゃんとコメントが保存されなくてもいいです。(DB実習をしてから、ちゃんとやることにしましょう。) 426 | 427 | 発展課題 428 | 429 | - コメント内容をメモリ上に保存できるようにしてみましょう。適当な `map[int][]string` なんかにいれるといいかも?誰のコメントかとりあえず気にしなくてもいいです。 430 | - 誰のコメントかも保存できるようにしてみましょう。`type Comment struct` をつくって上の `map` の値としていれられるといいかもしれません。 431 | 432 | * 実習: CSRF対策 433 | 434 | wikiのフォームにトークンが入っていることに気が付きましたか? 435 | 436 | 437 | 438 | .link https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/301.html IPA CSRF対策 439 | 440 | 実習: CSRFトークンをブラウザの開発ツールで上書きして、リクエストが失敗することを確認してみましょう。 441 | 442 | * Goとデータベース 443 | 444 | * ここまでの振り返り 445 | 446 | - MVCのCとVをやりました。 447 | 448 | .image https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/MVC-Process.svg/1000px-MVC-Process.svg.png 300 _ 449 | 450 | 残りのM: Modelをみていきましょう。 451 | 452 | コメント欄の実習でメモリに `map` で保持してもらいました。ここを永続化する、ということをやっていきます。 453 | 454 | * Goとデータベース: database/sql 455 | 456 | Goからデータベースを扱うときは `database/sql` を使います。 457 | 458 | .link https://golang.org/pkg/database/sql/ 459 | .link https://golang.org/s/sqlwiki 460 | .link https://golang.org/s/sqldrivers 461 | 462 | MySQLでの利用例は以下を見つつ説明します。 463 | 464 | .link https://github.com/go-sql-driver/mysql/wiki/Examples 465 | 466 | 以下の資料もおすすめです。 467 | 468 | .link http://go-database-sql.org/ 469 | 470 | * クエリするときの原則 471 | 472 | - クエリにパラメータを埋め込むときは必ずプリペアドステートメントを使いましょう。 473 | - 決して `fmt.Sprintf` などによる文字列結合でクエリを組み立ててはいけません。 474 | 475 | .link https://www.ipa.go.jp/files/000024396.pdf SQLインジェクション対策について 476 | .link http://www.ipa.go.jp/files/000017320.pdf 安全なSQLの呼び出し方 477 | .link http://d.hatena.ne.jp/ajiyoshi/20100409/1270809525 478 | 479 | Prepare, Query、どちらでもプリペアドステートメントが使えます。 480 | 481 | .link https://golang.org/pkg/database/sql/#DB.Prepare 482 | .link https://golang.org/pkg/database/sql/#DB.Query 483 | 484 | db.Query(`select * from users where name = ?`, "s-tanno") 485 | 486 | * DB実習: 一通り自分でDB操作をしてみよう 487 | 488 | MySQLに自分用の日報を保存するコマンドラインアプリケーションをつくりましょう。 489 | 490 | - 日報用のテーブル `reports` を用意し、保存できるようにしてください。更新、削除の機能はお好みで。テーブルは自分で考えてみましょう。 491 | 492 | 発展課題 493 | 494 | - いい感じに日報を整形して標準出力できるようなオプションをつけてみましょう。 495 | - 日報にタグを追加できるようにしてみましょう。タグによって日報を取得できるようにすると便利そうです。 496 | - Webアプリにしてみましょう。type Report struct に `ServeHTTP` を実装するのがおすすめです。 497 | 498 | .link https://golang.org/pkg/net/http/#Handler 499 | 500 | * データベースマイグレーション 501 | 502 | - 複数人でアプリケーションをつくるときにスキーマの管理どうしますか? 503 | - データベースマイグレーションをつかうと便利です 504 | 505 | .link https://github.com/rubenv/sql-migrate 506 | 507 | `suzuken/wiki` でのセットアップにすでにつかってもらっていました。`migrations` ディレクトリにSQLが入っています。 `sql-migrate` からこれらを実行できます。 508 | 509 | sql-migrate up 510 | 511 | たとえば新しいテーブルを追加するときやスキーマを変更するときには、SQLファイルを `migrations` 以下にコミットするといいです。すると機能とセットでDBスキーマの変更もコードで管理することができます。 512 | 513 | 後半環境ではDBマイグレーションをいい感じに扱う仕組みも用意しました。詳しくは [[https://github.com/s-tajima][@s-tajima]] [[https://github.com/nishigori][@nishigori]] の講義をお楽しみに。 514 | 515 | * DB実習 516 | 517 | wikiの記事につけたコメントをちゃんとDBに保存できるようにしてみましょう。 518 | 519 | テーブル例 520 | 521 | CREATE TABLE `comments` ( 522 | `comment_id` int(11) NOT NULL AUTO_INCREMENT, 523 | `article_id` int(11) NOT NULL, 524 | `body` TEXT NOT NULL, 525 | `created` timestamp NOT NULL DEFAULT NOW(), 526 | `updated` timestamp NOT NULL DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP, 527 | PRIMARY KEY (`comment_id`) 528 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='list of comments'; 529 | 530 | - データベースマイグレーションも用意してあるので、つかってみるといいでしょう 531 | 532 | 発展課題 533 | 534 | - コメントについて誰がコメントしたかも保存できるようにしましょう 535 | - コメントの編集削除をサポートしましょう 536 | - トップページに記事のコメント数も出すようにしましょう 537 | 538 | * Tips: クエリ結果のstructへのbind 539 | 540 | reflectをつかってstructにbindする方法があります。 541 | 542 | .link https://github.com/jmoiron/sqlx 543 | .link https://github.com/naoina/genmai 544 | 545 | `voyagegroup/gin-boilerplate` では `sqlx` をつかっています。 546 | 547 | またはコード生成をつかうのも良い選択でしょう。 `database/sql` のための `Scan` 用手続きをstructから生成すると便利です。 `suzuken/wiki` では `scaneo` をつかっています。 548 | 549 | .link https://blog.golang.org/generate 550 | .link https://github.com/variadico/scaneo 551 | 552 | * コード生成 553 | 554 | デモします 555 | 556 | * サンプルアプリケーションの説明 その2: TODOアプリ 557 | 558 | サーバサイドにginをつかい、フロントをReactで実装したSPAのTODOアプリです。 559 | 560 | .link https://github.com/voyagegroup/gin-boilerplate 561 | 562 | コードベースは `gin-gonic` なので `suzuken/wiki` にも似ていますが、以下を変えています。スマホアプリがフロントになる場合にも、設計は似ているかと思います。 563 | 564 | - JSON APIでクライアントとやりとり 565 | - RestfulっぽいAPI設計 566 | 567 | コードをみつつ説明します。 568 | 569 | .link https://facebook.github.io/react/docs/thinking-in-react.html Thinking in React 570 | 571 | ModelやControllerの実装はwikiとそれほど変わりません。viewのレイヤはまるっとReactで書いています。 572 | 573 | * 実習: アプリケーションを拡張してみよう wiki編 574 | 575 | - 投稿したユーザを保存できるようにしましょう 576 | - 自分の投稿を一覧で見られるようにしましょう 577 | - 投稿に「いいね!」できるようにしてみましょう 578 | 579 | 発展課題 580 | 581 | - 記事の閲覧数を保存できるようにしてみましょう。 582 | - 人気の投稿一覧を見られるようにしましょう。どんな投稿が人気かは自分で決めてよいです。 583 | - 本文中に他の記事のタイトルがあれば自動でリンクを貼るようにしてみましょう。(これをやるとwikiっぽくなりますね) 584 | - 記事の編集履歴をみれるようにしてみましょう。 585 | 586 | * 実習: アプリケーションを拡張してみよう TODOアプリ編 587 | 588 | - TODOにコメントを追加できるようにしてみましょう: React側のComponentも拡張するといいでしょう 589 | - ログインできるようにしてみましょう 590 | 591 | 発展課題 592 | 593 | - TODOの進捗率を表示できるようにしてみましょう。例えば10タスクあって8タスクおわっているなら進捗80%といった具合に。 594 | - TODOに画像を設定できるようにしてみましょう。 595 | - structからRestfulなAPIをコード生成でつくってみましょう。 `Scan` を生成しているのと同じ要領で、ModelとControllerを生成するとよいでしょう。 596 | - GitHubアカウントでログインできるようにしてみましょう 597 | 598 | * HTTPサーバを書くときに考えることはたくさんある 599 | 600 | - どのようなHTTPリクエストを受け取るか 601 | - どのようなルーティングにすべきか 602 | - HTTPレスポンスステータスは何にすべきか 603 | - HTTPレスポンスヘッダには何を入れるべきか 604 | - HTTPレスポンスボディにはどのような形式で返すべきか 605 | 606 | より詳しくは [[https://twitter.com/brtriver][@brtriver]] の講義でやります 607 | 608 | 609 | -------------------------------------------------------------------------------- /2016/treasure-go/report/report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "time" 7 | ) 8 | 9 | type Report struct { 10 | Title string 11 | Body string 12 | } 13 | 14 | func (r *Report) Insert(db *sql.DB) (sql.Result, error) { 15 | stmt, err := db.Prepare(`insert into (title, body) values(?, ?)`) 16 | if err != nil { 17 | return err 18 | } 19 | defer stmt.Close() 20 | return stmt.Exec(r.Title, r.Body) 21 | } 22 | 23 | func Select(db *sql.DB) ([]Report, error) { 24 | rows, err := db.Query(`select * from reports`) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer rows.Close() 29 | 30 | reports := make([]Report{}) 31 | for rows.Next() { 32 | var title string 33 | var body string 34 | if err := rows.Scan(&title, &body); err != nil { 35 | return nil, err 36 | } 37 | r := Report{Title: title, Body: body} 38 | reports = append(reports, r) 39 | } 40 | return reports, nil 41 | } 42 | 43 | func main() { 44 | var ( 45 | title = flag.String("title", "kuke", "cool title") 46 | body = flag.String("body", "treasure", "cool body") 47 | ) 48 | flag.Parse() 49 | db, err := sql.Open("sqlite3", ":memory:") 50 | if err != nil { 51 | log.Fatalf("open failed: %s", err) 52 | } 53 | r := &Report{*title, *body} 54 | if _, err := r.Insert(db); err != nil { 55 | log.Fatalf("insert failed: %s", err) 56 | } 57 | 58 | result, err := Select(db) 59 | if err != nil { 60 | log.Fatalf("select failed: %s", err) 61 | } 62 | fmt.Printf("%v", result) 63 | } 64 | -------------------------------------------------------------------------------- /2016/treasure-go/scraper/scraper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "golang.org/x/net/html" 9 | ) 10 | 11 | type Page struct { 12 | Title string `json:"title"` 13 | Description string `json:"description"` 14 | } 15 | 16 | func Get(url string) (*Page, error) { 17 | resp, err := http.Get(url) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer resp.Body.Close() 22 | 23 | doc, err := html.Parse(resp.Body) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // START OMIT 29 | page := &Page{} 30 | var f func(*html.Node) 31 | f = func(n *html.Node) { 32 | if n.Type == html.ElementNode && n.Data == "title" { 33 | page.Title = n.FirstChild.Data 34 | } 35 | if n.Type == html.ElementNode && n.Data == "meta" { 36 | if isDescription(n.Attr) { 37 | for _, attr := range n.Attr { 38 | if attr.Key == "content" { 39 | page.Description = attr.Val 40 | } 41 | } 42 | } 43 | } 44 | for c := n.FirstChild; c != nil; c = c.NextSibling { 45 | f(c) 46 | } 47 | } 48 | f(doc) 49 | // END OMIT 50 | return page, nil 51 | } 52 | 53 | func isDescription(attrs []html.Attribute) bool { 54 | for _, attr := range attrs { 55 | if attr.Key == "name" && attr.Val == "description" { 56 | return true 57 | } 58 | } 59 | return false 60 | } 61 | 62 | func handler() { 63 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 64 | rawurl := r.FormValue("url") 65 | if rawurl == "" { 66 | http.Error(w, "url not specified", http.StatusBadRequest) 67 | return 68 | } 69 | p, err := Get(rawurl) 70 | if err != nil { 71 | http.Error(w, "request failed", http.StatusInternalServerError) 72 | return 73 | } 74 | w.Header().Set("Content-Type", "application/json") 75 | enc := json.NewEncoder(w) 76 | if err := enc.Encode(p); err != nil { 77 | http.Error(w, "encoding failed", http.StatusInternalServerError) 78 | return 79 | } 80 | }) 81 | } 82 | 83 | func main() { 84 | handler() 85 | log.Fatal(http.ListenAndServe(":8080", nil)) 86 | } 87 | -------------------------------------------------------------------------------- /2016/treasure-go/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | type Stack struct { 4 | s []interface{} 5 | } 6 | 7 | func (s *Stack) Pop() interface{} { 8 | return s.s[len(s.s)-1] 9 | } 10 | 11 | func (s *Stack) Push(i interface{}) { 12 | s.s = append(s.s, i) 13 | } 14 | -------------------------------------------------------------------------------- /2016/treasure-go/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPushAndPop(t *testing.T) { 8 | s := &Stack{} 9 | s.Push(1) 10 | if r := s.Pop(); r != 1 { 11 | t.Fatalf("want 1, got %v", r) 12 | } 13 | if r := s.Pop(); r != nil { 14 | t.Fatalf("want nil, got %v", r) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /2016/treasure-go/template/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | tmpl, err := template.New("").Parse(` 11 | 12 | 13 | {{.Title}} 14 | 15 | 16 | {{.Body}} 17 | 18 | 19 | `) 20 | if err != nil { 21 | log.Fatalf("compile template failed: %s", err) 22 | } 23 | if err := tmpl.Execute(os.Stdout, map[string]interface{}{ 24 | "Title": "cool title", 25 | "Body": "here is body", 26 | }); err != nil { 27 | log.Fatalf("execute template failed: %s", err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /2016/treasure/infra_ci.slide: -------------------------------------------------------------------------------- 1 | インフラ・CI 最初の一歩 2 | Treasure2016版 3 | 00:00 23 Aug 2016 4 | 5 | Takuya Nishigori 6 | Deployment prayer at VOYAGE GROUP 7 | @_nishigori 8 | https://github.com/nishigori 9 | 10 | * この資料について 11 | 12 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2016年度版講義資料です。 13 | 14 | .link https://voyagegroup.com/internship/treasure/ 15 | 16 | GitHubで公開されています 17 | 18 | .link https://github.com/voyagegroup/talks 19 | 20 | * 今日学んでほしいこと 21 | 22 | - Linuxで何かパッケージをインストールして動かしてみよう 23 | - CIって何か、なんとなく分かる 24 | - アプリケーションのデプロイ設計 25 | 26 | * アウトライン 27 | 28 | - インフラ is 何?? 29 | - デプロイメント ~ 解説 30 | - デプロイメント ~ 演習 31 | - デプロイメント ~ 考察 32 | - デプロイメント ~ デモ 33 | 34 | * インフラ is 何?? 35 | 36 | インフラとは ~ コトバンク 37 | 38 | .link https://kotobank.jp/word/インフラ-12087 39 | 40 | For examples at VOYAGE GROUP: 41 | 42 | - SRE (Service Reliability Engineering) 43 | - IDCG (Internet Data Center Group) 44 | - Nothing (all for one) 45 | 46 | * OSI Reference Model 47 | 48 | .link https://ja.wikipedia.org/wiki/OSI%E5%8F%82%E7%85%A7%E3%83%A2%E3%83%87%E3%83%AB 49 | 50 | 1. 物理層 | ハブ、ケーブル 51 | 1. データリンク層 | ethernet 52 | 1. ネットワーク層 | IP, IPSec, DDP 53 | 1. トランスポート層 | TCP, UDP 54 | 1. セッション層 | TLS, NetBIOS 55 | 1. プレゼンテーション層 | SMTP, FTP, Telnt 56 | 1. アプリケーション層 | HTTP, SMTP, Telnet 57 | 58 | .image https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/OSI-model-Communication.svg/434px-OSI-model-Communication.svg.png 59 | 60 | * アプリケーションエンジニアがインフラレイヤーを知る必要性 61 | 62 | - データの流れを理解したい 63 | - キャパシティプランニングの知見 64 | 65 | レイヤーは分かれど人が分かれるなんて定義されてない 66 | 67 | * Solving Large Scale Web Operations 68 | 69 | This project was originally focused only on automated provisioning. 70 | It's original goal was to help make sense - 71 | of the various open source automation tools that are available. 72 | 73 | .image http://img.scoop.it/XfDZD6Q4FMGgvdpvfXJ3cTl72eJkfbmt4t8yenImKBVvK0kTmF0xjctABnaLJIm9 74 | 75 | .link http://cdn.oreillystatic.com/en/assets/1/event/48/Provisioning%20Toolchain%20Presentation.pdf 76 | 77 | * デプロイメント 78 | 79 | - そもそも語源は戦争用語から来ている 80 | 81 | .link http://martinfowler.com/books/continuousDelivery.html 82 | 83 | .image http://martinfowler.com/books/continuousDelivery.jpg 84 | 85 | * デプロイメント 86 | ** 考えてみよう 87 | 88 | - まず何から始める? 89 | - いつデプロイすべき? 90 | - テスト必要? 91 | 92 | * デプロイメント 93 | ** やってみよう! 94 | 95 | - 中間課題で作成したアプリケーションコードをサーバにデプロイしよう 96 | - 必要なミドルウェア等があればインストールしよう 97 | - あとで発表しよう 98 | 99 | .link https://git.io/v69TY 100 | 101 | * デプロイメント 102 | ** 設計パターン例 103 | 104 | - push型 105 | - pull型 106 | - (+ event発火) 107 | 108 | * デプロイメント 109 | ** DEMO 110 | 111 | * CI 112 | 113 | ** 継続的に回していきたいですね 114 | 115 | - アプリケーションコードのデプロイ 116 | - Git commit毎にビルド実行(チェック) 117 | - Git commit毎にテスト実行 118 | - Git commitg(ry 119 | 120 | * CI 121 | 122 | ** DEMO 123 | 124 | .link https://travis-ci.com/ 125 | 126 | * クラスタリング 127 | 128 | Slack Case Study 129 | 130 | .link https://aws.amazon.com/solutions/case-studies/slack/ 131 | 132 | .image https://d0.awsstatic.com/architecture-diagrams/customers/slack-arch-diagram.png 133 | 134 | * スケーラビリティ 135 | 136 | .link https://aws.amazon.com/jp/cdp/cdp-autoscaling/ 137 | 138 | .image https://d0.awsstatic.com/icons/jp/cdp/cdp-jp-biz-009_v3.png 139 | 140 | * クラスタリング / スケーラビリティ 141 | ** DEMO 142 | 143 | * DevOps 144 | 145 | ”10 deploys per day dev & ops cooperation at Flickr” 146 | 147 | .link http://www.slideshare.net/jallspaw/10-deploys-per-day-dev-and-ops-cooperation-at-flickr/12-The_business_requires_change 148 | 149 | - 一日数十回のデプロイ 150 | - 当時(2009)「開発と運用が協力するなんてばかげている」とか言われてた by patrick 151 | 152 | - Automated infrastructure 153 | - Shared version control 154 | - One step build and deploy 155 | 156 | * 以下時間があったら 157 | 158 | - Daemonize (SUpervisor, daemontools, rc script) 159 | - Monitoring 160 | - Infra testing 161 | - F/W (netfiltering, iptables, SELinux) 162 | - SSL / TLS 163 | - HTTP2 / WebSocket 164 | - Benchmarking (round robin, least connections, ...) 165 | - Caching (CDN, Varnish, ...) 166 | - In-memory Database 167 | - Chroot 168 | - Hypervisor (Citrix Xen, VMWare) 169 | - Container (Docker, rkt, CoreOS) 170 | 171 | * 最後に 172 | 173 | あなたのアプリケーションのよりよいデプロイメント方法は 174 | あなたが一番考えられるはずです 175 | -------------------------------------------------------------------------------- /2016/treasure/pre.slide: -------------------------------------------------------------------------------- 1 | Treasure初日オリエン 2 | Treasure2016版 3 | 00:00 14 Aug 2016 4 | 5 | Kenta SUZUKI 6 | Software Engineer at VOYAGE GROUP 7 | @suzu_v 8 | https://github.com/suzuken 9 | 10 | * Treasureへようこそ! 11 | 12 | * みなさんにおすすめなこと 13 | 14 | - メモは大量にとりましょう。日報はそのための下書きだと思うといいです。作業ログや学んだことは文章に書いておきましょう。 15 | - 僕たちからどんどん盗めるものは盗んでいってください。コードの書き方、どんどん真似してください。まずは手を動かしてやってみましょう。 16 | - 妥協せずにやりましょう。あなたが納得するまでTreasureの時間をまんべんなくつかってみてください。僕たちも本気で取り組んでいます。 17 | 18 | * 講義概要 19 | 20 | - アイデアソン: サービスをつくれる頭にブレイクしてもらいます by みっきー先生 21 | - Go: 2日間みっちりやる予定です。ごりごりコーディングしてもらいます。(ΦωΦ)フフフ… 22 | - HTTP: Webアプリケーションを書くための基礎を学んでもらいます。 23 | - DB: 三浦さんのスペシャルコースです。 24 | - インフラ / DevOps: アプリケーションエンジニアにも知ってもらいたい基礎をお届けします。僕らが普段どう開発しているかも感じられるはずです。 25 | - チーム: 後半に向けて大事なことをkatzchangから。 26 | 27 | * 明日のGo講義までの事前準備 28 | 29 | ローカルのMySQL環境を整えておいてください。Dockerおすすめ。 30 | 31 | .link https://github.com/VG-Tech-Dojo/treasure2016-pre/tree/master/docs/env 32 | 33 | それと任意のGo製アプリが動く環境を作っておいてください。Go事前課題その2が動いていれば問題ないはず。 34 | 35 | .link https://github.com/VG-Tech-Dojo/treasure2016-pre/tree/master/docs/go 36 | 37 | 以下のアプリを動かせるようにしておいてください。 38 | 39 | .link https://github.com/voyagegroup/gin-boilerplate 40 | .link https://github.com/suzuken/wiki 41 | -------------------------------------------------------------------------------- /2017/treasure-go/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("go go go") 7 | } 8 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func main() { 9 | hello() 10 | } 11 | 12 | func hello() { 13 | for { 14 | time.Sleep(1 * time.Second) 15 | fmt.Println("hello") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/basic2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Task struct { 4 | } 5 | 6 | var numWorkers = 3 7 | 8 | func process(t Task) { 9 | // ... 10 | } 11 | 12 | func worker(ch chan Task) { 13 | for { 14 | task := <-ch 15 | process(task) 16 | } 17 | } 18 | 19 | func main() { 20 | ch := make(chan Task, 3) 21 | for i := 0; i < numWorkers; i++ { 22 | go worker(ch) 23 | } 24 | 25 | tasks := getTasks() 26 | for _, task := range tasks { 27 | ch <- task 28 | } 29 | 30 | // brabra.. 31 | } 32 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/crawl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | type Task struct { 9 | rawurl string 10 | } 11 | 12 | var numWorkers = 3 13 | 14 | func crawl(t Task) error { 15 | resp, err := http.Get(t.rawurl) 16 | if err != nil { 17 | return err 18 | } 19 | defer resp.Body.Close() 20 | log.Printf("crawled: %s", t.rawurl) 21 | // save to file.. 22 | return nil 23 | } 24 | 25 | func worker(ch chan Task) { 26 | for { 27 | task := <-ch 28 | crawl(task) 29 | } 30 | } 31 | 32 | func getTasks() []Task { 33 | return []Task{ 34 | {"https://voyagegroup.com"}, 35 | {"http://ecnavi.jp"}, 36 | } 37 | } 38 | 39 | func main() { 40 | ch := make(chan Task, 3) 41 | for i := 0; i < numWorkers; i++ { 42 | go worker(ch) 43 | } 44 | 45 | tasks := getTasks() 46 | for _, task := range tasks { 47 | ch <- task 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/du/du.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | type info struct { 12 | size int64 13 | name string 14 | } 15 | 16 | func trace(dir string, ch chan<- info) error { 17 | f, err := os.Open(dir) 18 | if err != nil { 19 | return err 20 | } 21 | files, err := f.Readdir(0) 22 | if err != nil { 23 | return err 24 | } 25 | for _, file := range files { 26 | if file.IsDir() { 27 | trace(file.Name(), ch) 28 | continue 29 | } 30 | ch <- info{ 31 | size: file.Size(), 32 | name: file.Name(), 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | func main() { 39 | flag.Parse() 40 | info := make(chan info) 41 | dirs := flag.Args() 42 | if len(dirs) == 0 { 43 | dirs = []string{"."} 44 | } 45 | var wg sync.WaitGroup 46 | for _, d := range dirs { 47 | wg.Add(1) 48 | go func(d string) { 49 | defer wg.Done() 50 | if err := trace(d, info); err != nil { 51 | log.Printf("err: %s", err) 52 | } 53 | }(d) 54 | } 55 | 56 | go func() { 57 | wg.Wait() 58 | close(info) 59 | }() 60 | 61 | var total, files int64 62 | LOOP: 63 | for { 64 | s, ok := <-info 65 | if !ok { 66 | break LOOP 67 | } 68 | // fmt.Printf("%s: %d\n", s.name, s.size) 69 | total += s.size 70 | files++ 71 | } 72 | 73 | fmt.Printf("%d files, %d bytes", files, total) 74 | } 75 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/echoserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // use `nc localhost 8888` for debug 10 | func main() { 11 | listener, err := net.Listen("tcp", "localhost:8888") 12 | if err != nil { 13 | panic(err) 14 | } 15 | for { 16 | conn, err := listener.Accept() 17 | if err != nil { 18 | log.Print(err) 19 | continue 20 | } 21 | go handleConn(conn) 22 | log.Println("connect") 23 | } 24 | } 25 | 26 | func handleConn(c net.Conn) { 27 | io.Copy(c, c) 28 | c.Close() 29 | } 30 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/race.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | ch := make(chan bool) 7 | m := make(map[int]bool) 8 | go func() { 9 | m[1] = false 10 | ch <- true 11 | }() 12 | _ = m[2] 13 | <-ch 14 | for k, v := range m { 15 | fmt.Println(k, v) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/race/race.go: -------------------------------------------------------------------------------- 1 | package race 2 | 3 | import "fmt" 4 | 5 | func f() { 6 | ch := make(chan bool) 7 | m := make(map[int]bool) 8 | go func() { 9 | m[1] = false 10 | ch <- true 11 | }() 12 | _ = m[2] 13 | <-ch 14 | for k, v := range m { 15 | fmt.Println(k, v) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/race/race_test.go: -------------------------------------------------------------------------------- 1 | package race 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFunction(t *testing.T) { 8 | f() 9 | } 10 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/race_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/waitgroup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | type Task struct { 10 | rawurl string 11 | } 12 | 13 | var numWorkers = 3 14 | 15 | func crawl(t Task) error { 16 | resp, err := http.Get(t.rawurl) 17 | if err != nil { 18 | return err 19 | } 20 | defer resp.Body.Close() 21 | log.Printf("crawled: %s", t.rawurl) 22 | // save to file.. 23 | return nil 24 | } 25 | 26 | func worker(ch chan Task) { 27 | for { 28 | task := <-ch 29 | crawl(task) 30 | } 31 | } 32 | 33 | func getTasks() []Task { 34 | return []Task{ 35 | {"https://voyagegroup.com"}, 36 | {"http://ecnavi.jp"}, 37 | } 38 | } 39 | 40 | func main() { 41 | var wg sync.WaitGroup 42 | ch := make(chan Task, 3) 43 | for i := 0; i < numWorkers; i++ { 44 | wg.Add(1) 45 | go func() { 46 | defer wg.Done() 47 | worker(ch) 48 | }() 49 | } 50 | 51 | tasks := getTasks() 52 | for _, task := range tasks { 53 | ch <- task 54 | } 55 | 56 | wg.Wait() 57 | } 58 | -------------------------------------------------------------------------------- /2017/treasure-go/concurrency/waitgroup_primer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | func main() { 9 | var wg sync.WaitGroup 10 | var urls = []string{ 11 | "http://www.golang.org/", 12 | "http://www.google.com/", 13 | "http://www.somestupidname.com/", 14 | } 15 | for _, url := range urls { 16 | // Increment the WaitGroup counter. 17 | wg.Add(1) 18 | // Launch a goroutine to fetch the URL. 19 | go func(url string) { 20 | // Decrement the counter when the goroutine completes. 21 | defer wg.Done() 22 | // Fetch the URL. 23 | http.Get(url) 24 | }(url) 25 | } 26 | // Wait for all HTTP fetches to complete. 27 | wg.Wait() 28 | } 29 | -------------------------------------------------------------------------------- /2017/treasure-go/db/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func UserByAge(db *sql.DB, age int) (names []string, err error) { 13 | rows, err := db.Query( 14 | `select name from users where age = ?`, 15 | age) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer rows.Close() 20 | 21 | for rows.Next() { 22 | var name string 23 | if err := rows.Scan(&name); err != nil { 24 | return nil, err 25 | } 26 | names = append(names, name) 27 | } 28 | return names, nil 29 | } 30 | 31 | func main() { 32 | var ( 33 | age = flag.Int("age", 10, "user's age") 34 | ) 35 | db, err := sql.Open("mysql", "") 36 | if err != nil { 37 | log.Fatalf("open db failed: %s", err) 38 | } 39 | users, err := UserByAge(db, *age) 40 | if err != nil { 41 | log.Fatalf("query failed: %s", err) 42 | } 43 | fmt.Printf("%v", users) 44 | } 45 | -------------------------------------------------------------------------------- /2017/treasure-go/fib/fib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func fib(n int) int { 10 | if n == 0 || n == 1 { 11 | return n 12 | } 13 | return fib(n-1) + fib(n-2) 14 | } 15 | 16 | func main() { 17 | if len(os.Args) < 2 { 18 | fmt.Print("usage: ./fib [integer]") 19 | os.Exit(1) 20 | } 21 | i, err := strconv.Atoi(os.Args[1]) 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "argument must be integer: %s\n", err) 24 | os.Exit(1) 25 | } 26 | fmt.Printf("%d", fib(i)) 27 | } 28 | -------------------------------------------------------------------------------- /2017/treasure-go/fib/fib_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFib(t *testing.T) { 8 | type Case struct { 9 | n, out int 10 | } 11 | cases := []Case{ 12 | {0, 0}, 13 | {1, 1}, 14 | {10, 55}, 15 | } 16 | for i, c := range cases { 17 | if got := fib(c.n); got != c.out { 18 | t.Errorf("#%d: fib(%d) want %d, got %d\n", i, c.n, c.out, got) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /2017/treasure-go/gen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/build" 5 | "os" 6 | "strings" 7 | "text/template" 8 | ) 9 | 10 | var tmpl = template.Must(template.New("").Parse(`// Don't edit! generated by gen.go 11 | 12 | package {{.Package}} 13 | 14 | {{range .Params}}func Handle{{title .Name}}(w http.ResponseWriter, r *http.Request) { 15 | p := r.FormValue("{{.Name}}") 16 | if p == "" { 17 | http.Error(w, "{{.Name}} not specified", http.StatusBadRequest) 18 | return 19 | } 20 | fmt.Fprintf(w, "%s is god.", p) 21 | } 22 | {{end}} 23 | `)) 24 | 25 | func Generate(name string...) error { 26 | pkg, err := build.Default.Import(".", 0) 27 | if err != nil { 28 | return err 29 | } 30 | f, err := os.Create(fmt.Sprintf("%s_gen.go"), strings.ToLower(name)) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | return tmpl.Execute(f, map[string]string{}) 36 | } 37 | -------------------------------------------------------------------------------- /2017/treasure-go/http/curl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | flag.Parse() 13 | rawurl := flag.Arg(0) 14 | resp, err := http.Get(rawurl) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer resp.Body.Close() 19 | 20 | b, err := ioutil.ReadAll(resp.Body) 21 | if err != nil { 22 | panic(err) 23 | } 24 | fmt.Fprint(os.Stdout, string(b)) 25 | } 26 | -------------------------------------------------------------------------------- /2017/treasure-go/http/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "net/http" 5 | 6 | func main() { 7 | // START OMIT 8 | resp, err := http.Get("https://example.com") 9 | // ... 10 | resp, err := http.Post("https://example.com") 11 | // ... 12 | req, err := http.NewRequest("GET", "https://example.com", nil) 13 | // ... 14 | req.Header.Add("X-Treasure", "🍺") 15 | resp, err := http.DefaultClient.Do(req) 16 | // END OMIT 17 | } 18 | -------------------------------------------------------------------------------- /2017/treasure-go/img/legacybook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2017/treasure-go/img/legacybook.jpg -------------------------------------------------------------------------------- /2017/treasure-go/img/tddcycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2017/treasure-go/img/tddcycle.png -------------------------------------------------------------------------------- /2017/treasure-go/intro.slide: -------------------------------------------------------------------------------- 1 | Go入門 2 | Treasure2017版 3 | 00:00 14 Aug 2017 4 | Tags: golang 5 | 6 | Kenta SUZUKI 7 | Gopher at VOYAGE GROUP 8 | @suzu_v 9 | https://github.com/suzuken 10 | 11 | * この資料について 12 | 13 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2017年度版講義資料です。Goの講義は3日間です。 14 | 15 | .link https://voyagegroup.com/internship/treasure/ 16 | 17 | GitHubで公開されています。 18 | 19 | .link https://github.com/voyagegroup/talks 20 | .link https://go-talks.appspot.com/github.com/voyagegroup/talks/2017/treasure-go/intro.slide 21 | 22 | * はじめに 23 | 24 | * となりの人と話しましょう 1分 25 | 26 | - 好きな言語 27 | - 好きなエディタ 28 | - 最近つくってるもの 29 | 30 | * Treasureについて簡単に 31 | 32 | - バックグラウンドの違うみなさんが3週間過ごします 33 | - どんどん話してください 34 | - コードを書いて、質問して、がっつり勉強してください 35 | 36 | Enjoy! :) 37 | 38 | * 3日間で学んでほしいこと 39 | 40 | - Goで小さなコマンドラインツールを書けるようになること 41 | - 一通り小さなWebアプリケーションを書けるようになること 42 | - Goのコードの書き方に慣れること 43 | 44 | とにかくどんどんコードを書きましょう。 45 | 46 | * アウトライン 47 | 48 | - なぜGoなのか? 49 | - 事前課題解説 50 | - testing 51 | - net/http 52 | - encoding/json 53 | - html/template 54 | - database/sql 55 | - Concurrency 56 | 57 | .image https://golang.org/doc/gopher/project.png 58 | 59 | * なぜGoを使うのか? 60 | 61 | いろいろな良い点があります。 62 | 63 | - 読みやすい 64 | - 言語仕様がfixしている 65 | - 軽快なbuild / 高速な動作 / concurrencyサポート 66 | - ツール (go tool, gofmt ...) 67 | - ほどよい標準ライブラリ 68 | 69 | Goの一番の特徴はSimplicityだと思います。 70 | 71 | .link https://talks.golang.org/2015/simplicity-is-complicated.slide#1 Simplicity is Complicated 72 | 73 | * 準備: Tour of Go 74 | 75 | .link https://tour.golang.org 76 | 77 | - まずTour of Goおすすめ 78 | - 言語仕様が簡単に学べる 79 | - ほどよい演習 80 | 81 | 演習やってないひとはやりましょう 82 | 83 | .link https://gist.github.com/suzuken/d892c42e56c986bca813 84 | 85 | * 準備: How to write Go code 86 | 87 | .link https://golang.org/doc/code.html 88 | 89 | - `$GOPATH/bin` をPATHに追加しましょう 90 | - これから書いてもらうコードもGoのワークスペースに則って配置して貰う必要があります。 91 | - 例えば `$GOPATH/src/github.com/suzuken/wiki` などに配置します。go getをつかうとこのルールに従って配置されます。 92 | 93 | tips: ghqとpeco 94 | 95 | .link https://github.com/motemen/ghq 96 | .link https://github.com/peco/peco 97 | 98 | DEMOします。 99 | 100 | * 手続き、コマンドラインツール、そしてテスト 101 | 102 | * 事前課題その1 103 | 104 | n番目のフィボナッチ数を返す手続きを実装してください。 105 | 106 | f(0) = 0 107 | f(1) = 1 108 | f(n+2) = f(n) + f(n+1) 109 | 110 | .link https://gist.github.com/suzuken/a8ec8fc95cd0c0d18d8d5584fdd4f3ab 111 | 112 | * 事前課題その1 フィボナッチ数 解答例 113 | 114 | func fib(n int) int { 115 | if n == 0 { 116 | return 0 117 | } 118 | if n == 1 { 119 | return 1 120 | } 121 | return fib(n-1) + fib(n-2) 122 | } 123 | 124 | テストを書いてからリファクタリングしてみましょう。 125 | 126 | * 開発者テスト 127 | 128 | テスト大事 129 | 130 | .image https://connpass-tokyo.s3.amazonaws.com/event/27540/41d84cf0e6494e2e91e51ad8e9c85310.png 131 | 132 | .link https://golang.org/pkg/testing/ 133 | 134 | * テストのないコードはレガシーコードである 135 | 136 | .image ./img/legacybook.jpg 137 | 138 | * TDDのリズム 139 | 140 | .image ./img/tddcycle.png _ 600 141 | .caption [[https://www.slideshare.net/t_wada/osh2014-sprit-of-tdd][TDDのこころ by twada]] 142 | 143 | * フィボナッチのテスト (発展課題 その1) 144 | 145 | .code fib/fib_test.go /func TestFib/,/^}/ 146 | 147 | .link https://github.com/golang/go/wiki/TableDrivenTests 148 | 149 | $ go test -v ./... 150 | === RUN TestFib 151 | --- PASS: TestFib (0.00s) 152 | PASS 153 | ok github.com/voyagegroup/talks/2017/treasure-go/fib 0.006s 154 | 155 | 156 | * リファクタリング 157 | 158 | .code fib/fib.go /func fib/,/^}/ 159 | 160 | 書き換えたらテストをします。 161 | 162 | $ go test -v ./... 163 | === RUN TestFib 164 | --- PASS: TestFib (0.00s) 165 | PASS 166 | ok github.com/voyagegroup/talks/2017/treasure-go/fib 0.006s 167 | 168 | リファクタリング前にはテストを書きましょう。 169 | 170 | * コマンドラインツールをつくろう(発展課題 その2) 171 | 172 | Goで `go run` させられるのは `main` パッケージのみ。 173 | 174 | .play basic/basic.go 175 | 176 | `fib` をするコマンドラインツールをつくってみましょう。 177 | 178 | * os.Args をつかう 179 | 180 | .play fib/fib.go /func main/,/^}/ 181 | 182 | ほとんどの場合は `flag` をつかったほうが楽です。 183 | 184 | .link https://golang.org/pkg/flag/ 185 | 186 | * よくしたレビューコメント 187 | 188 | 可読性大事。可読性のあるコードは信頼できるコードです。 189 | 190 | - `gofmt` おねがいします 191 | - `main` 以外で `os.Exit` や `panic` するのは大抵好ましくないです 192 | - `error` はよっぽど自明じゃないかぎり無視しないように 193 | - 変数や手続き名は `snake_case` ではなくて `camelCase` 194 | - 変数名は短く 195 | - 余分なelse: インデントを最小にしましょう 196 | - テストパターンが複数あるときはTableDrivenTestsおすすめ 197 | 198 | .link https://golang.org/s/style 199 | .link https://blog.golang.org/go-fmt-your-code 200 | .link https://github.com/golang/go/wiki/TableDrivenTests 201 | 202 | * Goに入ってはGoに従え 203 | 204 | .link https://ukai-go-talks.appspot.com/2014/gocon.slide#1 205 | 206 | 言語ごとに作法が違う 207 | *作法にかなったやりかた* で書きましょう。 208 | 209 | * TDD実習 & ペアプログラミング 210 | 211 | * TDD実習の説明 212 | 213 | - これからのとなりの人と一緒にプログラムを書いてもらいます 214 | - どちらか1台のPCをつかいます 215 | - どちらか一方がテストを、どちらか一方が実装側を書きます 216 | 217 | デモします。 218 | 219 | * TDD実習 その1: スタック 30分 220 | 221 | - スタックを作成してください 222 | 223 | .image https://upload.wikimedia.org/wikipedia/commons/b/b4/Lifo_stack.png _ 350 224 | 225 | // 簡単のため、stringしかいれられないようにしています 226 | type Stack struct{} 227 | func (s *Stack) Pop() string {} 228 | func (s *Stack) Push(ss string) {} 229 | 230 | 内部のデータ構造はsliceをつかうといいでしょう。 231 | 232 | .link https://blog.golang.org/go-slices-usage-and-internals 233 | 234 | * 動作例 235 | 236 | s := &Stack{} 237 | s.Push("dataA") 238 | s.Push("dataB") 239 | s.Push("dataC") 240 | s.Pop() // -> "dataC" 241 | s.Pop() // -> "dataB" 242 | s.Push("dataD") 243 | s.Pop() // -> "dataD" 244 | s.Pop() // -> "dataA" 245 | s.Pop() // -> "" 246 | 247 | * TDD実習 その2: 制限付きスタック 10分 248 | 249 | - スタックに最大長を設定できるようにしてください 250 | - 最大長を超えるPushについては、古いものから削除されるようにしましょう。 251 | 252 | 動作例 253 | 254 | s := &Stack{limit: 2} 255 | s.Push("dataA") 256 | s.Push("dataB") 257 | s.Push("dataC") 258 | s.Pop() // -> "dataC" 259 | s.Pop() // -> "dataB" 260 | s.Pop() // -> "" 261 | 262 | インタフェースについては自分たちで相談して決めてみましょう。 263 | 264 | 発展 265 | 266 | - あとから制限長を変更できるようにしましょう 267 | 268 | * testing振り返り 269 | 270 | - どんなテストをかけばいいかを考えるのは最初は難しいかもしれません。 271 | - `testing` パッケージではふつうのGoコードでテストを書くことができます。テストを書くのに特別な知識は必要ありません。 272 | - ユニットテストを書くことで既存の機能が正常に動いていることを担保しつつ、新しい機能の追加を手助けしてくれます。 273 | - デバッグプリントではなく気軽にテスト書いていきましょう。 274 | 275 | * net/http 入門 276 | 277 | * net/http: Request OverView 278 | 279 | .code http/example.go /START OMIT/,/END OMIT/ 280 | 281 | .link https://golang.org/pkg/net/http/ 282 | 283 | * net/http 実習(30分): curl実装 284 | 285 | 以下を実装してみましょう。 286 | 287 | $ curl example.com 288 | 289 | 正常系については同等に、エラーの場合については適宜わかりやすいメッセージを出力するようにしてください。 290 | 291 | できたら以下もやってみましょう。 292 | 293 | $ curl --header "X-Treasure: 🍺" example.com 294 | $ curl -H "Content-Type: application/json" -X POST -d '{"ajito":"🍺"}' example.com 295 | 296 | 全部できたら好きなcurlのオプションを実装してみましょう。 (例: `-b`, `-c`, `-A` etc.) 297 | 298 | *発展課題*: このコマンドのテストを書いてみましょう。(ポイント: コマンドラインツールとしてのテストと内部APIのテストを分けて考えましょう。) 299 | 300 | * 余談: SimpleとEasy 301 | 302 | .link http://php.net/file_get_contents 303 | .link http://eed3si9n.com/ja/simplicity-matters シンプルさの必要性 304 | .link https://www.amazon.co.jp/dp/4274064069 UNIXという考え方―その設計思想と哲学 305 | 306 | : シンプルさはあとから追加できない 307 | 308 | * 事前課題その2: net/http 演習 309 | 310 | - 指定したURLにあるコンテンツについて、タイトルとdescriptionを抜き出すツールを書きましょう 311 | - HTTPサーバとして実装してこの機能をつかえるようにしましょう 312 | 313 | .link https://gist.github.com/suzuken/b456e0f4679f86da572839d6d86f159e 314 | 315 | $ go run scraper/scraper.go& 316 | # 自分でつくったcurlを使ってももちろんOK 317 | $ curl -D - "http://localhost:8080?url=https://golang.org" 318 | HTTP/1.1 200 OK 319 | Content-Type: application/json 320 | Date: Fri, 05 Aug 2017 07:45:23 GMT 321 | Content-Length: 57 322 | 323 | {"title":"The Go Programming Language","description":""} 324 | 325 | この課題の狙いは、ある仕事をHTTPサーバに組み込むことを試してもらうことです。適切に仕事をわけて実装すれば、楽に組み込むことができます。 326 | 327 | * net/http.(*Server) 328 | 329 | Goの標準ライブラリでは簡単にHTTPサーバを立ち上げることができます。 330 | 331 | func main() { 332 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 333 | // wはio.Writerなので書き込むことができます 334 | fmt.Fprint(w, "hello world") 335 | }) 336 | http.ListenAndServe(":8080", nil) 337 | } 338 | 339 | and 340 | 341 | $ go run server.go& 342 | $ curl http://localhost:8080 343 | hello world 344 | 345 | .link https://golang.org/pkg/fmt/#Fprint 346 | .link https://golang.org/pkg/net/http/ 347 | 348 | * encoding/json 349 | 350 | .code scraper/scraper.go /type Page/,/^}/ 351 | 352 | and 353 | 354 | enc := json.NewEncoder(w) 355 | if err := enc.Encode(p); err != nil { 356 | // ... 357 | } 358 | 359 | すると io.Writer である `w` にJSONが出力されます。 360 | 361 | .link https://golang.org/pkg/encoding/json/ 362 | 363 | 364 | * 2の解答例 365 | 366 | .code scraper/scraper.go /START OMIT/,/END OMIT/ 367 | 368 | .link https://gist.github.com/suzuken/e211f01171e2a841ddf6417e479004e0 369 | 370 | * 2の解答例 (cont.) 371 | 372 | .code scraper/scraper.go /func handler/,/^}/ 373 | 374 | .link https://gist.github.com/suzuken/e211f01171e2a841ddf6417e479004e0 375 | 376 | * io.Writer またはインタフェース 377 | 378 | type Writer interface { 379 | Write(p []byte) (n int, err error) 380 | } 381 | 382 | .link https://golang.org/pkg/io/#Writer 383 | 384 | `io.Writer` を出力先としてサポートすると、Goの標準的なインタフェースを利用した機能に出力する機能を提供することができます。例えば `os.Stdout` なども `io.Writer` を実装しています。 385 | 386 | .link http://mattn.kaoriya.net/software/lang/go/20140501172821.htm Big Sky :: Golang のオフィシャルが提供するインタフェースまとめ 387 | .link http://spf13.com/presentation/7-biggest-mistakes-in-go/ 7 Common mistakes in Go and when to avoid them 388 | 389 | * 実習 net/http: 複数のページ取得 45分 390 | 391 | 事前課題その2でつくったHTTPサーバに以下の機能を追加しましょう。 392 | 393 | - URLを複数受け取り、複数ページのtitleとdescriptionを取ってこれるようにしましょう。パラメータの渡し方はお好みで。(カンマ区切りなど) 394 | - 結果はJSONの配列で返すようにしましょう。その際、どのURLからどの結果が取得できたかをわかるようにしてください。 395 | - `og:title`, `og:image` などの [[http://ogp.me/][Open Graph protocol]]に含まれるデータもあれば取得し、同様にJSONで返せるようにしてください。 396 | 397 | 発展課題 398 | 399 | - 各ページへのHTTPリクエストのタイムアウトを設定し、長く時間のかかったURLは無視できるようにしましょう。正常に取得できた結果のみ返すようにします。 400 | - URLごとにページの取得を並列化しましょう。すべてのページの結果が揃ったら結果を返すようにしてみましょう。 401 | - `net/http/httptest` をつかってこの機能のテストを書いてみましょう。 402 | 403 | * Concurrency 404 | 405 | * Goのconcurrency 406 | 407 | - goroutine: 独立してタスクを実行する。並列にできる。 408 | - channel: goroutineのコミュニケーションと同期を担当。 409 | 410 | > Do not communicate by sharing memory; instead, share memory by communicating. 411 | 412 | .link https://golang.org/doc/effective_go.html#concurrency 413 | .link https://golang.org/ref/mem 414 | 415 | * 例: 同時接続数1のEcho Server 416 | 417 | .code concurrency/echoserver.go /func main/,/^}/ 418 | 419 | コネクションのハンドリングは以下の通り。 420 | 421 | .code concurrency/echoserver.go /func handleConn/,/^}/ 422 | 423 | `nc localhost 8888` でつないでみてください。 424 | 425 | * 例: 並列リクエスト 426 | 427 | .code concurrency/waitgroup_primer.go /func main/,/^}/ 428 | 429 | .link https://golang.org/pkg/sync/#example_WaitGroup 430 | 431 | * Basic: Task Queue 432 | 433 | .code concurrency/basic2.go /func main/,/^}/ 434 | 435 | * Basic: Task Queue 436 | 437 | .code concurrency/basic2.go /func process/,/^}/ 438 | .code concurrency/basic2.go /func worker/,/^}/ 439 | 440 | * Basic: Channel 441 | 442 | - goroutine-safe 443 | - goroutine間で値を受け渡しできる 444 | - FIFO semantics 445 | - goroutineのblock / unblockが可能 446 | 447 | .link https://www.youtube.com/watch?v=KBZlN0izeiY&index=6&list=PL2ntRZ1ySWBdD9bru6IR-_WXUgJqvrtx9 GopherCon 2017: Understanding Channels 448 | 449 | * Data Race 450 | 451 | mapはgoroutine-safeではない。 452 | 453 | .code concurrency/race/race.go /func f/,/^}/ 454 | 455 | Data Raceは2つのgoroutineが同じ変数にアクセスしており、そのうち少なくとも1つが書き込みである場合に発生する。 456 | 457 | .link https://tip.golang.org/pkg/sync/#Map 458 | 459 | * Race Detector 460 | 461 | .link https://golang.org/doc/articles/race_detector.html Data Race Detector 462 | 463 | $ go test -race ./concurrency/race 464 | 465 | ただしすべてのraceが見つかるわけではない。 466 | 467 | * Concurrency演習1 (20分) 468 | 469 | - Stack実装をgoroutine-safeに書き直してください(おそらく既にgoroutine-safeに実装されている場合がほとんどだと思います) 470 | - 実際に複数のgoroutineからアクセスしてみてください 471 | - race detectorでデータ競合が検出されないことを確認して下さい 472 | 473 | * Concurrencyは設計 474 | 475 | .link https://talks.golang.org/2012/waza.slide#1 Concurrency is not Parallelism 476 | 477 | - Webサーバが数万のリクエストをさばく 478 | - スマートフォンで複数のAPIにバックグラウンドでリクエストを送る 479 | - 一方のファイルからデータを読み取り、もう一方に書き込むバッチ 480 | 481 | 身近なところで(もしかしたら気にせずに)Concurrencyを使っています。 482 | 483 | _「並列にしたら遅くなったんだけど?」_ 484 | 485 | .link http://freak-da.hatenablog.com/entry/20100915/p1 parallel と concurrent、並列と並行の違い 486 | 487 | * Concurrency演習2 (30分): duを実装しなさい 488 | 489 | duの簡易版実装をしましょう。複数のディレクトリを引数にとり、合計のファイル数及び合計のバイト数を計算して出力します。 490 | 491 | ./du dir1 dir2 dir3 492 | 4 files, 586454 bytes 493 | 494 | まずはgoroutineを使わないで実装してみましょう。その後、いろんなConcurrencyパターンを試してみてください。 495 | 496 | - ディレクトリごとにgoroutine 497 | - 1階層下のtreeごとにgoroutine 498 | - あるいはあなたの考えた最高のパターン 499 | 500 | 実行時間を計測してどのパターンが速いか調べてみましょう。 501 | 502 | * Concurrencyまとめ 503 | 504 | - Goではgoroutineとchannelを使うことで既存実装をConcurrencyを利用した世界に適用できます。 505 | - ただしデータ競合には気をつけましょう。 506 | - 並列にする前に直列で十分ではないかどうか考えましょう。適切な箇所でConcurrencyを使いましょう。 507 | 508 | * 小さなWebアプリケーションを実装しよう 509 | 510 | * ところで 511 | 512 | Webアプリケーションってなんでしょう? 513 | 514 | - HTTPリクエスト受け付ければWebアプリケーション? 515 | - フォームがあるもの? 516 | - たくさんページがあればWebアプリ? 517 | - ユーザが使えばWebアプリ? 518 | - 入出力によってページ内容がかわればWebアプリ? 519 | - TwitterやGoogleはWebアプリ? 520 | 521 | Webアプリとひとえにいってもいろいろありますね。 522 | 523 | * 実アプリケーションの例 524 | 525 | .link https://github.com/suzuken/wiki 526 | 527 | デモしつつ解説します。MySQLがあれば手元でも動かせるので、やってみてください。 528 | 機能は以下のもののみです。 529 | 530 | - ユーザ登録とログイン・ログアウト 531 | - 記事の投稿・編集・削除 532 | 533 | このあとの実習でこのアプリケーションに機能を追加してもらいます。 534 | 535 | .link https://gist.github.com/suzuken/cf301c11800f4558e12f4d28dcbefe1c MySQLの立ち上げについて 536 | 537 | cloneしたら `go get -d -v .` してください。 538 | 539 | 540 | * よくあるWebアプリケーションの構成 541 | 542 | 今回はいわゆるMVCの構成にしてみました。Goのpackageの機能をつかってディレクトリをわけているだけです。 543 | 544 | コードをみつつ、1つ1つ中身を解説します。 545 | 546 | .image https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/MVC-Process.svg/1000px-MVC-Process.svg.png 300 _ 547 | 548 | * なぜこういう構成なの? 549 | 550 | Webアプリケーションによくある仕事をわけると、下の3つの層に分けることができます。 551 | 552 | - データソースからの参照、あるいは更新をする層 553 | - ユーザへの見せ方をいい感じに整える層 554 | - HTTPリクエストをうけて、仕事を制御して振り分ける層 555 | 556 | .link http://www.slideshare.net/brtriver/php-14295877 フラットなPHPからフレームワークへ 557 | 558 | 仕事をわけることで、各実装をシンプルにすることができます。ただし分割しすぎるのはよくありません。またGo特有ですが、適切にpackageを分割することによるメリットもあります。 559 | 560 | - 構造体やインタフェースの名前をシンプルにできる。 561 | - 依存関係が明瞭になり、package単位のテストを書きやすくなる。 562 | 563 | * テンプレート 564 | 565 | * WebアプリケーションにおけるHTMLテンプレート 566 | 567 | - 構造化してviewをつくる 568 | - 適切にエスケープをしてHTMLを出力する 569 | 570 | クライアントサイドでテンプレートを担当する場合、サーバサイドでテンプレートを使う必要性は減ります。 571 | 572 | .link http://www.slideshare.net/ockeghem/xssreintroduction 573 | 574 | * html/template の利用例 575 | 576 | .play template/template.go /func main/,/^}/ 577 | 578 | * 実習: コメント欄の追加 579 | 580 | - 記事にコメント欄をつけてみましょう 581 | 582 | テンプレートを追加して、フォーム作って、送信、あたりを作る必要があります。エンドポイントも新しくつくりましょう。ただ、ちゃんとコメントが保存されなくてもいいです。(DB実習をしてから、ちゃんとやることにしましょう。) 583 | 584 | 発展課題 585 | 586 | - コメント内容をメモリ上に保存できるようにしてみましょう。適当な `map[int][]string` なんかにいれるといいかも?誰のコメントかとりあえず気にしなくてもいいです。 587 | - 誰のコメントかも保存できるようにしてみましょう。`type Comment struct` をつくって上の `map` の値としていれられるといいかもしれません。 588 | 589 | * 実習: CSRF対策 590 | 591 | wikiのフォームにトークンが入っていることに気が付きましたか? 592 | 593 | 594 | 595 | .link https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/301.html IPA CSRF対策 596 | 597 | 実習: CSRFトークンをブラウザの開発ツールで上書きして、リクエストが失敗することを確認してみましょう。 598 | 599 | CSRF, XSSまわりなどについては来週の [[https://twitter.com/co3k][@co3k]] のセキュリティ講義で詳しくやります。 600 | 601 | * Goとデータベース 602 | 603 | * ここまでの振り返り 604 | 605 | - MVCのCとVをやりました。 606 | 607 | .image https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/MVC-Process.svg/1000px-MVC-Process.svg.png 300 _ 608 | 609 | 残りのM: Modelをみていきましょう。 610 | 611 | コメント欄の実習でメモリに `map` で保持してもらいました。ここを永続化する、ということをやっていきます。 612 | 613 | * Goとデータベース: database/sql 614 | 615 | Goからデータベースを扱うときは `database/sql` を使います。 616 | 617 | .link https://golang.org/pkg/database/sql/ 618 | .link https://golang.org/s/sqlwiki 619 | .link https://golang.org/s/sqldrivers 620 | 621 | MySQLでの利用例は以下を見つつ説明します。 622 | 623 | .link https://github.com/go-sql-driver/mysql/wiki/Examples 624 | 625 | 以下の資料もおすすめです。 626 | 627 | .link http://go-database-sql.org/ 628 | 629 | * クエリするときの原則 630 | 631 | - クエリにパラメータを埋め込むときは必ずプリペアドステートメントを使いましょう。 632 | - 決して `fmt.Sprintf` などによる文字列結合でクエリを組み立ててはいけません。 633 | 634 | .link https://www.ipa.go.jp/files/000024396.pdf SQLインジェクション対策について 635 | .link http://www.ipa.go.jp/files/000017320.pdf 安全なSQLの呼び出し方 636 | .link http://d.hatena.ne.jp/ajiyoshi/20100409/1270809525 637 | 638 | Prepare, Query、どちらでもプリペアドステートメントが使えます。 639 | 640 | .link https://golang.org/pkg/database/sql/#DB.Prepare 641 | .link https://golang.org/pkg/database/sql/#DB.Query 642 | 643 | db.Query(`select * from users where name = ?`, "s-tanno") 644 | 645 | * DB実習: 一通り自分でDB操作をしてみよう 646 | 647 | MySQLに自分用の日報を保存するコマンドラインアプリケーションをつくりましょう。 648 | 649 | - 日報用のテーブル `reports` を用意し、保存できるようにしてください。更新、削除の機能はお好みで。テーブルは自分で考えてみましょう。 650 | 651 | 発展課題 652 | 653 | - いい感じに日報を整形して標準出力できるようなオプションをつけてみましょう。 654 | - 日報にタグを追加できるようにしてみましょう。タグによって日報を取得できるようにすると便利そうです。 655 | - Webアプリにしてみましょう。type Report struct に `ServeHTTP` を実装するのがおすすめです。 656 | 657 | .link https://golang.org/pkg/net/http/#Handler 658 | 659 | * データベースマイグレーション 660 | 661 | - 複数人でアプリケーションをつくるときにスキーマの管理どうしますか? 662 | - データベースマイグレーションをつかうと便利です 663 | 664 | .link https://github.com/rubenv/sql-migrate 665 | 666 | `suzuken/wiki` でのセットアップにすでにつかってもらっていました。`migrations` ディレクトリにSQLが入っています。 `sql-migrate` からこれらを実行できます。 667 | 668 | sql-migrate up 669 | 670 | たとえば新しいテーブルを追加するときやスキーマを変更するときには、SQLファイルを `migrations` 以下にコミットするといいです。すると機能とセットでDBスキーマの変更もコードで管理することができます。 671 | 672 | 後半環境ではDBマイグレーションをいい感じに扱う仕組みも用意しました。詳しくは [[https://github.com/s-tajima][@s-tajima]] [[https://github.com/nishigori][@nishigori]] の講義をお楽しみに。 673 | 674 | * DB実習 675 | 676 | wikiの記事につけたコメントをちゃんとDBに保存できるようにしてみましょう。 677 | 678 | テーブル例 679 | 680 | CREATE TABLE `comments` ( 681 | `comment_id` int(11) NOT NULL AUTO_INCREMENT, 682 | `article_id` int(11) NOT NULL, 683 | `body` TEXT NOT NULL, 684 | `created` timestamp NOT NULL DEFAULT NOW(), 685 | `updated` timestamp NOT NULL DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP, 686 | PRIMARY KEY (`comment_id`) 687 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='list of comments'; 688 | 689 | - データベースマイグレーションも用意してあるので、つかってみるといいでしょう 690 | 691 | 発展課題 692 | 693 | - コメントについて誰がコメントしたかも保存できるようにしましょう 694 | - コメントの編集削除をサポートしましょう 695 | - トップページに記事のコメント数も出すようにしましょう 696 | 697 | * Tips: クエリ結果のstructへのbind 698 | 699 | reflectをつかってstructにbindする方法があります。 700 | 701 | .link https://github.com/jmoiron/sqlx 702 | .link https://github.com/naoina/genmai 703 | 704 | `suzuken/go-todo` では `sqlx` をつかっています。 705 | 706 | またはコード生成をつかうのも良い選択でしょう。 `database/sql` のための `Scan` 用手続きをstructから生成すると便利です。 `suzuken/wiki` では `scaneo` をつかっています。 707 | 708 | .link https://blog.golang.org/generate 709 | .link https://github.com/variadico/scaneo 710 | 711 | * コード生成 712 | 713 | デモします 714 | 715 | * サンプルアプリケーションの説明 その2: TODOアプリ 716 | 717 | フロントをReactで実装したSPAのTODOアプリです。 718 | 719 | .link https://github.com/suzuken/go-todo 720 | 721 | `suzuken/wiki` にも似ていますが、以下を変えています。スマホアプリがフロントになる場合にも、設計は似ているかと思います。 722 | 723 | - JSON APIでクライアントとやりとり 724 | - RESTfulなAPI設計 725 | 726 | コードをみつつ説明します。 727 | 728 | .link https://facebook.github.io/react/docs/thinking-in-react.html Thinking in React 729 | 730 | ModelやControllerの実装はwikiとそれほど変わりません。viewのレイヤはまるっとReactで書いています。 731 | 732 | * 実習: アプリケーションを拡張してみよう wiki編 733 | 734 | - 投稿したユーザを保存できるようにしましょう 735 | - 自分の投稿を一覧で見られるようにしましょう 736 | - 投稿に「いいね!」できるようにしてみましょう 737 | 738 | 発展課題 739 | 740 | - 記事の閲覧数を保存できるようにしてみましょう。 741 | - 人気の投稿一覧を見られるようにしましょう。どんな投稿が人気かは自分で決めてよいです。 742 | - 本文中に他の記事のタイトルがあれば自動でリンクを貼るようにしてみましょう。(これをやるとwikiっぽくなりますね) 743 | - 記事の編集履歴をみれるようにしてみましょう。 744 | - Markdownでかけるようにしてみましょう。 745 | 746 | * 実習: アプリケーションを拡張してみよう TODOアプリ編 747 | 748 | - TODOにコメントを追加できるようにしてみましょう: React側のComponentも拡張するといいでしょう 749 | - ログインできるようにしてみましょう 750 | 751 | 発展課題 752 | 753 | - TODOの進捗率を表示できるようにしてみましょう。例えば10タスクあって8タスクおわっているなら進捗80%といった具合に。 754 | - TODOに画像を設定できるようにしてみましょう。 755 | - structからRestfulなAPIをコード生成でつくってみましょう。 `Scan` を生成しているのと同じ要領で、ModelとControllerを生成するとよいでしょう。 756 | - GitHubアカウントでログインできるようにしてみましょう 757 | 758 | * HTTPサーバを書くときに考えることはたくさんある 759 | 760 | - どのようなHTTPリクエストを受け取るか 761 | - どのようなルーティングにすべきか 762 | - HTTPレスポンスステータスは何にすべきか 763 | - HTTPレスポンスヘッダには何を入れるべきか 764 | - HTTPレスポンスボディにはどのような形式で返すべきか 765 | 766 | より詳しくは [[https://twitter.com/gomachan46][@gomachan46]] の講義でやります 767 | -------------------------------------------------------------------------------- /2017/treasure-go/report/report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "time" 7 | ) 8 | 9 | type Report struct { 10 | Title string 11 | Body string 12 | } 13 | 14 | func (r *Report) Insert(db *sql.DB) (sql.Result, error) { 15 | stmt, err := db.Prepare(`insert into (title, body) values(?, ?)`) 16 | if err != nil { 17 | return err 18 | } 19 | defer stmt.Close() 20 | return stmt.Exec(r.Title, r.Body) 21 | } 22 | 23 | func Select(db *sql.DB) ([]Report, error) { 24 | rows, err := db.Query(`select * from reports`) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer rows.Close() 29 | 30 | reports := make([]Report{}) 31 | for rows.Next() { 32 | var title string 33 | var body string 34 | if err := rows.Scan(&title, &body); err != nil { 35 | return nil, err 36 | } 37 | r := Report{Title: title, Body: body} 38 | reports = append(reports, r) 39 | } 40 | return reports, nil 41 | } 42 | 43 | func main() { 44 | var ( 45 | title = flag.String("title", "kuke", "cool title") 46 | body = flag.String("body", "treasure", "cool body") 47 | ) 48 | flag.Parse() 49 | db, err := sql.Open("sqlite3", ":memory:") 50 | if err != nil { 51 | log.Fatalf("open failed: %s", err) 52 | } 53 | r := &Report{*title, *body} 54 | if _, err := r.Insert(db); err != nil { 55 | log.Fatalf("insert failed: %s", err) 56 | } 57 | 58 | result, err := Select(db) 59 | if err != nil { 60 | log.Fatalf("select failed: %s", err) 61 | } 62 | fmt.Printf("%v", result) 63 | } 64 | -------------------------------------------------------------------------------- /2017/treasure-go/scraper/scraper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "golang.org/x/net/html" 9 | ) 10 | 11 | type Page struct { 12 | Title string `json:"title"` 13 | Description string `json:"description"` 14 | } 15 | 16 | func Get(url string) (*Page, error) { 17 | resp, err := http.Get(url) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer resp.Body.Close() 22 | 23 | doc, err := html.Parse(resp.Body) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // START OMIT 29 | page := &Page{} 30 | var f func(*html.Node) 31 | f = func(n *html.Node) { 32 | if n.Type == html.ElementNode && n.Data == "title" { 33 | page.Title = n.FirstChild.Data 34 | } 35 | if n.Type == html.ElementNode && n.Data == "meta" { 36 | if isDescription(n.Attr) { 37 | for _, attr := range n.Attr { 38 | if attr.Key == "content" { 39 | page.Description = attr.Val 40 | } 41 | } 42 | } 43 | } 44 | for c := n.FirstChild; c != nil; c = c.NextSibling { 45 | f(c) 46 | } 47 | } 48 | f(doc) 49 | // END OMIT 50 | return page, nil 51 | } 52 | 53 | func isDescription(attrs []html.Attribute) bool { 54 | for _, attr := range attrs { 55 | if attr.Key == "name" && attr.Val == "description" { 56 | return true 57 | } 58 | } 59 | return false 60 | } 61 | 62 | func handler() { 63 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 64 | rawurl := r.FormValue("url") 65 | if rawurl == "" { 66 | http.Error(w, "url not specified", http.StatusBadRequest) 67 | return 68 | } 69 | p, err := Get(rawurl) 70 | if err != nil { 71 | http.Error(w, "request failed", http.StatusInternalServerError) 72 | return 73 | } 74 | w.Header().Set("Content-Type", "application/json") 75 | enc := json.NewEncoder(w) 76 | if err := enc.Encode(p); err != nil { 77 | http.Error(w, "encoding failed", http.StatusInternalServerError) 78 | return 79 | } 80 | }) 81 | } 82 | 83 | func main() { 84 | handler() 85 | log.Fatal(http.ListenAndServe(":8080", nil)) 86 | } 87 | -------------------------------------------------------------------------------- /2017/treasure-go/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | type Stack struct { 4 | s []interface{} 5 | } 6 | 7 | func (s *Stack) Pop() interface{} { 8 | return s.s[len(s.s)-1] 9 | } 10 | 11 | func (s *Stack) Push(i interface{}) { 12 | s.s = append(s.s, i) 13 | } 14 | -------------------------------------------------------------------------------- /2017/treasure-go/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPushAndPop(t *testing.T) { 8 | s := &Stack{} 9 | s.Push(1) 10 | if r := s.Pop(); r != 1 { 11 | t.Fatalf("want 1, got %v", r) 12 | } 13 | if r := s.Pop(); r != nil { 14 | t.Fatalf("want nil, got %v", r) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /2017/treasure-go/template/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | tmpl, err := template.New("").Parse(` 11 | 12 | 13 | {{.Title}} 14 | 15 | 16 | {{.Body}} 17 | 18 | 19 | `) 20 | if err != nil { 21 | log.Fatalf("compile template failed: %s", err) 22 | } 23 | if err := tmpl.Execute(os.Stdout, map[string]interface{}{ 24 | "Title": "cool title", 25 | "Body": "here is body", 26 | }); err != nil { 27 | log.Fatalf("execute template failed: %s", err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /2018/treasure-go/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("go go go") 7 | } 8 | -------------------------------------------------------------------------------- /2018/treasure-go/db/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func UserByAge(db *sql.DB, age int) (names []string, err error) { 13 | rows, err := db.Query( 14 | `select name from users where age = ?`, 15 | age) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer rows.Close() 20 | 21 | for rows.Next() { 22 | var name string 23 | if err := rows.Scan(&name); err != nil { 24 | return nil, err 25 | } 26 | names = append(names, name) 27 | } 28 | return names, nil 29 | } 30 | 31 | func main() { 32 | var ( 33 | age = flag.Int("age", 10, "user's age") 34 | ) 35 | db, err := sql.Open("mysql", "") 36 | if err != nil { 37 | log.Fatalf("open db failed: %s", err) 38 | } 39 | users, err := UserByAge(db, *age) 40 | if err != nil { 41 | log.Fatalf("query failed: %s", err) 42 | } 43 | fmt.Printf("%v", users) 44 | } 45 | -------------------------------------------------------------------------------- /2018/treasure-go/fib/fib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func fib(n int) int { 10 | if n == 0 || n == 1 { 11 | return n 12 | } 13 | return fib(n-1) + fib(n-2) 14 | } 15 | 16 | func main() { 17 | if len(os.Args) < 2 { 18 | fmt.Print("usage: ./fib [integer]") 19 | os.Exit(1) 20 | } 21 | i, err := strconv.Atoi(os.Args[1]) 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "argument must be integer: %s\n", err) 24 | os.Exit(1) 25 | } 26 | fmt.Printf("%d", fib(i)) 27 | } 28 | -------------------------------------------------------------------------------- /2018/treasure-go/fib/fib_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFib(t *testing.T) { 8 | type Case struct { 9 | n, out int 10 | } 11 | cases := []Case{ 12 | {0, 0}, 13 | {1, 1}, 14 | {10, 55}, 15 | } 16 | for i, c := range cases { 17 | if got := fib(c.n); got != c.out { 18 | t.Errorf("#%d: fib(%d) want %d, got %d\n", i, c.n, c.out, got) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /2018/treasure-go/gen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/build" 5 | "os" 6 | "strings" 7 | "text/template" 8 | ) 9 | 10 | var tmpl = template.Must(template.New("").Parse(`// Don't edit! generated by gen.go 11 | 12 | package {{.Package}} 13 | 14 | {{range .Params}}func Handle{{title .Name}}(w http.ResponseWriter, r *http.Request) { 15 | p := r.FormValue("{{.Name}}") 16 | if p == "" { 17 | http.Error(w, "{{.Name}} not specified", http.StatusBadRequest) 18 | return 19 | } 20 | fmt.Fprintf(w, "%s is god.", p) 21 | } 22 | {{end}} 23 | `)) 24 | 25 | func Generate(name string...) error { 26 | pkg, err := build.Default.Import(".", 0) 27 | if err != nil { 28 | return err 29 | } 30 | f, err := os.Create(fmt.Sprintf("%s_gen.go"), strings.ToLower(name)) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | return tmpl.Execute(f, map[string]string{}) 36 | } 37 | -------------------------------------------------------------------------------- /2018/treasure-go/http/curl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | flag.Parse() 13 | rawurl := flag.Arg(0) 14 | resp, err := http.Get(rawurl) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer resp.Body.Close() 19 | 20 | b, err := ioutil.ReadAll(resp.Body) 21 | if err != nil { 22 | panic(err) 23 | } 24 | fmt.Fprint(os.Stdout, string(b)) 25 | } 26 | -------------------------------------------------------------------------------- /2018/treasure-go/http/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "net/http" 5 | 6 | func main() { 7 | // START OMIT 8 | resp, err := http.Get("https://example.com") 9 | // ... 10 | resp, err := http.Post("https://example.com") 11 | // ... 12 | req, err := http.NewRequest("GET", "https://example.com", nil) 13 | // ... 14 | req.Header.Add("X-Treasure", "🍺") 15 | resp, err := http.DefaultClient.Do(req) 16 | // END OMIT 17 | } 18 | -------------------------------------------------------------------------------- /2018/treasure-go/img/legacybook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2018/treasure-go/img/legacybook.jpg -------------------------------------------------------------------------------- /2018/treasure-go/img/tddcycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2018/treasure-go/img/tddcycle.png -------------------------------------------------------------------------------- /2018/treasure-go/intro.slide: -------------------------------------------------------------------------------- 1 | Go入門 2 | Treasure2018版 3 | 00:00 13 Aug 2018 4 | Tags: golang 5 | 6 | Kenta SUZUKI 7 | Gopher at VOYAGE GROUP 8 | @suzu_v 9 | https://github.com/suzuken 10 | 11 | * この資料について 12 | 13 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2018年度版講義資料です。Goの講義は2日間です。 14 | 15 | .link https://voyagegroup.com/internship/treasure/ 16 | 17 | GitHubで公開されています。 18 | 19 | .link https://github.com/voyagegroup/talks 20 | .link https://go-talks.appspot.com/github.com/voyagegroup/talks/2018/treasure-go/intro.slide 21 | 22 | 今年のGo講義は一日目がsuzuken、二日目はsaxsir担当です。 23 | 24 | * はじめに 25 | 26 | * となりの人と話しましょう 1分 27 | 28 | - 好きな言語 29 | - 好きなエディタ 30 | - 最近つくってるもの 31 | 32 | * Treasureについて簡単に 33 | 34 | - バックグラウンドの違うみなさんが3週間過ごします 35 | - どんどん話してください 36 | - コードを書いて、質問して、がっつり勉強してください 37 | 38 | Enjoy! :) 39 | 40 | * 2日間で学んでほしいこと 41 | 42 | - Goで小さなコマンドラインツールを書けるようになること 43 | - 一通り小さなWebアプリケーションを書けるようになること 44 | - Goのコードの書き方に慣れること 45 | 46 | とにかくどんどんコードを書きましょう。 47 | 48 | * アウトライン 49 | 50 | - なぜGoなのか? 51 | - 事前課題解説 52 | - testing 53 | - net/http 54 | - encoding/json 55 | - database/sql 56 | - APIサーバの作り方 57 | 58 | .image https://golang.org/doc/gopher/project.png 59 | 60 | * なぜGoを使うのか? 61 | 62 | いろいろな良い点があります。 63 | 64 | - 読みやすい 65 | - 言語仕様がfixしている 66 | - 軽快なbuild / 高速な動作 / concurrencyサポート 67 | - ツール (go tool, gofmt ...) 68 | - ほどよい標準ライブラリ 69 | 70 | Goの一番の特徴はSimplicityだと思います。 71 | 72 | .link https://talks.golang.org/2015/simplicity-is-complicated.slide#1 Simplicity is Complicated 73 | 74 | * 準備: Tour of Go 75 | 76 | .link https://tour.golang.org 77 | 78 | - まずTour of Goおすすめ 79 | - 言語仕様が簡単に学べる 80 | - ほどよい演習 81 | 82 | 演習やってないひとはやりましょう 83 | 84 | .link https://gist.github.com/suzuken/d892c42e56c986bca813 85 | 86 | * 準備: How to write Go code 87 | 88 | .link https://golang.org/doc/code.html 89 | 90 | - `$GOPATH/bin` をPATHに追加しましょう 91 | - これから書いてもらうコードもGoのワークスペースに則って配置して貰う必要があります。 92 | - 例えば `$GOPATH/src/github.com/suzuken/wiki` などに配置します。go getをつかうとこのルールに従って配置されます。 93 | 94 | tips: ghqとpeco 95 | 96 | .link https://github.com/motemen/ghq 97 | .link https://github.com/peco/peco 98 | 99 | DEMOします。 100 | 101 | * 手続き、コマンドラインツール、そしてテスト 102 | 103 | * 事前課題その1 104 | 105 | n番目のフィボナッチ数を返す手続きを実装してください。 106 | 107 | f(0) = 0 108 | f(1) = 1 109 | f(n+2) = f(n) + f(n+1) 110 | 111 | .link https://gist.github.com/suzuken/a8ec8fc95cd0c0d18d8d5584fdd4f3ab 112 | 113 | * 事前課題その1 フィボナッチ数 解答例 114 | 115 | func fib(n int) int { 116 | if n == 0 { 117 | return 0 118 | } 119 | if n == 1 { 120 | return 1 121 | } 122 | return fib(n-1) + fib(n-2) 123 | } 124 | 125 | テストを書いてからリファクタリングしてみましょう。 126 | 127 | * 開発者テスト 128 | 129 | テスト大事 130 | 131 | .image https://connpass-tokyo.s3.amazonaws.com/event/27540/41d84cf0e6494e2e91e51ad8e9c85310.png 132 | 133 | .link https://golang.org/pkg/testing/ 134 | 135 | * テストのないコードはレガシーコードである 136 | 137 | .image ./img/legacybook.jpg 138 | 139 | * TDDのリズム 140 | 141 | .image ./img/tddcycle.png _ 600 142 | .caption [[https://www.slideshare.net/t_wada/osh2014-sprit-of-tdd][TDDのこころ by twada]] 143 | 144 | * フィボナッチのテスト (発展課題 その1) 145 | 146 | .code fib/fib_test.go /func TestFib/,/^}/ 147 | 148 | .link https://github.com/golang/go/wiki/TableDrivenTests 149 | 150 | $ go test -v ./... 151 | === RUN TestFib 152 | --- PASS: TestFib (0.00s) 153 | PASS 154 | ok github.com/voyagegroup/talks/2018/treasure-go/fib 0.006s 155 | 156 | 157 | * リファクタリング 158 | 159 | .code fib/fib.go /func fib/,/^}/ 160 | 161 | 書き換えたらテストをします。 162 | 163 | $ go test -v ./... 164 | === RUN TestFib 165 | --- PASS: TestFib (0.00s) 166 | PASS 167 | ok github.com/voyagegroup/talks/2018/treasure-go/fib 0.006s 168 | 169 | リファクタリング前にはテストを書きましょう。 170 | 171 | * コマンドラインツールをつくろう(発展課題 その2) 172 | 173 | Goで `go run` させられるのは `main` パッケージのみ。 174 | 175 | .play basic/basic.go 176 | 177 | `fib` をするコマンドラインツールをつくってみましょう。 178 | 179 | * os.Args をつかう 180 | 181 | .play fib/fib.go /func main/,/^}/ 182 | 183 | ほとんどの場合は `flag` をつかったほうが楽です。 184 | 185 | .link https://golang.org/pkg/flag/ 186 | 187 | * よくしたレビューコメント 188 | 189 | 可読性大事。可読性の高いコードは信頼できるコードです。 190 | 191 | - `gofmt` おねがいします 192 | - `main` 以外で `os.Exit` や `panic` するのは大抵好ましくないです 193 | - `error` はよっぽど自明じゃないかぎり無視しないように 194 | - 変数や手続き名は `snake_case` ではなくて `camelCase` 195 | - 変数名は短く 196 | - [[https://github.com/golang/go/wiki/CodeReviewComments#indent-error-flow][通常のコードパスはインデントを最小に]] 197 | - [[https://github.com/golang/go/wiki/TableDrivenTests][テストパターンが複数あるときはTableDrivenTestsおすすめ]] 198 | 199 | .link https://golang.org/s/style 200 | .link https://blog.golang.org/go-fmt-your-code 201 | 202 | * Goに入ってはGoに従え 203 | 204 | .link https://ukai-go-talks.appspot.com/2014/gocon.slide#1 205 | 206 | 言語ごとに作法が違う 207 | *作法にかなったやりかた* で書きましょう。 208 | 209 | * TDD実習 & ペアプログラミング 210 | 211 | * TDD実習の説明 212 | 213 | - これからのとなりの人と一緒にプログラムを書いてもらいます 214 | - どちらか1台のPCをつかいます 215 | - どちらか一方がテストを、どちらか一方が実装側を書きます 216 | 217 | デモします。 218 | 219 | * TDD実習 その1: スタック 30分 220 | 221 | - スタックを作成してください 222 | 223 | .image https://upload.wikimedia.org/wikipedia/commons/b/b4/Lifo_stack.png _ 350 224 | 225 | // 簡単のため、stringしかいれられないようにしています 226 | type Stack struct{} 227 | func (s *Stack) Pop() string {} 228 | func (s *Stack) Push(ss string) {} 229 | 230 | 内部のデータ構造はsliceをつかうといいでしょう。 231 | 232 | .link https://blog.golang.org/go-slices-usage-and-internals 233 | 234 | * 動作例 235 | 236 | s := &Stack{} 237 | s.Push("dataA") 238 | s.Push("dataB") 239 | s.Push("dataC") 240 | s.Pop() // -> "dataC" 241 | s.Pop() // -> "dataB" 242 | s.Push("dataD") 243 | s.Pop() // -> "dataD" 244 | s.Pop() // -> "dataA" 245 | s.Pop() // -> "" 246 | 247 | * TDD実習 その2: 制限付きスタック 10分 248 | 249 | - スタックに最大長を設定できるようにしてください 250 | - 最大長を超えるPushについては、古いものから削除されるようにしましょう。 251 | 252 | 動作例 253 | 254 | s := &Stack{limit: 2} 255 | s.Push("dataA") 256 | s.Push("dataB") 257 | s.Push("dataC") 258 | s.Pop() // -> "dataC" 259 | s.Pop() // -> "dataB" 260 | s.Pop() // -> "" 261 | 262 | インタフェースについては自分たちで相談して決めてみましょう。 263 | 264 | 発展 265 | 266 | - あとから制限長を変更できるようにしましょう 267 | 268 | * testing振り返り 269 | 270 | - どんなテストをかけばいいかを考えるのは最初は難しいかもしれません。 271 | - `testing` パッケージではふつうのGoコードでテストを書くことができます。テストを書くのに特別な知識は必要ありません。 272 | - ユニットテストを書くことで既存の機能が正常に動いていることを担保しつつ、新しい機能の追加を手助けしてくれます。 273 | - デバッグプリントではなく気軽にテスト書いていきましょう。 274 | 275 | * net/http 入門 276 | 277 | * net/http: Request OverView 278 | 279 | .code http/example.go /START OMIT/,/END OMIT/ 280 | 281 | .link https://golang.org/pkg/net/http/ 282 | 283 | * net/http 実習(40分): curl実装 284 | 285 | 以下を実装してみましょう。 286 | 287 | $ curl example.com 288 | 289 | 正常系については同等に、エラーの場合については適宜わかりやすいメッセージを出力するようにしてください。 290 | 291 | できたらHTTPヘッダーの変更、メソッドの変更もやってみましょう。 292 | 293 | $ curl --header "X-Treasure: 🍺" example.com 294 | $ curl -H "Content-Type: application/json" -X POST -d '{"ajito":"🍺"}' example.com 295 | 296 | 全部できたら好きなcurlのオプションを実装してみましょう。 (例: `--cookie`, `--cookie-jar`, `--user-agent` etc.) 297 | 298 | *発展課題*: このコマンドのテストを書いてみましょう。(ポイント: コマンドラインツールとしてのテストと内部APIのテストを分けて考えましょう。) 299 | 300 | * 余談: SimpleとEasy 301 | 302 | .link http://php.net/file_get_contents 303 | .link http://eed3si9n.com/ja/simplicity-matters シンプルさの必要性 304 | .link https://www.amazon.co.jp/dp/4274064069 UNIXという考え方―その設計思想と哲学 305 | 306 | : シンプルさはあとから追加できない 307 | 308 | * 事前課題その2: net/http 演習 309 | 310 | - 指定したURLにあるコンテンツについて、タイトルとdescriptionを抜き出すツールを書きましょう 311 | - HTTPサーバとして実装してこの機能をつかえるようにしましょう 312 | 313 | .link https://gist.github.com/suzuken/b456e0f4679f86da572839d6d86f159e 314 | 315 | $ go run scraper/scraper.go& 316 | # 自分でつくったcurlを使ってももちろんOK 317 | $ curl -D - "http://localhost:8080?url=https://golang.org" 318 | HTTP/1.1 200 OK 319 | Content-Type: application/json 320 | Date: Fri, 05 Aug 2018 07:45:23 GMT 321 | Content-Length: 57 322 | 323 | {"title":"The Go Programming Language","description":""} 324 | 325 | この課題の狙いは、ある仕事をHTTPサーバに組み込むことを試してもらうことです。適切に仕事をわけて実装すれば、楽に組み込むことができます。 326 | 327 | * net/http.(*Server) 328 | 329 | Goの標準ライブラリでは簡単にHTTPサーバを立ち上げることができます。 330 | 331 | func main() { 332 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 333 | // wはio.Writerなので書き込むことができます 334 | fmt.Fprint(w, "hello world") 335 | }) 336 | http.ListenAndServe(":8080", nil) 337 | } 338 | 339 | and 340 | 341 | $ go run server.go& 342 | $ curl http://localhost:8080 343 | hello world 344 | 345 | .link https://golang.org/pkg/fmt/#Fprint 346 | .link https://golang.org/pkg/net/http/ 347 | 348 | * encoding/json 349 | 350 | .code scraper/scraper.go /type Page/,/^}/ 351 | 352 | and 353 | 354 | enc := json.NewEncoder(w) 355 | if err := enc.Encode(p); err != nil { 356 | // ... 357 | } 358 | 359 | すると io.Writer である `w` にJSONが出力されます。 360 | 361 | .link https://golang.org/pkg/encoding/json/ 362 | 363 | * 実習 net/http: 複数のページ取得 45分 364 | 365 | 事前課題その2でつくったHTTPサーバに以下の機能を追加しましょう。 366 | 367 | - URLを複数受け取り、複数ページのtitleとdescriptionを取ってこれるようにしましょう。パラメータの渡し方はお好みで。(カンマ区切りなど) 368 | - 結果はJSONの配列で返すようにしましょう。その際、どのURLからどの結果が取得できたかをわかるようにしてください。 369 | - `og:title`, `og:image` などの [[http://ogp.me/][Open Graph protocol]]に含まれるデータもあれば取得し、同様にJSONで返せるようにしてください。 370 | 371 | 発展課題 372 | 373 | - 各ページへのHTTPリクエストのタイムアウトを設定し、長く時間のかかったURLは無視できるようにしましょう。正常に取得できた結果のみ返すようにします。 374 | - URLごとにページの取得を並列化しましょう。すべてのページの結果が揃ったら結果を返すようにしてみましょう。 375 | - [[https://golang.org/pkg/net/http/httptest/][net/http/httptest]] をつかってこの機能のテストを書いてみましょう。 376 | -------------------------------------------------------------------------------- /2018/treasure-go/report/report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "time" 7 | ) 8 | 9 | type Report struct { 10 | Title string 11 | Body string 12 | } 13 | 14 | func (r *Report) Insert(db *sql.DB) (sql.Result, error) { 15 | stmt, err := db.Prepare(`insert into (title, body) values(?, ?)`) 16 | if err != nil { 17 | return err 18 | } 19 | defer stmt.Close() 20 | return stmt.Exec(r.Title, r.Body) 21 | } 22 | 23 | func Select(db *sql.DB) ([]Report, error) { 24 | rows, err := db.Query(`select * from reports`) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer rows.Close() 29 | 30 | reports := make([]Report{}) 31 | for rows.Next() { 32 | var title string 33 | var body string 34 | if err := rows.Scan(&title, &body); err != nil { 35 | return nil, err 36 | } 37 | r := Report{Title: title, Body: body} 38 | reports = append(reports, r) 39 | } 40 | return reports, nil 41 | } 42 | 43 | func main() { 44 | var ( 45 | title = flag.String("title", "kuke", "cool title") 46 | body = flag.String("body", "treasure", "cool body") 47 | ) 48 | flag.Parse() 49 | db, err := sql.Open("sqlite3", ":memory:") 50 | if err != nil { 51 | log.Fatalf("open failed: %s", err) 52 | } 53 | r := &Report{*title, *body} 54 | if _, err := r.Insert(db); err != nil { 55 | log.Fatalf("insert failed: %s", err) 56 | } 57 | 58 | result, err := Select(db) 59 | if err != nil { 60 | log.Fatalf("select failed: %s", err) 61 | } 62 | fmt.Printf("%v", result) 63 | } 64 | -------------------------------------------------------------------------------- /2018/treasure-go/scraper/scraper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "golang.org/x/net/html" 9 | ) 10 | 11 | type Page struct { 12 | Title string `json:"title"` 13 | Description string `json:"description"` 14 | } 15 | 16 | func Get(url string) (*Page, error) { 17 | resp, err := http.Get(url) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer resp.Body.Close() 22 | 23 | doc, err := html.Parse(resp.Body) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // START OMIT 29 | page := &Page{} 30 | var f func(*html.Node) 31 | f = func(n *html.Node) { 32 | if n.Type == html.ElementNode && n.Data == "title" { 33 | page.Title = n.FirstChild.Data 34 | } 35 | if n.Type == html.ElementNode && n.Data == "meta" { 36 | if isDescription(n.Attr) { 37 | for _, attr := range n.Attr { 38 | if attr.Key == "content" { 39 | page.Description = attr.Val 40 | } 41 | } 42 | } 43 | } 44 | for c := n.FirstChild; c != nil; c = c.NextSibling { 45 | f(c) 46 | } 47 | } 48 | f(doc) 49 | // END OMIT 50 | return page, nil 51 | } 52 | 53 | func isDescription(attrs []html.Attribute) bool { 54 | for _, attr := range attrs { 55 | if attr.Key == "name" && attr.Val == "description" { 56 | return true 57 | } 58 | } 59 | return false 60 | } 61 | 62 | func handler() { 63 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 64 | rawurl := r.FormValue("url") 65 | if rawurl == "" { 66 | http.Error(w, "url not specified", http.StatusBadRequest) 67 | return 68 | } 69 | p, err := Get(rawurl) 70 | if err != nil { 71 | http.Error(w, "request failed", http.StatusInternalServerError) 72 | return 73 | } 74 | w.Header().Set("Content-Type", "application/json") 75 | enc := json.NewEncoder(w) 76 | if err := enc.Encode(p); err != nil { 77 | http.Error(w, "encoding failed", http.StatusInternalServerError) 78 | return 79 | } 80 | }) 81 | } 82 | 83 | func main() { 84 | handler() 85 | log.Fatal(http.ListenAndServe(":8080", nil)) 86 | } 87 | -------------------------------------------------------------------------------- /2018/treasure-go/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | type Stack struct { 4 | s []interface{} 5 | } 6 | 7 | func (s *Stack) Pop() interface{} { 8 | return s.s[len(s.s)-1] 9 | } 10 | 11 | func (s *Stack) Push(i interface{}) { 12 | s.s = append(s.s, i) 13 | } 14 | -------------------------------------------------------------------------------- /2018/treasure-go/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPushAndPop(t *testing.T) { 8 | s := &Stack{} 9 | s.Push(1) 10 | if r := s.Pop(); r != 1 { 11 | t.Fatalf("want 1, got %v", r) 12 | } 13 | if r := s.Pop(); r != nil { 14 | t.Fatalf("want nil, got %v", r) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /2019/treasure-go-day2/img/mysql_viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2019/treasure-go-day2/img/mysql_viewer.png -------------------------------------------------------------------------------- /2019/treasure-go-day2/intro.slide: -------------------------------------------------------------------------------- 1 | Go Webアプリケーション入門 2 | Treasure2019版 Day2 3 | 00:00 13 Aug 2019 4 | Tags: golang 5 | 6 | Jumpei Chikamori 7 | Gopher at VOYAGE GROUP 8 | @pei0804 9 | https://github.com/pei0804 10 | 11 | * この資料について 12 | 13 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2019年度版講義資料です。Goの講義は2日間です。 14 | 15 | .link https://voyagegroup.com/internship/treasure/ 16 | 17 | GitHubで公開されています。 18 | 19 | .link https://github.com/voyagegroup/talks 20 | .link https://talks.godoc.org/github.com/voyagegroup/talks/2019/treasure-go-day2/intro.slide 21 | 22 | 今年のGo講義は一日目がsuzuken、二日目はpei担当です。 23 | 24 | * 今日のゴール 25 | 26 | 15:00からはフロントエンドの講義になるので、それまでに「API余裕っすよ」になる。 27 | 28 | .image https://golang.org/doc/gopher/project.png 29 | 30 | * 講義の進め方 31 | 32 | 講義っぽいことよりも、実際にこの資料を見ながら各々で進めてもらいます。 33 | 基本課題は、タイミング見て解説していきます。 34 | 35 | * 今日やらないこと 36 | 37 | - 認証周り: 後日講義あります 38 | - フロント: 15時から講義があります 39 | - テーブル設計: 後日講義あります 40 | 41 | * バックエンドの環境構築 42 | 43 | README.md通りに進めると出来上がります。 44 | .link https://github.com/voyagegroup/treasure-app/tree/master/backend 45 | 46 | * ベースアプリの構成 47 | 48 | 主にいじる場所 49 | 50 | - Controller: 外部からのHTTPリクエストの解釈、レスポンスの作成などをする 51 | - Service: アプリケーションのロジック 52 | - Repository: データベースへのアクセス 53 | - Model: 型定義(データベースのテーブルを型定義など) 54 | - server.go: サーバーの振る舞い 55 | - handler.go: カスタムハンドラー 56 | 57 | * ベースアプリの構成 58 | 59 | - cmd: コマンド 60 | - db: データベースとのコネクションなどを扱う 61 | - dbutil: データベースとのやり取りで使う便利関数置き場 62 | - firebase: Firebaseに関する何か 63 | - img: 画像置き場 64 | - httputil: HTTP周りの便利関数置き場 65 | - middleware: ミドルウェア置き場 66 | - sample サンプルです 67 | - .env サーバーの設定ファイル 68 | 69 | * 命名規則 70 | 71 | Railsを参考にしました。 72 | 命名などで悩まず、今日はガンガン書いてほしい意図で採用しているだけなので、基本的には自由です。 73 | 74 | .link https://railsguides.jp/routing.html ルーティング 75 | 76 | .link https://railsguides.jp/active_record_basics.html アクティブレコード 77 | 78 | * ステータスコードやルーティング規則の参考資料 79 | 80 | .link https://developer.mozilla.org/ja/docs/Web/HTTP/Status MDN Web docs 81 | 82 | .link https://www.oreilly.co.jp/books/9784873116860/ Web API: The Good Parts 83 | 84 | .link https://developer.github.com/v3/ Github REST APIv3 85 | 86 | .link https://qiita.com/mserizawa/items/b833e407d89abd21ee72#%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%82%BD%E3%83%BC%E3%83%88%E6%A4%9C%E7%B4%A2%E3%81%AF%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A7%E3%82%84%E3%82%8D%E3%81%86 翻訳: WebAPI 設計のベストプラクティス 87 | 88 | * Migrationの使い方 89 | 90 | treasure-app/database/Makefileを参照 91 | 92 | make migrate/up を叩くと新しく追加されたSQLが実行されます。 93 | 94 | .link https://github.com/pressly/goose 95 | 96 | *downは現場では使わない* 97 | 98 | upだけで運用しています。また、削除もしません。 99 | 修正したいなら、新しくテーブル、カラムを追加して、新しく作ったものを使うようにすることがほとんどです。 100 | 意図せず消してしまう可能性も当然あるので、downは無いほうがむしろ良い。 101 | 102 | * 開発に使えるTIPS: jq 103 | 104 | jsonを整形できます 105 | .link https://qiita.com/takeshinoda@github/items/2dec7a72930ec1f658af 106 | 107 | json以外の文字列が標準出力にある場合は、無視して整形も可能です。私は以下のようなaliasを設定しています。 108 | 109 | alias jqr="jq -R 'fromjson?'" 110 | 111 | 使用例 112 | 113 | ❯ make -f integration.mk req-articles-post | jqr 114 | { 115 | "id": 16, 116 | "title": "title", 117 | "body": "body" 118 | } 119 | 120 | * 開発に使えるTIPS: auto build 121 | 122 | treasure-app/backend 123 | コード書き変える度にbuildは手間なので、ツールにやってもらいましょう。 124 | 125 | ❯ make dev-deps 126 | ❯ make refresh-run 127 | 128 | 129 | * 開発に使えるTIPS: logを見れば大体何が起きているかわかる 130 | 131 | ❯ make refresh-run 132 | realize start 133 | [15:37:48][BACKEND] : Watching 22 file/s 15 folder/s 134 | [15:37:48][BACKEND] : Install started 135 | [15:37:50][BACKEND] : Install completed in 2.170 s 136 | [15:37:50][BACKEND] : Running.. 137 | [15:37:50][BACKEND] : 2019/08/10 15:37:50 server.go:51: Listening on port 1991 138 | [15:38:01][BACKEND] : ::1 - - [10/Aug/2019:15:38:00 +0900] "POST /articles HTTP/1.1" 201 39 139 | 140 | * 開発に使えるTIPS: database editor 141 | 142 | JetBrainsのdatabase editorで簡単にデータベースの中身を操作できます。 143 | 本番のデータベースに繋ぐのは禁止! 144 | 145 | .link https://pleiades.io/help/idea/database-tool-window.html 146 | 147 | .image ./img/mysql_viewer.png 400 500 148 | 149 | * リクエストしてみよう 150 | 151 | - 記事作成 POST /articles 152 | - 記事更新 PUT /articles/:id 153 | - 記事削除 DELETE /articles/:id 154 | - 記事取得 GET /articles/:id 155 | - 記事一覧取得 GET /articles 156 | 157 | treasure-app/backendにあるintegration.mkにターゲットを用意しています。 158 | 159 | ❯ make -f integration.mk req-articles-post 160 | 161 | 162 | * 少しだけ一緒にコードを追ってみましょう 163 | 164 | - 記事取得 165 | - エンドポイントの増やし方 166 | - カスタムハンドラ 167 | - ミドルウェア 168 | 169 | * 課題をやっていきましょう 170 | 171 | 課題1については、隣に座っている人とペアプロでやってみましょう。 172 | わからない点があれば、TAや講師にガンガン質問してください。 173 | 174 | * 課題の進め方の例 175 | 176 | - 現状把握 177 | - テーブル、エンドポイントを設計 178 | - migration sqlを追加 179 | - migrate up 180 | - エンドポイント作成 181 | - ロジックを書く 182 | - 適宜curl投げてみたりする(integration.mkに追加してやりやすくするのもあり) 183 | 184 | * 課題1 記事の作成者 185 | 186 | * 要件 187 | 188 | *出来るようにしてほしいこと* 189 | 190 | - 記事投稿時に、記事とユーザーを紐付けて、誰の記事か分かるようにする 191 | - 記事の取得時にどのユーザーが書いたかを含めて返す(もし、ユーザーが紐付いてない場合は、nilを返す) 192 | 193 | *実装のポイント* 194 | 195 | - user tableからユーザーを一意に特定できるものキーを使います 196 | - article tableには、作成者を保持するカラムが存在しません 197 | 198 | * イメージ 199 | 200 | 記事投稿時に、ユーザーを紐付ける。また、記事取得時はuser_idを返す。 201 | 202 | $ curl -XPOST -H "Authorization: Bearer hoge" localhost:1991/articles 203 | -d '{"title": "title", "body": "body"}' 204 | 205 | {"id":2,"title":"title","body":"body","user_id":1} 206 | 207 | $ curl localhost:1991/articles 208 | 209 | [ 210 | {"id":1,"title":"title","body":"body", "user_id": nil}, 211 | {"id":2,"title":"title","body":"body", "user_id": 1} 212 | ] 213 | 214 | * この先に課題のヒントがあります。まずは自力でやってみましょう。 215 | 216 | * ヒント 217 | 218 | カラム追加と外部キー属性の作成 219 | treasure-app/database/migrations/3_add_user_column_to_article.sql 220 | 221 | -- +goose Up 222 | ALTER TABLE article ADD user_id int(10) UNSIGNED DEFAULT NULL; 223 | ALTER TABLE article ADD CONSTRAINT article_fk_user FOREIGN KEY (user_id) REFERENCES user(id); 224 | 225 | 226 | migration up 227 | upするとデータベースに適用されます。 228 | 229 | ❯ pwd 230 | /Users/j-chikamori/go/src/github.com/voyagegroup/treasure-app/database 231 | ❯ make migrate/up 232 | goose -dir migrations mysql "root:password@tcp(127.0.0.1:3306)/treasure_app" up 233 | 2019/08/08 16:33:51 OK 3_add_user_column_to_article.sql 234 | 2019/08/08 16:33:51 goose: no migrations to run. current version: 3 235 | 236 | * ヒント 237 | 238 | treasure-app/backend/middleware/auth.go 239 | ユーザーの情報はauth時にcontext valueに格納しています。 240 | 241 | * ヒント 242 | 243 | Article structの作成例 244 | treasure-app/backend/model/article.go 245 | 246 | type Article struct { 247 | ID int64 `db:"id"` 248 | Title string `db:"title"` 249 | Body string `db:"body"` 250 | UserID *int64 `db:"user_id"` 251 | } 252 | 253 | ※レスポンス、リクエスト、テーブルのstructを分けると、後々幸せになれるかも? 254 | 255 | * ヒント 256 | 257 | controllerのCreateで使っているrepository.Createを調整しよう 258 | 259 | package controller 260 | 261 | func (a *Article) Create(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { 262 | newArticle := &model.Article{} 263 | if err := json.NewDecoder(r.Body).Decode(&newArticle); err != nil { 264 | return http.StatusBadRequest, nil, err 265 | } 266 | 267 | articleService := service.NewArticleService(a.dbx) 268 | id, err := articleService.Create(newArticle) 269 | if err != nil { 270 | return http.StatusInternalServerError, nil, err 271 | } 272 | newArticle.ID = id 273 | 274 | return http.StatusCreated, newArticle, nil 275 | } 276 | 277 | * 課題2 記事へのコメント機能 278 | 279 | * 要件 280 | 281 | *出来るようにしてほしいこと* 282 | 283 | - 投稿されている記事に対して、コメントを投稿出来るようにする 284 | - GET /articles/:id のレスポンスに、コメント情報も返すようにする 285 | - コメントは自分のだけ更新、削除出来る 286 | 287 | *実装のポイント* 288 | 289 | - 記事に複数のコメントが投稿できるテーブル設計をしましょう 290 | - コメントの情報を保持するテーブルを作成する必要があります 291 | - エンドポイントのURLをどうすると直感的か考えてみよう 292 | 293 | * イメージ 294 | 295 | いい感じのエンドポイント設計をして、いい感じのJSONを投げる。そして作成されたJSONの情報を返ってくるようにする 296 | 297 | curl -XPOST -H "Authorization: Bearer hoge" localhost:1991/iikanzini -d '{iikanzino json}' 298 | {"id":6,"user_id":1,"article_id":1,"body":":commentbody"} 299 | 300 | curl -v localhost:1991/articles/1 301 | { 302 | "id":1,"title":"title","body":"body","user_id":1, 303 | "comments":{"id":6,"user_id":1,"article_id":1,"body":"commentbody"} 304 | } 305 | 306 | * この先に課題のヒントがあります。まずは自力でやってみましょう 307 | 308 | * ヒント 309 | 310 | 記事コメントテーブル作成 311 | treasure-app/database/migrations/4_add_table_article_comment.sql 312 | 313 | -- +goose Up 314 | CREATE TABLE article_comment ( 315 | id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 316 | user_id int(10) UNSIGNED NOT NULL, 317 | article_id int(10) UNSIGNED NOT NULL, 318 | body VARCHAR(255), 319 | ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 320 | utime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 321 | PRIMARY KEY (id), 322 | CONSTRAINT comment_fk_user FOREIGN KEY (user_id) REFERENCES user (id), 323 | CONSTRAINT comment_fk_article FOREIGN KEY (article_id) REFERENCES article (id) 324 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 325 | 326 | * ヒント 327 | 328 | コメント投稿のURLの例。 329 | では、更新、削除なら?を考えてみましょう。 330 | 331 | POST /articles/:article_id/comments 332 | 333 | * 課題3 記事のタグ付 334 | 335 | * 要件 336 | 337 | *出来るようにしてほしいこと* 338 | 339 | - 記事に複数のタグが紐づく、タグは複数の記事に紐づく 340 | - タグに紐づく記事を検索出来るようにしたい 341 | - タグはいくつか適当作っておくか、タグを作るためのエンドポイントを用意してください 342 | 343 | *実装のポイント* 344 | 345 | - 記事に複数のタグが紐づく、かつ、タグを一覧できるテーブルが必要です 346 | - 一度に複数回のINSERTが発生することがあるため、一度でも失敗したら、全ての処理が実行されないようにしてください 347 | 348 | * イメージ 349 | 350 | フロントエンドを想像しながら、どういうエンドポイントを作れば良さそうか考えてみましょう。 351 | 352 | * この先に課題のヒントがあります。まずは自力でやってみましょう 353 | 354 | * ヒント 355 | 356 | タグ、記事とタグを関連付けるテーブルを作成する。 357 | treasure-app/database/migrations/5_add_table_tag.sql 358 | 359 | -- +goose Up 360 | CREATE TABLE tag ( 361 | id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 362 | name VARCHAR(255), 363 | ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 364 | PRIMARY KEY (id) 365 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 366 | 367 | treasure-app/database/migrations/6_add_table_article_tag.sql 368 | 369 | -- +goose Up 370 | CREATE TABLE article_tag ( 371 | article_id int(10) UNSIGNED NOT NULL, 372 | tag_id int(10) UNSIGNED NOT NULL, 373 | ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 374 | PRIMARY KEY (article_id, tag_id), 375 | CONSTRAINT article_tag_fk_article FOREIGN KEY (article_id) REFERENCES article (id), 376 | CONSTRAINT article_tag_fk_tag FOREIGN KEY (tag_id) REFERENCES tag (id) 377 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 378 | 379 | 380 | * ヒント 381 | 382 | リクエスト、レスポンス、テーブルのstructを分けてもいいかもしれない。 383 | 384 | type CreateArticleRequest struct { 385 | Title string `json:"title"` 386 | Body string `json:"body"` 387 | TagIDs []int64 `json:"tag_ids"` 388 | } 389 | 390 | type CreateArticleResponse struct { 391 | ID int64 `json:"id"` 392 | Title string `json:"title"` 393 | Body string `json:"body"` 394 | UserID int64 `json:"user_id"` 395 | Tags []Tag `json:"tags"` 396 | } 397 | 398 | type Article struct { 399 | ID int64 `db:"id"` 400 | Title string `db:"title"` 401 | Body string `db:"body"` 402 | UserID int64 `db:"user_id"` 403 | } 404 | 405 | * ヒント 406 | 407 | repositoryの実装はあくまでシンプルに保ち、ロジックはサービスに寄せる。 408 | 409 | func (a *Article) Create(newArticle *model.Article, tagIds []int64) (int64, error) { 410 | var createdId int64 411 | if err := dbutil.TXHandler(a.db, func(tx *sqlx.Tx) error { 412 | result, err := repository.CreateArticle(tx, newArticle) 413 | if err != nil { 414 | return err 415 | } 416 | createdId, err := result.LastInsertId() 417 | if err != nil { 418 | return err 419 | } 420 | for _, tagId := range tagIds { 421 | _, err = repository.CreateArticleTag(tx, createdId, tagId) 422 | if err != nil { 423 | return err 424 | } 425 | } 426 | if err := tx.Commit(); err != nil { 427 | return err 428 | } 429 | return err 430 | }); err != nil { 431 | 432 | * ヒント 433 | 434 | CreateArticleTagはこんな実装です。 435 | 436 | func CreateArticleTag(db *sqlx.Tx, articleId int64, tagId int64) (sql.Result, error) { 437 | stmt, err := db.Prepare(` 438 | INSERT INTO article_tag (article_id, tag_id) VALUES (?, ?) 439 | `) 440 | if err != nil { 441 | return nil, err 442 | } 443 | defer stmt.Close() 444 | return stmt.Exec(articleId, tagId) 445 | } 446 | 447 | * 基礎課題まとめ 448 | 449 | * API余裕っすになりました(?) 450 | 451 | 課題1 記事の作成者機能 452 | 453 | - 既に存在しているテーブルにカラムを追加 454 | - 外部キー 455 | - 既存のエンドポイントの修正 456 | - マイグレーション 457 | 458 | 課題2 記事へのコメント機能 459 | 460 | - エンドポイントの新規作成 461 | - 1:N テーブル設計 462 | 463 | 課題3 記事のタグ付け 464 | 465 | - N:Nのテーブル設計(中間テーブル) 466 | - 複数テーブルへの書き込み 467 | 468 | * その他色々 469 | 470 | - エラー発生時に、ログとして残しておきたいメッセージとクライアントに返す時は同じで良いか?(内部のエラーメッセージ見えてよいのか?とか) 471 | - テーブルの表現に使ってるstructを使い回すと事故る可能性がある(機微情報) 472 | - エラーの設計、関数内で複数回発生するエラーどうするか 473 | - エンドポイント、ステータスコード設計 474 | - 今はバリデーションしていませんが、やるとしたらどこまでやるか、そのコードをどこに置くか 475 | 476 | * 発展課題1 記事一覧取得の機能拡充 477 | 478 | - 作成、更新日時順 479 | - 記事内文字列検索 480 | - カテゴリ検索 481 | 482 | 実装する際に、以下の記事が参考になるので、読んでみてください。 483 | 484 | .link https://qiita.com/mserizawa/items/b833e407d89abd21ee72#%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%82%BD%E3%83%BC%E3%83%88%E6%A4%9C%E7%B4%A2%E3%81%AF%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A7%E3%82%84%E3%82%8D%E3%81%86 クエリパラメータの設計について 485 | 486 | * 発展課題2 ファイルアップロード 487 | 488 | 後半開発で使いそうなので、余裕があれば、以下の記事を参考に実装してみましょう。 489 | 490 | .link https://tutorialedge.net/golang/go-file-upload-tutorial/ 491 | 492 | * 発展課題色々 493 | 494 | - 共同編集者機能 495 | - いいね機能 496 | - view数 497 | - auth時に毎回データベースに問い合わせている部分をKVSに置き換える 498 | -------------------------------------------------------------------------------- /2019/treasure-go/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("go go go") 7 | } 8 | -------------------------------------------------------------------------------- /2019/treasure-go/db/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func UserByAge(db *sql.DB, age int) (names []string, err error) { 13 | rows, err := db.Query( 14 | `select name from users where age = ?`, 15 | age) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer rows.Close() 20 | 21 | for rows.Next() { 22 | var name string 23 | if err := rows.Scan(&name); err != nil { 24 | return nil, err 25 | } 26 | names = append(names, name) 27 | } 28 | return names, nil 29 | } 30 | 31 | func main() { 32 | var ( 33 | age = flag.Int("age", 10, "user's age") 34 | ) 35 | db, err := sql.Open("mysql", "") 36 | if err != nil { 37 | log.Fatalf("open db failed: %s", err) 38 | } 39 | users, err := UserByAge(db, *age) 40 | if err != nil { 41 | log.Fatalf("query failed: %s", err) 42 | } 43 | fmt.Printf("%v", users) 44 | } 45 | -------------------------------------------------------------------------------- /2019/treasure-go/fib/fib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func fib(n int) int { 10 | if n == 0 || n == 1 { 11 | return n 12 | } 13 | return fib(n-1) + fib(n-2) 14 | } 15 | 16 | func main() { 17 | if len(os.Args) < 2 { 18 | fmt.Print("usage: ./fib [integer]") 19 | os.Exit(1) 20 | } 21 | i, err := strconv.Atoi(os.Args[1]) 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "argument must be integer: %s\n", err) 24 | os.Exit(1) 25 | } 26 | fmt.Printf("%d", fib(i)) 27 | } 28 | -------------------------------------------------------------------------------- /2019/treasure-go/fib/fib_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFib(t *testing.T) { 8 | type Case struct { 9 | n, out int 10 | } 11 | cases := []Case{ 12 | {0, 0}, 13 | {1, 1}, 14 | {10, 55}, 15 | } 16 | for i, c := range cases { 17 | if got := fib(c.n); got != c.out { 18 | t.Errorf("#%d: fib(%d) want %d, got %d\n", i, c.n, c.out, got) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /2019/treasure-go/gen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/build" 5 | "os" 6 | "strings" 7 | "text/template" 8 | ) 9 | 10 | var tmpl = template.Must(template.New("").Parse(`// Don't edit! generated by gen.go 11 | 12 | package {{.Package}} 13 | 14 | {{range .Params}}func Handle{{title .Name}}(w http.ResponseWriter, r *http.Request) { 15 | p := r.FormValue("{{.Name}}") 16 | if p == "" { 17 | http.Error(w, "{{.Name}} not specified", http.StatusBadRequest) 18 | return 19 | } 20 | fmt.Fprintf(w, "%s is god.", p) 21 | } 22 | {{end}} 23 | `)) 24 | 25 | func Generate(name string...) error { 26 | pkg, err := build.Default.Import(".", 0) 27 | if err != nil { 28 | return err 29 | } 30 | f, err := os.Create(fmt.Sprintf("%s_gen.go"), strings.ToLower(name)) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | return tmpl.Execute(f, map[string]string{}) 36 | } 37 | -------------------------------------------------------------------------------- /2019/treasure-go/http/curl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | flag.Parse() 13 | rawurl := flag.Arg(0) 14 | resp, err := http.Get(rawurl) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer resp.Body.Close() 19 | 20 | b, err := ioutil.ReadAll(resp.Body) 21 | if err != nil { 22 | panic(err) 23 | } 24 | fmt.Fprint(os.Stdout, string(b)) 25 | } 26 | -------------------------------------------------------------------------------- /2019/treasure-go/http/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "net/http" 5 | 6 | func main() { 7 | // START OMIT 8 | resp, err := http.Get("https://example.com") 9 | // ... 10 | resp, err := http.Post("https://example.com") 11 | // ... 12 | req, err := http.NewRequest("GET", "https://example.com", nil) 13 | // ... 14 | req.Header.Add("X-Treasure", "🍺") 15 | resp, err := http.DefaultClient.Do(req) 16 | // END OMIT 17 | } 18 | -------------------------------------------------------------------------------- /2019/treasure-go/img/legacybook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2019/treasure-go/img/legacybook.jpg -------------------------------------------------------------------------------- /2019/treasure-go/img/tddcycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2019/treasure-go/img/tddcycle.png -------------------------------------------------------------------------------- /2019/treasure-go/intro.slide: -------------------------------------------------------------------------------- 1 | Go入門 2 | Treasure2019版 3 | 00:00 12 Aug 2019 4 | Tags: golang 5 | 6 | Kenta SUZUKI 7 | Gopher at VOYAGE GROUP 8 | @suzu_v 9 | https://github.com/suzuken 10 | 11 | * この資料について 12 | 13 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2019年度版講義資料です。Goの講義は2日間です。 14 | 15 | .link https://voyagegroup.com/internship/treasure/ 16 | 17 | GitHubで公開されています。 18 | 19 | .link https://github.com/voyagegroup/talks 20 | .link https://talks.godoc.org/github.com/voyagegroup/talks/2019/treasure-go/intro.slide 21 | 22 | 今年のGo講義は一日目がsuzuken、二日目はpei担当です。 23 | 24 | * はじめに 25 | 26 | * となりの人と話しましょう 1分 27 | 28 | - 好きな言語 29 | - 好きなエディタ 30 | - 最近つくってるもの 31 | 32 | * Treasureについて簡単に 33 | 34 | - バックグラウンドの違うみなさんが3週間過ごします 35 | - どんどん話してください 36 | - コードを書いて、質問して、がっつり勉強してください 37 | 38 | Enjoy! :) 39 | 40 | * 2日間で学んでほしいこと 41 | 42 | - Goで小さなコマンドラインツールを書けるようになること 43 | - 一通り小さなWebアプリケーションを書けるようになること 44 | - Goのコードの書き方に慣れること 45 | 46 | とにかくどんどんコードを書きましょう。 47 | 48 | * アウトライン 49 | 50 | - Goの特徴 51 | - net/http 52 | - encoding/json 53 | - APIサーバの作り方 54 | - database/sql 55 | 56 | .image https://golang.org/doc/gopher/project.png 57 | 58 | * なぜGoを使うのか? 59 | 60 | いろいろな良い点があります。 61 | 62 | - シンプルさ 63 | - 読みやすい 64 | - 言語仕様がfixしている 65 | - 軽快なbuild / 高速な動作 / concurrencyサポート 66 | - ツール (go tool, gofmt ...) 67 | - ほどよい標準ライブラリ 68 | 69 | * net/http 入門 70 | 71 | * net/http: Request OverView 72 | 73 | .code http/example.go /START OMIT/,/END OMIT/ 74 | 75 | .link https://golang.org/pkg/net/http/ 76 | .link https://developer.mozilla.org/ja/docs/Web/HTTP/Overview 77 | 78 | * net/http 実習(30分): curl実装 79 | 80 | 以下を実装してみましょう。 81 | 82 | $ curl example.com 83 | 84 | 正常系については同等に、エラーの場合については適宜わかりやすいメッセージを出力するようにしてください。 85 | 86 | できたらHTTPヘッダーの変更、メソッドの変更もやってみましょう。 87 | 88 | $ curl --header "X-Treasure: 🍺" example.com 89 | $ curl -H "Content-Type: application/json" -X POST -d '{"ajito":"🍺"}' example.com 90 | 91 | 全部できたら好きなcurlのオプションを実装してみましょう。 (例: `--cookie`, `--cookie-jar`, `--user-agent` etc.) 92 | 93 | *発展課題1*: このコマンドのテストを書いてみましょう。(ポイント: コマンドラインツールとしてのテストと内部APIのテストを分けて考えましょう。) 94 | *発展課題2*: このコマンドからのリクエストをデバッグしやすくするHTTPサーバを自分で実装してみましょう。 95 | 96 | * 事前課題その2: net/http 演習(振り返り) 97 | 98 | - 指定したURLにあるコンテンツについて、タイトルとdescriptionを抜き出すツールを書きましょう 99 | - HTTPサーバとして実装してこの機能をつかえるようにしましょう 100 | 101 | .link https://gist.github.com/suzuken/b456e0f4679f86da572839d6d86f159e 102 | 103 | $ go run scraper/scraper.go& 104 | # 自分でつくったcurlを使ってももちろんOK 105 | $ curl -D - "http://localhost:8080?url=https://golang.org" 106 | HTTP/1.1 200 OK 107 | Content-Type: application/json 108 | Date: Fri, 05 Aug 2019 07:45:23 GMT 109 | Content-Length: 57 110 | 111 | {"title":"The Go Programming Language","description":""} 112 | 113 | この課題の狙いは、ある仕事をHTTPサーバに組み込むことを試してもらうことです。適切に仕事をわけて実装すれば、楽に組み込むことができます。 114 | 115 | * net/http.(*Server) 116 | 117 | Goの標準ライブラリでは簡単にHTTPサーバを立ち上げることができます。 118 | 119 | func main() { 120 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 121 | // wはio.Writerなので書き込むことができます 122 | fmt.Fprint(w, "hello world") 123 | }) 124 | http.ListenAndServe(":8080", nil) 125 | } 126 | 127 | and 128 | 129 | $ go run server.go& 130 | $ curl http://localhost:8080 131 | hello world 132 | 133 | .link https://golang.org/pkg/fmt/#Fprint 134 | .link https://golang.org/pkg/net/http/ 135 | 136 | * encoding/json 137 | 138 | .code scraper/scraper.go /type Page/,/^}/ 139 | 140 | and 141 | 142 | enc := json.NewEncoder(w) 143 | if err := enc.Encode(p); err != nil { 144 | // ... 145 | } 146 | 147 | すると io.Writer である `w` にJSONが出力されます。 148 | 149 | .link https://golang.org/pkg/encoding/json/ 150 | 151 | * 実習 net/http: 複数のページ取得 45分 152 | 153 | 事前課題その2でつくったHTTPサーバに以下の機能を追加しましょう。 154 | 155 | - URLを複数受け取り、複数ページのtitleとdescriptionを取ってこれるようにしましょう。パラメータの渡し方はお好みで。(カンマ区切りなど) 156 | - 結果はJSONの配列で返すようにしましょう。その際、どのURLからどの結果が取得できたかをわかるようにしてください。 157 | - `og:title`, `og:image` などの [[http://ogp.me/][Open Graph protocol]]に含まれるデータもあれば取得し、同様にJSONで返せるようにしてください。 158 | 159 | 発展課題 160 | 161 | - 各ページへのHTTPリクエストのタイムアウトを設定し、長く時間のかかったURLは無視できるようにしましょう。正常に取得できた結果のみ返すようにします。 162 | - URLごとにページの取得を並列化しましょう。すべてのページの結果が揃ったら結果を返すようにしてみましょう。 163 | - [[https://golang.org/pkg/net/http/httptest/][net/http/httptest]] をつかってこの機能のテストを書いてみましょう。 164 | 165 | * Goとデータベース 166 | 167 | * Goとデータベース: database/sql 168 | 169 | Goからデータベースを扱うときは `database/sql` を使います。 170 | 171 | .link https://golang.org/pkg/database/sql/ 172 | .link https://golang.org/s/sqlwiki 173 | .link https://golang.org/s/sqldrivers 174 | 175 | MySQLでの利用例は以下を見つつ説明します。 176 | 177 | .link https://github.com/go-sql-driver/mysql/wiki/Examples 178 | 179 | 以下の資料もおすすめです。 180 | 181 | .link http://go-database-sql.org/ 182 | 183 | * クエリするときの原則 184 | 185 | - クエリにパラメータを埋め込むときは必ずプリペアドステートメントを使いましょう。 186 | - 決して `fmt.Sprintf` などによる文字列結合でクエリを組み立ててはいけません。 187 | 188 | .link https://www.ipa.go.jp/files/000024396.pdf SQLインジェクション対策について 189 | .link http://www.ipa.go.jp/files/000017320.pdf 安全なSQLの呼び出し方 190 | .link http://d.hatena.ne.jp/ajiyoshi/20100409/1270809525 191 | 192 | Prepare, Query、どちらでもプリペアドステートメントが使えます。 193 | 194 | .link https://golang.org/pkg/database/sql/#DB.Prepare 195 | .link https://golang.org/pkg/database/sql/#DB.Query 196 | 197 | db.Query(`select * from users where name = ?`, "s-tanno") 198 | 199 | * ここから先の実習はvoyagegroup/treasure-appの環境を使います 200 | 201 | .link https://github.com/voyagegroup/treasure-app 202 | 203 | 今日は `./database` 以下をつかいます。 204 | 205 | * DBの立ち上げと接続 206 | 207 | $ cd path/to/treasure-app 208 | $ make docker-compose/up # or docker-compose up -d 209 | $ docker-compose ps # DBコンテナが立ち上がっていることを確認 210 | $ make mysql # mysql -u root -h --protocol -p 211 | # パスワードをいれる 212 | # mysqlシェルが立ち上がる 213 | 214 | 演習用のデータを分離するため、`tutorial`データベースを作成します。 215 | 216 | mysql> create database tutorial; # チュートリアル用のDB作成 217 | mysql> use tutorial; 218 | 219 | Goから繋ぐ場合は次のようにしてつなげます。 220 | 221 | db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/tutorial") 222 | 223 | ちなみに root:password@tcp(127.0.0.1:3306)/tutorial はDSN(Database Source Name)です。 224 | 225 | * DB実習: 一通り自分でDB操作をしてみよう(40分) 226 | 227 | MySQLに自分用の日報を保存するコマンドラインアプリケーションをつくりましょう。 228 | 229 | - 日報用のテーブル `reports` を用意し、保存できるようにしてください。更新、削除の機能はお好みで。テーブル設計は自分で考えてみましょう。 230 | 231 | 発展課題 232 | 233 | - いい感じに日報を整形して標準出力できるようなオプションをつけてみましょう。 234 | - 日報にタグを追加できるようにしてみましょう。タグによって日報を取得できるようにすると便利そうです。 235 | - Webアプリにしてみましょう。`type Report struct` に `ServeHTTP` を実装するのがおすすめです。 236 | 237 | .link https://golang.org/pkg/net/http/#Handler 238 | 239 | * データベースマイグレーション 240 | 241 | - 複数人でアプリケーションをつくるときにスキーマの管理どうしますか? 242 | - データベースマイグレーションをつかうと便利です 243 | 244 | .link https://github.com/pressly/goose 245 | 246 | たとえば新しいテーブルを追加するときやスキーマを変更するときには、SQLファイルを `migrations` 以下にコミットするといいです。すると機能とセットでDBスキーマの変更もコードで管理することができます。 247 | 248 | * DB実習 249 | 250 | 日報につけたコメントをちゃんとDBに保存できるようにしてみましょう。 251 | 252 | テーブル例 253 | 254 | CREATE TABLE `comments` ( 255 | `comment_id` int(11) NOT NULL AUTO_INCREMENT, 256 | `report_id` int(11) NOT NULL, 257 | `body` TEXT NOT NULL, 258 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 259 | `updated` timestamp ON UPDATE CURRENT_TIMESTAMP, 260 | PRIMARY KEY (`comment_id`) 261 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='list of comments'; 262 | 263 | - データベースマイグレーションも用意してあるので、つかってみるといいでしょう 264 | - できれば外部キー制約も貼りましょう 265 | 266 | 発展課題 267 | 268 | - コメントについて誰がコメントしたかも保存できるようにしましょう 269 | - コメントの編集削除をサポートしましょう 270 | - トップページに記事のコメント数も出すようにしましょう 271 | -------------------------------------------------------------------------------- /2019/treasure-go/report/report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "time" 7 | ) 8 | 9 | type Report struct { 10 | Title string 11 | Body string 12 | } 13 | 14 | func (r *Report) Insert(db *sql.DB) (sql.Result, error) { 15 | stmt, err := db.Prepare(`insert into (title, body) values(?, ?)`) 16 | if err != nil { 17 | return err 18 | } 19 | defer stmt.Close() 20 | return stmt.Exec(r.Title, r.Body) 21 | } 22 | 23 | func Select(db *sql.DB) ([]Report, error) { 24 | rows, err := db.Query(`select * from reports`) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer rows.Close() 29 | 30 | reports := make([]Report{}) 31 | for rows.Next() { 32 | var title string 33 | var body string 34 | if err := rows.Scan(&title, &body); err != nil { 35 | return nil, err 36 | } 37 | r := Report{Title: title, Body: body} 38 | reports = append(reports, r) 39 | } 40 | return reports, nil 41 | } 42 | 43 | func main() { 44 | var ( 45 | title = flag.String("title", "kuke", "cool title") 46 | body = flag.String("body", "treasure", "cool body") 47 | ) 48 | flag.Parse() 49 | db, err := sql.Open("sqlite3", ":memory:") 50 | if err != nil { 51 | log.Fatalf("open failed: %s", err) 52 | } 53 | r := &Report{*title, *body} 54 | if _, err := r.Insert(db); err != nil { 55 | log.Fatalf("insert failed: %s", err) 56 | } 57 | 58 | result, err := Select(db) 59 | if err != nil { 60 | log.Fatalf("select failed: %s", err) 61 | } 62 | fmt.Printf("%v", result) 63 | } 64 | -------------------------------------------------------------------------------- /2019/treasure-go/scraper/scraper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "golang.org/x/net/html" 9 | ) 10 | 11 | type Page struct { 12 | Title string `json:"title"` 13 | Description string `json:"description"` 14 | } 15 | 16 | func Get(url string) (*Page, error) { 17 | resp, err := http.Get(url) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer resp.Body.Close() 22 | 23 | doc, err := html.Parse(resp.Body) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // START OMIT 29 | page := &Page{} 30 | var f func(*html.Node) 31 | f = func(n *html.Node) { 32 | if n.Type == html.ElementNode && n.Data == "title" { 33 | page.Title = n.FirstChild.Data 34 | } 35 | if n.Type == html.ElementNode && n.Data == "meta" { 36 | if isDescription(n.Attr) { 37 | for _, attr := range n.Attr { 38 | if attr.Key == "content" { 39 | page.Description = attr.Val 40 | } 41 | } 42 | } 43 | } 44 | for c := n.FirstChild; c != nil; c = c.NextSibling { 45 | f(c) 46 | } 47 | } 48 | f(doc) 49 | // END OMIT 50 | return page, nil 51 | } 52 | 53 | func isDescription(attrs []html.Attribute) bool { 54 | for _, attr := range attrs { 55 | if attr.Key == "name" && attr.Val == "description" { 56 | return true 57 | } 58 | } 59 | return false 60 | } 61 | 62 | func handler() { 63 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 64 | rawurl := r.FormValue("url") 65 | if rawurl == "" { 66 | http.Error(w, "url not specified", http.StatusBadRequest) 67 | return 68 | } 69 | p, err := Get(rawurl) 70 | if err != nil { 71 | http.Error(w, "request failed", http.StatusInternalServerError) 72 | return 73 | } 74 | w.Header().Set("Content-Type", "application/json") 75 | enc := json.NewEncoder(w) 76 | if err := enc.Encode(p); err != nil { 77 | http.Error(w, "encoding failed", http.StatusInternalServerError) 78 | return 79 | } 80 | }) 81 | } 82 | 83 | func main() { 84 | handler() 85 | log.Fatal(http.ListenAndServe(":8080", nil)) 86 | } 87 | -------------------------------------------------------------------------------- /2019/treasure-go/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | type Stack struct { 4 | s []interface{} 5 | } 6 | 7 | func (s *Stack) Pop() interface{} { 8 | return s.s[len(s.s)-1] 9 | } 10 | 11 | func (s *Stack) Push(i interface{}) { 12 | s.s = append(s.s, i) 13 | } 14 | -------------------------------------------------------------------------------- /2019/treasure-go/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPushAndPop(t *testing.T) { 8 | s := &Stack{} 9 | s.Push(1) 10 | if r := s.Pop(); r != 1 { 11 | t.Fatalf("want 1, got %v", r) 12 | } 13 | if r := s.Pop(); r != nil { 14 | t.Fatalf("want nil, got %v", r) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /2019/treasure-javascript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /2019/treasure-javascript/deck.mdx: -------------------------------------------------------------------------------- 1 | # This is the title of my deck 2 | 3 | --- 4 | 5 | # About Me 6 | 7 | --- 8 | 9 | ```jsx 10 | 11 | ``` 12 | 13 | --- 14 | 15 | import Demo from './components/Demo' 16 | 17 | ## 18 | 19 | # The end 20 | -------------------------------------------------------------------------------- /2019/treasure-javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treasure-javascript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "mdx-deck": "^3.0.9" 9 | }, 10 | "scripts": { 11 | "start": "mdx-deck deck.mdx" 12 | }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /2021/treasure-go/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: 3 | go install golang.org/x/tools/cmd/present@latest 4 | 5 | .PHONY: get 6 | get: 7 | go get golang.org/x/tools 8 | 9 | .PHONY: present 10 | present: 11 | present 12 | -------------------------------------------------------------------------------- /2021/treasure-go/README.md: -------------------------------------------------------------------------------- 1 | https://talks.godoc.org/github.com/voyagegroup/talks/2021/treasure-go/go.slide#1 2 | -------------------------------------------------------------------------------- /2021/treasure-go/assets/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2021/treasure-go/assets/arch.png -------------------------------------------------------------------------------- /2021/treasure-go/assets/endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2021/treasure-go/assets/endpoint.png -------------------------------------------------------------------------------- /2021/treasure-go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/voyagegroup/talks/2021/treasure-go 2 | 3 | go 1.16 4 | 5 | require golang.org/x/tools v0.1.1 // indirect 6 | -------------------------------------------------------------------------------- /2021/treasure-go/go.slide: -------------------------------------------------------------------------------- 1 | Backend with Go 2 | Treasure2021版 3 | Tags: go 4 | 5 | Jumpei Chikamori 6 | Gopher at VOYAGE GROUP 7 | @pei0804 8 | https://github.com/pei0804 9 | 10 | * この資料について 11 | 12 | VOYAGE GROUPの学生エンジニア向けインターンシップTreasureの2021年度版講義資料です。 13 | 14 | .link https://github.com/voyagegroup/talks 15 | .link https://talks.godoc.org/github.com/voyagegroup/talks/2021/treasure-go/go.slide 16 | 17 | * はじめに 18 | 19 | * アイスブレイク 20 | 21 | - 自己紹介。 22 | - 好きな言語。 23 | - 好きなエディタ。 24 | - 最近つくってるもの。 25 | 26 | ブレイクアウトルームに分けます。 27 | 28 | * About Treasure 29 | 30 | - バックグラウンドの違うみなさんが3週間過ごします。 31 | - どんどん話してください。 32 | - コードを書いて、質問して、がっつり勉強してください。 33 | 34 | Enjoy! :) 35 | 36 | * バックエンド講義のゴール 37 | 38 | - 一通り小さなWebアプリケーションを書けるようになること。 39 | - Goのコードの書き方に慣れること。 40 | 41 | とにかくどんどんコードを書きましょう。 42 | 43 | .image https://golang.org/doc/gopher/project.png 44 | 45 | * 質問はガンガン投げてほしい 46 | 47 | Slackでも、口頭でも何でもおk。 48 | 49 | * 事前課題 50 | 51 | PR見ていく。 52 | 53 | * Goどうだった? 54 | 55 | 適当に聞いていく。 56 | 57 | * Goにした理由 58 | 59 | - 開発にほしいものが標準で揃ってる(fmt, mod, build, test)。 60 | - 書き方に大きな違いが発生しにくいので、レビューしやすい。 61 | 62 | * ベースアプリのアーキテクチャ 63 | 64 | それぞれの層ってどういう役割?を聞いてみる。 65 | 66 | - interface 67 | - usecase 68 | - domain 69 | - domain/repository 70 | - domain/model 71 | 72 | * ベースアプリのアーキテクチャ 73 | 74 | .image ./assets/arch.png 540 717 75 | 76 | * ベースアプリの構成 77 | 78 | - cmd コマンド。 79 | - app アプリケーションコード置き場。 80 | - app/db データベース系。 81 | - app/interfaces 利用者とのやり取り。 82 | - app/usecase 利用者の目的を達成する部分。例えば、記事を投稿する。 83 | - app/domain ビジネスロジック。 84 | - app/domain/repository 永続化層とのやり取り。 85 | - app/domain/model 型定義。 86 | 87 | * Let's Go. 88 | 89 | * 課題 1 90 | 91 | - GET /articles でタイトル検索出来るようにしたい。 92 | 93 | クエリパラメータで検索文字列を受け取って、SQLでLIKEを使って検索しよう。 94 | 95 | * 課題の進め方 96 | 97 | * 作業ブランチを切ってPRを作る 98 | 99 | ブランチ名は何でもいいけど、課題毎に作業ブランチ切って、PRを作ってください。PR名は、課題1 名前とか。 100 | 101 | git checkout -b name-kadai-1 102 | 103 | * API First 104 | 105 | API Firstで開発しよう。 106 | 107 | 1. treasure-app-2021/swagger/swagger.yamlを編集する 108 | 2. treasure-app-2021/backend/で make genする 109 | 3. 必要に応じてendpointを生やす 110 | 4. ロジックを作る 111 | 112 | 後半の開発も、まずはAPIから決めましょう。 113 | 114 | .link https://developer.ntt.com/ja/blog/58aa2ca4-ef7c-4f50-86b6-b5758df58de6 APIファーストで開発するメリットとは? 115 | .link https://stoplight.io/studio/ Stoplight Studioを使うのがおすすめ 116 | 117 | * REST API 参考資料 118 | 119 | .link https://developer.mozilla.org/ja/docs/Web/HTTP/Status MDN Web docs 120 | 121 | .link https://www.oreilly.co.jp/books/9784873116860/ Web API: The Good Parts 122 | 123 | .link https://developer.github.com/v3/ Github REST APIv3 124 | 125 | .link https://qiita.com/mserizawa/items/b833e407d89abd21ee72#%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%82%BD%E3%83%BC%E3%83%88%E6%A4%9C%E7%B4%A2%E3%81%AF%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A7%E3%82%84%E3%82%8D%E3%81%86 翻訳: WebAPI 設計のベストプラクティス 126 | 127 | * 命名規則 128 | 129 | Railsを参考にしました。 130 | 命名などで悩まず、今日はガンガン書いてほしい意図で採用しているだけなので、基本的には自由です。 131 | 132 | .link https://railsguides.jp/routing.html ルーティング 133 | 134 | .link https://railsguides.jp/active_record_basics.html アクティブレコード 135 | 136 | * レビュー 137 | 138 | 適当にPR見ていく。 139 | 140 | * コラム: Swaggerとは 141 | 142 | .link https://swagger.io/ Swagger 143 | 144 | APIのドキュメント化に使えるツール。今回はSwagger2を使っている。 145 | ちなみに、OpenAPI3とSwagger2は別物になる。元々はSwaggerフレームワークの一部だったけど、色々あったらしい。 146 | 147 | なぜ、OpenAPI3ではなく、Swagger2を採用したか? 148 | それは、関連するツールがSwagger2には対応してるけど、OpenAPI3には対応してないパターンが多いから。 149 | 私も過去に、Swagger2系のOSSを作っていたので分かるけど、Swagger2とOpenAPI3に全然互換性がなくて、追従出来なくてやる気を失ったことがある。使い込まれてるツールほど、後から出てきたOpenAPIに追従出来ていない現状がある。 150 | 151 | .link https://ackintosh.github.io/blog/2018/05/12/openapi-generator/ OpenAPI Generator - community drivenで成長するコードジェネレータ 152 | .link https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/ What Is the Difference Between Swagger and OpenAPI? 153 | 154 | * 課題 2 155 | 156 | - GET /articles/{article_id} で記事詳細を取ってきた時に、作成者の情報も返したい。 157 | 158 | * 作業ブランチを切ってPRを作る 159 | 160 | git checkout -b name-kadai-2 161 | 162 | * レビュー 163 | 164 | 適当にPR見ていく。 165 | 166 | * コラム: 現場ではどんなアーキテクチャが採用されているか 167 | 168 | DDDもクリーンアーキテクチャも特に採用していません。 169 | 誰かが考えたアーキテクチャが、自分たちの仕事に、完全にマッチすることはまずないので、それぞれのやりたいことに合わせてアーキテクチャを考える。 170 | では、どのように考える力を鍛えるか? 171 | 172 | 色んな本とコードを読んで、引き出しを増やす。しかない。つまり、頑張ろう。 173 | 174 | * 課題 3 175 | 176 | - 記事へコメントできるようにしたい。 177 | 178 | やること 179 | 180 | - APIも考える 181 | - 記事へコメントが出来る。 182 | - 誰がコメントしたか分かる 183 | - コメントはコメント作成者のみ削除できる。 184 | 185 | * ディスカッションしてみよう 186 | 187 | ブレイクアウトルームへ分かれます。 188 | 189 | - エンドポイントのパスは? 190 | - HTTPメソッドは何が望ましい? 191 | - Request Body 192 | - Response Body 193 | - HTTP Status Code 194 | 195 | 終わったら、何人か当てて、どんなAPIにするか聞いてみる。 196 | 197 | * 作業ブランチを切ってPRを作る 198 | 199 | git checkout -b name-kadai-3 200 | 201 | * レビュー 202 | 203 | 適当にPR見ていく。 204 | 205 | * コラム: どこでバリデーションするか? 206 | 207 | - 他人の記事消そうとしたら? 208 | - そもそもパラメーターが期待しているものと違う? 209 | - 自分以外のコメントを削除しようとしたら? 210 | 211 | 適当に当てて聞く。 212 | 213 | * 課題 4 214 | 215 | プロフィール情報が取得できるエンドポイント。API設計も含めてやる。 216 | 217 | やること 218 | 219 | - APIも考える。 220 | - どんな情報返したいかは考える。 221 | - 余力があれば、マイページ的な情報を返すには?も考える。 222 | 223 | * ディスカッションしてみよう 224 | 225 | ブレイクアウトルームへ分かれます。 226 | 227 | - エンドポイントのパスは? 228 | - HTTPメソッドは何が望ましい? 229 | - Request Body 230 | - Response Body 231 | - HTTP Status Code 232 | 233 | * 作業ブランチを切ってPRを作る 234 | 235 | git checkout -b name-kadai-4 236 | 237 | * レビュー 238 | 239 | 適当にPR見ていく。 240 | 241 | * コラム: コードを書く時に考えていること 242 | 243 | - 推測が必要なコードになってないか 244 | - 早すぎる最適化が行われてないか 245 | - コードから仕様が見えるか 246 | - コードから意図が読み取れない時は、コメントを書く 247 | - 不安な部分がないか 248 | - クラス、メソッドは一言で説明できるか 249 | - 参照透過性があるか 250 | - 冪等性があるか 251 | - テストを通すための差分が発生してないか 252 | - テスト名が曖昧な表現になってないか 253 | - 依存の方向が逆流してないか 254 | 255 | みんなどんなこと意識してる? 256 | 257 | * 課題5 258 | 259 | 記事にタグ付けする。 260 | 261 | やること 262 | 263 | - APIも考える。 264 | - タグを作成できる。 265 | - 記事投稿時にタグをつけれる。 266 | - 記事更新時にタグを更新できる。 267 | 268 | * ディスカッションしてみよう 269 | 270 | ブレイクアウトルームへ分かれます。 271 | 272 | - エンドポイントのパスは? 273 | - HTTPメソッドは何が望ましい? 274 | - Request Body 275 | - Response Body 276 | - HTTP Status Code 277 | 278 | * 作業ブランチを切ってPRを作る 279 | 280 | git checkout -b name-kadai-5 281 | 282 | * レビュー 283 | 284 | 適当にPR見ていく。 285 | 286 | * まとめ 287 | 288 | * フルサイクル開発するということ 289 | 290 | VOYAGE GROUPでは、フルサイクル開発が一般的です。 291 | フルサイクルとは、フルスタックとは違います。フルサイクルの責任範囲はサービス全体に渡ります。つまり、サービスに必要なことを全部やっていると、必然的にフルスタックなスキルはつきますが、それを目指しているわけではありません。 292 | 基本的には、サービスに必要なことをただやる。それだけです。 293 | 294 | 今回の講義の様に、はい!作ってください!ではなく、本当はそれ本当に必要?から考えていきます。ここらへんのマインドは後半開発でたっぷり堪能できますので、楽しみにしててください! 295 | 296 | .link https://techlog.voyagegroup.com/entry/2019/02/04/171325 Netflixにおけるフルサイクル開発者―開発したものが運用する 297 | 298 | * おすすめの書籍 299 | 300 | 聞かれそうだったので、先におすすめをしておくスタイル。 301 | 302 | .link https://tanzu.vmware.com/content/blog/beyond-the-twelve-factor-app Beyond the twelve factor app 303 | .link https://qiita.com/IshitaTakeshi/items/e4145921c8dbf7ba57ef The Zen of Python 304 | .link https://www.amazon.co.jp/%E3%82%A4%E3%82%B7%E3%83%A5%E3%83%BC%E3%81%8B%E3%82%89%E3%81%AF%E3%81%98%E3%82%81%E3%82%88%E2%80%95%E7%9F%A5%E7%9A%84%E7%94%9F%E7%94%A3%E3%81%AE%E3%80%8C%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AA%E6%9C%AC%E8%B3%AA%E3%80%8D-%E5%AE%89%E5%AE%85%E5%92%8C%E4%BA%BA/dp/4862760856 イシューからはじめよ 305 | .link https://www.amazon.co.jp/Lean%E3%81%A8DevOps%E3%81%AE%E7%A7%91%E5%AD%A6%EF%BC%BBAccelerate%EF%BC%BD-%E3%83%86%E3%82%AF%E3%83%8E%E3%83%AD%E3%82%B8%E3%83%BC%E3%81%AE%E6%88%A6%E7%95%A5%E7%9A%84%E6%B4%BB%E7%94%A8%E3%81%8C%E7%B5%84%E7%B9%94%E5%A4%89%E9%9D%A9%E3%82%92%E5%8A%A0%E9%80%9F%E3%81%99%E3%82%8B-impress-top-gear%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-ebook/dp/B07L2R3LTN/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=17Q48SNSXM4LO&dchild=1&keywords=lean%E3%81%A8devops%E3%81%AE%E7%A7%91%E5%AD%A6&qid=1624241000&s=books&sprefix=lean%E3%81%A8%2Cstripbooks%2C250&sr=1-1 LeanとDevOpsの科学 306 | .link https://www.amazon.co.jp/%E3%82%A8%E3%83%83%E3%82%BB%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E6%80%9D%E8%80%83-%E6%9C%80%E5%B0%91%E3%81%AE%E6%99%82%E9%96%93%E3%81%A7%E6%88%90%E6%9E%9C%E3%82%92%E6%9C%80%E5%A4%A7%E3%81%AB%E3%81%99%E3%82%8B-%E3%82%B0%E3%83%AC%E3%83%83%E3%82%B0%E3%83%BB%E3%83%9E%E3%82%AD%E3%83%A5%E3%83%BC%E3%83%B3-ebook/dp/B00QQKCV6E/ref=sr_1_1?adgrpid=52892487083&dchild=1&gclid=CjwKCAjwzruGBhBAEiwAUqMR8P0bgLiK9fTZ9uFr6ycS8raA1kohLEg1dsR4U5oT638CMB9WvOvzyRoCS6cQAvD_BwE&hvadid=338579871296&hvdev=c&hvlocphy=1009307&hvnetw=g&hvqmt=e&hvrand=9901430831330722557&hvtargid=kwd-307897582519&hydadcr=17961_11158981&jp-ad-ap=0&keywords=%E3%82%A8%E3%83%83%E3%82%BB%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E6%80%9D%E8%80%83&qid=1624241026&sr=8-1 エッセンシャル思考 307 | -------------------------------------------------------------------------------- /2021/treasure-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 4 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 5 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 6 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 7 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 8 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 9 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 18 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 19 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 20 | golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs= 21 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 22 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | -------------------------------------------------------------------------------- /2022/treasure-go/Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | present 3 | -------------------------------------------------------------------------------- /2022/treasure-go/README.md: -------------------------------------------------------------------------------- 1 | https://talks.godoc.org/github.com/voyagegroup/talks/2022/treasure-go/go.slide#1 2 | -------------------------------------------------------------------------------- /2022/treasure-go/fat_controller.go: -------------------------------------------------------------------------------- 1 | package contoller 2 | 3 | import "net/http" 4 | 5 | type FatController struct{} 6 | 7 | func (c *FatController) UpdateHoge(w http.ResponseWriter, r *http.Request) { 8 | // リクエストの処理 (クエリパラメータ、リクエストボディの処理など) 9 | // ビジネスロジック 10 | // DBとのやり取り 11 | // レスポンスの処理 12 | } 13 | -------------------------------------------------------------------------------- /2022/treasure-go/go.mod: -------------------------------------------------------------------------------- 1 | module treasure-go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/yuin/goldmark v1.4.1 // indirect 7 | golang.org/x/tools v0.1.10 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /2022/treasure-go/go.slide: -------------------------------------------------------------------------------- 1 | Backend with Go 2 | Treasure 2022 3 | Tags: go 4 | 5 | Shinya Suzuki 6 | Gopher at CARTA HOLDINGS 7 | @Yangyang_pareto 8 | https://github.com/shinya-ml 9 | 10 | * はじめに 11 | 12 | * アイスブレーク 13 | 14 | ブレイクアウトルームに分かれて色々話しましょう 15 | 16 | - 好きなプログラミング言語 17 | - 好きなエディタ 18 | - Treasureの意気込みとか 19 | 20 | * バックエンド講義の目標 21 | 22 | APIの開発がいいかんじにできるようになる 23 | 24 | いいかんじとは 25 | 26 | - 自分で新しいAPIを開発できる 27 | - 各レイヤーの責務を理解し、どこにどういう処理を書けばいいのかだいたいわかる 28 | - 設計時に考慮しなければならないことがだいたいわかる 29 | 30 | * 講義の進め方 31 | 32 | いくつか課題を用意したので、コードを書いてもらったり議論したりします 33 | 34 | たくさんコードを書きましょう 35 | 36 | 積極的に質問しましょう 37 | 38 | - 自力で解決できないことはきっとある 39 | - 聞けずに手が止まってしまうのはもったいない 40 | 41 | 自分の考えはどんどん言っていきましょう 42 | 43 | - そこから新たな発見があったり、議論が盛り上がるかもしれません 44 | - 間違ってたとしても、それはそれで学びがあるので恐れずに 45 | 46 | * 質問するときのポイント 47 | 48 | - 自分がなにをしたのか、どういうログやエラーメッセージが出ているのかといった事実を省かず具体的に伝える 49 | - 自分がどこまでわかっていてどこがわからないのかを書くと、教える側もどこから教えればよいのか考えやすい 50 | - 自力で解決するためにやったことがあればそれも伝える 51 | 52 | * Go言語について 53 | 54 | - 標準でいろいろそろっている (fmt, build, test, module...) 55 | - 大体素朴な書き方になるので読みやすい 56 | - Gopherくんかわいい 57 | 58 | .image https://go.dev/blog/gopher/gopher.png 59 | 60 | * Goに入ってはGoに従え 61 | 62 | Goらしく書く 63 | 64 | - 変数やpackageの命名に関するルール (こうすると良いというの) が言語化されている 65 | - 標準ライブラリのコードを読んだりすると理解が深まるかも 66 | .link https://cs.opensource.google/go/go/+/master:src/io/io.go io 67 | .link https://cs.opensource.google/go/go/+/master:src/net/http/ net/http 68 | 69 | 参考文献 70 | .link https://go.dev/doc/effective_go Effective Go 71 | .link https://github.com/golang/go/wiki/CodeReviewComments Go Code Review Comments 72 | .link https://github.com/golang/go/wiki/TestComments Go Test Comments 73 | 74 | 75 | * アーキテクチャ 76 | 77 | .image ./img/arch.png 540 717 78 | 79 | * アーキテクチャ設計で私が考えること 80 | 81 | - 関心事の分離 82 | - 依存の流れ 83 | 84 | クリーンアーキテクチャでググると出てくるあの図に従うとかはする必要ない 85 | 86 | 上に書いてあることを守るために、必要になったら必要な層を足す 87 | 88 | インターフェースも必要になったら作れば良い 89 | 90 | * 各レイヤーの役割 91 | 92 | - interface: API利用者 (Client) とのやりとりをする 93 | - usecase: 利用者の目的を達成する 94 | - domain/model: サービスの中心となるオブジェクトを定義する場所 95 | - domain/repository: 永続化層とのやりとりをする 96 | - interface/handler: HTTPリクエストを受け取り、レスポンスを返す 97 | - interface/middleware: handlerの前後にやりたい処理をする (認可とかロギングとか) 98 | 99 | * 関心事の分離 100 | 101 | 今回のアーキテクチャにおける、各レイヤーの関心事はざっくり以下の通り 102 | 103 | - handler層: HTTP リクエスト / レスポンス 104 | - usecase層: ビジネスロジック 105 | - repository層: 永続化層 (DB) とのやりとり 106 | 107 | 各レイヤーが複数の関心事を持たないようにする 108 | 109 | .link https://www.ogis-ri.co.jp/otc/hiroba/others/OOcolumn/single-responsibility-principle.html 単一責任の原則 110 | 111 | - 変更しやすくなる 112 | - 認知負荷を下げる 113 | - テストしやすくなる 114 | 115 | * 関心事を分けなくて4ぬ例: ファットコントローラー 116 | 117 | コントローラー (今回でいうhandler的なレイヤー)にあらゆる関心事を書く 118 | 119 | .code fat_controller.go 120 | 121 | - 認知負荷の増大 122 | - 様々な理由ですぐ壊れる 123 | 124 | 125 | * ざっくり処理の流れ 126 | 127 | .image ./img/sequence_diagram.png 540 780 128 | 129 | * 現状できること 130 | 131 | - 記事の一覧取得、特定の記事詳細の取得、記事の作成、更新、削除 132 | - 記事に対するコメントの作成、削除 133 | - 自身のユーザー情報の取得 134 | 135 | * 適当なエンドポイントについて、コードを追ってみる 136 | 137 | - 記事一覧を取得するAPIの流れを見てみよう 138 | 139 | リクエストが来てからどういう流れでAPIがレスポンスを返すのか把握する 140 | 141 | * APIファーストで開発する 142 | 143 | - swagger.ymlをいじる 144 | - DB側に変更が必要ならマイグレーションスクリプトを書く (マイグレーションについては後述) 145 | - 新たにエンドポイントが必要なら生やす 146 | - ロジックを書く 147 | 148 | * Swagger 149 | 150 | OpenAPIというREST APIを記述するためのフォーマットでAPIを設計する際に用いるツール 151 | 152 | OpenAPIという共通言語でAPIを定義することでフロント/バックのコミュニケーションがとりやすい 153 | 154 | バックエンド側では、レスポンスに乗せるmodelの生成に使っている 155 | 156 | .link https://swagger.io/docs/specification/about/ What is OpenAPI? 157 | .link https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/ difference between swagger and openapi 158 | 159 | 160 | * DBのマイグレーション 161 | 162 | システムを運用していくと、DBへの変更というのは何度も起こる 163 | 164 | - テーブルの追加 165 | - カラムの追加 166 | - etc... 167 | 168 | 適用したいDDLをスクリプトとして管理し、適用されてないものだけ実行する 169 | 170 | - DBがどの状態かわかる (=どのDDLまで適用されているか) 171 | - 本番・ステージングといった複数環境への変更適用が楽 (マイグレーション流せば終わり) 172 | 173 | * 今回使うマイグレーションツール 174 | 175 | .link https://flywaydb.org/ flyway 176 | 177 | `treasure-app-2022/database/migration/schema` にスクリプトがおいてある 178 | 179 | ファイルの命名規則 180 | .link https://flywaydb.org/documentation/concepts/migrations#versioned-migrations Versioned Migrations 181 | 182 | .image ./img/migration_naming_convention.png 300 600 183 | 184 | * DBに対して変更を加えたいとき 185 | 186 | 1. 前述の命名規則で新しいスクリプトファイルを作る (versionが既存のものより最新になるようにする) 187 | 2. *make* *flyway/migrate* で適用する 188 | 3. *make* *flyway/info* でちゃんと適用されてるか確認する 189 | 190 | 文法エラーなどでマイグレーションに失敗した場合 191 | 192 | - *make* *flyway/info* すると該当バージョンのステータスがfailedになる 193 | - 直したら *make* *flyway/repair* するとステータスがpending (適用前) に戻るので、再度マイグレーションする 194 | 195 | * マイグレーションで注意したいこと 196 | 197 | Undoはしない 198 | 199 | - いわゆる変更の巻き戻し 200 | - 例えば新たなカラムの追加を巻き戻すと、そのカラムのデータが消えることを意味する 201 | - 変更したけどもとに戻したいときは、それを打ち消すマイグレーションスクリプトを書こう 202 | 203 | アプリケーション側で問題が起きないかを意識する 204 | 205 | - カラムの削除とかはアプリケーション側で参照している箇所があると壊れる 206 | - 先にアプリケーション側で参照しないようにしてからDB側をいじるなど、安全にリリースすることを意識しよう 207 | 208 | 209 | * Let's Go 210 | 211 | * 課題1: 記事一覧を作成日時でソートできるようにしたい 212 | 213 | - `GET/articles` で作成された記事一覧が取得できる 214 | - 作成日時 (created_at) でORDER BYする 215 | - クエリパラメータで昇順か降順かを指定して遅れるようにする 216 | .link https://webtan.impress.co.jp/e/2012/04/26/12663#:~:text=%E3%80%8C%E3%82%AF%E3%82%A8%E3%83%AA%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%80%8D%E3%81%A8%E3%81%84%E3%81%86%E3%81%AE%E3%81%AF,%E6%AC%A1%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8B%E3%80%82&text=%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%80%81%E3%80%8C%3F,gaiq%E3%80%8D%E3%81%8C%E3%82%AF%E3%82%A8%E3%83%AA%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A0%E3%80%82 クエリパラメータとは 217 | 218 | * 適当にブランチを切ってやってみよう 219 | 220 | ブランチ名はなんでもいい 221 | 222 | $ git checkout -b your-name-kadai-1 223 | 224 | PRを作る、誰のどの課題かがわかるようなPR名にしてくれると嬉しい 225 | 226 | - 課題1 yanyanとか 227 | 228 | * 作るときに考えたいこと 229 | 230 | クエリパラメータないときは? 231 | 意図しない値がクエリパラメータに来たらどうする? 232 | 233 | - ほっといたらSQLを実行するタイミングでエラー吐く 234 | - クエリパラメータのバリデーションは誰の責任? 235 | 236 | .link https://ikenox.info/blog/validation-in-clean-arch/ バリデーションはどこでやるべきか 237 | 238 | * ヒント (どこから手を付けていいかわからない人向け) 239 | 240 | - まずswagger.ymlの `GET/articles` の定義を見てみよう 241 | - そこにクエリパラメータの定義を足してみる 242 | .link https://swagger.io/docs/specification/2-0/describing-parameters/ swagger パラメータの書き方 243 | - クエリパラメータはHTTP リクエストに乗ってくるものなので、handler層で処理しよう 244 | .link https://pkg.go.dev/net/url#URL.Query クエリパラメータのとり方 245 | - クエリパラメータから取得した値をhandler->usecase->repositoryと流して、SQL文にORDER BY句を足そう 246 | 247 | * PRを見ていく 248 | 249 | * 課題2: 記事についたコメント一覧を取ってこれるようにしたい 250 | 251 | - コメントの作成と削除はすでにできる 252 | - ある記事のコメント一覧取得と、コメントの更新ができるエンドポイントを新しく作る 253 | 254 | 速攻でできちゃった人は 255 | 256 | - 削除と更新は自分のコメントだけできる状態にする 257 | 258 | * ブランチ切ってやってみよう 259 | 260 | 設計するときに考えてほしいポイント 261 | 262 | - エンドポイントのURIどうする? 263 | - HTTPメソッドは? 264 | - Request / Response Body どうする? 265 | - HTTP Status Code 266 | 267 | * ヒント (どこから手をつけていいかわからない人向け) 268 | 269 | - swagger.ymlにAPIの定義を足してみる (すでにある定義を参考にしてみよう) 270 | - server.goにエンドポイントを生やす 271 | - usecase, repository層に処理を書く 272 | 273 | * 課題3: 記事にタグを付けたい 274 | 275 | GoでAPI書きました!!みたいな記事ならGo, WebAPIみたいなタグをつける的な 276 | 277 | - タグを作成できる 278 | - 記事にタグを付ける 279 | - 関連する(= 同じタグがついてる)記事をもってくる 280 | 281 | 順番に作っていく 282 | 283 | * 課題3.1: タグを作成できるようにする 284 | 285 | - 記事につけるためのタグを作れるようにする 286 | 287 | * ポイント 288 | 289 | - 課題2と同じポイント 290 | - 作られたタグを管理するテーブル 291 | - タグの重複を許すか許さないか問題 292 | 293 | * PRを見ていく 294 | 295 | * コラム: 良いAPIとは 296 | 297 | - どんなAPIが良い設計だと思う? 298 | - ブレイクアウトルームに分かれて議論してみよう 299 | 300 | * 良いAPIとは (私の考え) 301 | 302 | _正しい使い方をするのが簡単で、間違った使い方をするのが難しい_ 303 | 304 | - つまり、APIを使う側のことを考えて設計しよう (Client First) 305 | - URIを見ればなにをするAPIなのかわかる 306 | - 命名の一貫性 307 | - 適切なレスポンス設計 308 | 309 | 読めば使い方がわかる、使えるAPIは良い 310 | 311 | - 変更しやすいとか、頑健であるとか他にも色々な観点はある 312 | 313 | * 課題3.2: 記事にタグを付けれるようにしたい 314 | 315 | - 3.1で作ったタグを記事と紐付けれるようにしたい 316 | - 記事作成時にタグをつけれるようにする 317 | - 記事の更新時にタグも更新できるようにする 318 | 319 | * ポイント 320 | 321 | - 3.1と同様の話 322 | - 記事とタグをどうやって紐付ける? 323 | - 記事の更新時にタグも更新できるようにするには? 324 | - 記事の更新時、すでについてるタグがRequestで送られてこなかったときの解釈 (つけたまま?それとも外してしまう?) 325 | - 作成されていないタグをつけようとしたらどうなる? 326 | 327 | * PRを見ていく 328 | 329 | * 課題3.3: ある記事に関連する記事を取得できるようにする 330 | 331 | - ある記事と同じタグをもつ記事たちを取ってくる 332 | 333 | * ポイント 334 | 335 | - エンドポイント設計 336 | - どうやって同じタグをもつ記事を引っ張ってくるか 337 | 338 | * PRを見ていく 339 | 340 | * コラム: GraphQLという選択肢 341 | 342 | GraphQLはクエリ言語とクエリを実行するサーバーサイドエンジンの仕様である 343 | 344 | .link https://graphql.org/ graphql.org 345 | 346 | - スキーマ定義を読めばどうクエリするとどんなデータが返ってくるかがわかる 347 | - クライアント側がほしいフィールドだけリクエストすることができる 348 | - 様々なユースケースに対して柔軟なクエリが書ける 349 | 350 | GraphQLとRESTは全く別物 (片方が一方の上位互換というわけではない) 351 | 352 | 353 | * 課題4: 他のユーザーをフォローできるようにしたい 354 | 355 | - ユーザーをフォローする、フォロー解除する 356 | - 自分がフォローしたユーザー一覧を見れるようにする 357 | - フォローしたユーザーの記事一覧を見れるようにする 358 | 359 | * ポイント 360 | 361 | - フォロー/アンフォローのAPI設計どうする? 362 | - 他人のフォロー情報見れるようにする? 363 | - フォローしたユーザーの記事一覧を出すAPI設計は? 364 | 365 | * PRを見ていく 366 | 367 | * おわりに 368 | 369 | * フルサイクル開発 370 | 371 | - 開発者はサービスのサイクル全体にオーナーシップを持とうという考え方 372 | - 今回やったような、与えられた要件に従って開発するだけが仕事ではない 373 | - その機能の必要性や重要性を考えるところから、どうつくるのか、作ってから運用してみてどうだったかまでを作った人が責任を持つ 374 | 375 | .link https://techblog.cartaholdings.co.jp/entry/2019/02/04/171325 Netflixにおけるフルサイクル開発者-開発したものが運用する 376 | 377 | *チーム開発ではフルサイクルな開発を意識してほしい* 378 | 379 | - フロントが得意だからといってフロントだけをやるのではもったいない 380 | - 自分が作りたい機能の実現に必要なことは全部やってみてほしい 381 | 382 | * コンフォートゾーンのちょっと外側に行く 383 | 384 | - 今の自分にとって苦手なこと、できないことにチャレンジする 385 | - 成長してコンフォートゾーンが広がると、別の課題が見えてきてチャレンジできる幅が広がる 386 | 387 | .image ./img/comfort_zone.png 350 550 388 | 389 | .link http://slope.valuesv.jp/2020/07/01/%E6%88%90%E9%95%B7%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF%E3%80%8C%E3%82%B3%E3%83%B3%E3%83%95%E3%82%A9%E3%83%BC%E3%83%88%E3%82%BE%E3%83%BC%E3%83%B3%E3%80%8D%E3%82%92%E5%87%BA%E3%82%88/ [出典]成長するには「コンフォートゾーン」を出よ 390 | 391 | * チーム開発で意識してほしいこと 392 | 393 | 「やること」と「やらないこと」を決める 394 | 395 | - やりたいと思ったことすべてを実現することはできない 396 | - 自分たちにとって必要・重要だと思うことと、あったらよさそうだけど重要ではないと思うことを分けてみよう 397 | 398 | * チーム開発で意識してほしいこと 399 | 400 | たくさんデプロイしよう 401 | 402 | - 開発 -> リリース -> 触ってみる -> 改善 のサイクルを小さく早くたくさん回す 403 | - 本番環境で触ってみて初めて分かることもある 404 | - とにかく、たくさんコードを書いてほしい 405 | 406 | * コラム: おすすめ本 407 | 408 | .link www.amazon.co.jp/dp/4048930656 Clean Architecture 409 | .link www.amazon.co.jp/dp/4873116864 Web API: The Good Parts 410 | .link www.amazon.co.jp/dp/B09HK66P5X デザインパターン入門 411 | .link www.amazon.co.jp/dp/B09B8LFKQL A Philosophy of Software Design 412 | .link www.amazon.co.jp/dp/4873119693 実用Go言語 413 | -------------------------------------------------------------------------------- /2022/treasure-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= 2 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 3 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 4 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 5 | -------------------------------------------------------------------------------- /2022/treasure-go/img/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2022/treasure-go/img/arch.png -------------------------------------------------------------------------------- /2022/treasure-go/img/comfort_zone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2022/treasure-go/img/comfort_zone.png -------------------------------------------------------------------------------- /2022/treasure-go/img/endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2022/treasure-go/img/endpoint.png -------------------------------------------------------------------------------- /2022/treasure-go/img/me.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2022/treasure-go/img/me.JPG -------------------------------------------------------------------------------- /2022/treasure-go/img/migration_naming_convention.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /2022/treasure-go/img/migration_naming_convention.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2022/treasure-go/img/migration_naming_convention.png -------------------------------------------------------------------------------- /2022/treasure-go/img/sequence_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voyagegroup/talks/ef9fdd5be398c1a4d15e6f914825597cb01cad5e/2022/treasure-go/img/sequence_diagram.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 VOYAGE GROUP, Inc 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # talks by VOYAGE GROUP 2 | 3 | see also: https://voyagegroup.com/design-and-tech/slide/ 4 | 5 | # LICENSE 6 | 7 | The image files of gopher which is created by Renee French are provided under the [Creative Commons 3.0 Attribution](https://creativecommons.org/licenses/by/3.0/) license. --------------------------------------------------------------------------------