├── 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.
--------------------------------------------------------------------------------