Go言語でTDDする勉強会 第6回(2020/02/05) 発表メモ (番外編). 「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」 第8章(8.3 8.4), 第3章(3.3.4) 解説
- [追記] 第6回(2020/02/05) 発表後に内容を一部追加したので、 第7回(2020/02/19) 発表時に補足説明 します
- 8.3 と 8.4 の フロー図
- 3.3.4 ハンドラ関数のチェイン の 解説 と、フレームワーク echo の 該当コード
- 書籍:「
Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る
」 から 以下の項 の 概要を少しだけ解説 します. - 詳細を知りたい方は ぜひ書籍のご購入を -> Amazon
https://github.com/mushahiroyuki/gowebprog
本をお読みでない方もダウンロードしてお試しいただけます。 次のいずれかの方法でダウンロードしてください。
ターミナルで $GOPATH/src など適当なディレクトリ(フォルダ)に移動してから、下記のコマンドを実行してください(gitが必要です)。
git clone https://github.com/mushahiroyuki/gowebprog.git
あるいは、ページ右上の[Clone or download]をクリック(タップ)して表示される[Download ZIP]でダウンロードしてください。
例1) ghq
インストール 済みの場合
ghq get mushahiroyuki/gowebprog
または
例2) ghq
未インストール で $GOPATH/src
以下に展開する場合
mkdir -p $GOPATH/src/github.com/mushahiroyuki
git clone https://github.com/mushahiroyuki/gowebprog.git $GOPATH/src/github.com/mushahiroyuki/gowebprog
go get github.com/lib/pq
補足: github.com/lib/pq
は data.go
で import しているライブラリ
import (
"database/sql"
_ "github.com/lib/pq"
)
以下は local で docker-compose
が動作する環境の方のみ 実行して下さい
docker image pull postgres:12.1-alpine
cd $GOPATH/src/github.com/mushahiroyuki/gowebprog
cd ch08/11httptest_2
ch08/11httptest_2
ディレクトリ 以下の 4ファイル を確認
% ls
data.go server.go server_test.go setup.sql
go test
結果が以下のように test 失敗 になれば OK
--- FAIL: TestHandleGet (0.00s)
server_test.go:36: Response code is 500
server_test.go:41: Cannot retrieve JSON post
--- FAIL: TestHandlePut (0.00s)
server_test.go:51: Response code is 500
FAIL
exit status 1
FAIL github.com/mushahiroyuki/gowebprog/ch08/11httptest_2 0.017s
以下は local で docker-compose
が動作する環境の方のみ 試してみて下さい
version: "3"
services:
postgres:
image: postgres:12.1-alpine
container_name: gowebprog_postgres
environment:
POSTGRES_USER: gwp
POSTGRES_PASSWORD: gwp
POSTGRES_DB: gwp
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
ports:
- 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
- ./setup.sql:/docker-entrypoint-initdb.d/setup.sql
volumes:
postgres-data:
data.go
で 設定している
var Db *sql.DB
func init() {
var err error
Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
// 中略
}
- MySQL や PostgreSQL の公式コンテナでは このディレクトリ直下に存在する
*.sql
or*.sql.gz
or*.sh
が実行される
-> https://hub.docker.com/_/postgres
If you would like to do additional initialization ...中略,
add one or more*.sql
,*.sql.gz
, or*.sh
scripts under/docker-entrypoint-initdb.d
(...中略).After the entrypoint calls
initdb
to create the defaultpostgres
user and database,
it will run any*.sql
files, run any executable*.sh
scripts, and source any non-executable*.sh
scripts found in that directory to do further initialization before starting the service.Warning: scripts in
/docker-entrypoint-initdb.d
are only run if you start the container with a data directory that is empty;
追加の初期化を行いたい場合は
/docker-entrypoint-initdb.d
の直下に 1つ以上の*.sql
,*.sql.gz
,*.sh
script を追加 しますentrypoint が
initdb
を呼び出して デフォルト のpostgres
ユーザーとデータベースを作成した後,
service を開始する前に さらに初期化を行うため, その ディレクトリ で見つかった*.sql
ファイルを実行し, 実行可能な*.sh
script を実行し, 実行不可能な*.sh
script を source コマンドで実行 します警告:
/docker-entrypoint-initdb.d
の script は, 空の data ディレクトリ で container を起動した場合にのみ, 実行されます
setup.sql
を 微修正
- drop table posts;
+ drop table IF EXISTS posts; -- IF EXISTS 追加
create table posts (
id serial primary key,
content text,
author varchar(255)
);
+INSERT INTO posts VALUES (1, 'contentA', 'authorA'); -- 1行 追加
docker-compose up -d
docker-compose ps
docker container ls
go test
結果が以下のように test 成功 になれば OK
PASS
ok github.com/mushahiroyuki/gowebprog/ch08/11httptest_2 0.032s
docker-compose down --volumes
再度 go test
を実行すると 8.3 動作確認(1-2)
と同様に test 失敗 になれば OK
- local で DB (postgres) が動作していないと test が失敗 する
cd $GOPATH/src/github.com/mushahiroyuki/gowebprog
cd ch08/18dependency_injection
ch08/18dependency_injection ディレクトリ 以下の 4ファイル を確認
% ls
data.go doubles.go server.go server_test.go
go test
- コンパイルエラーになる
# github.com/mushahiroyuki/gowebprog/ch08/18dependency_injection
./server_test.go:40:3: Error call has possible formatting directive %v
FAIL github.com/mushahiroyuki/gowebprog/ch08/18dependency_injection [build failed]
- server_test.go の 40行目 を変更
- t.Error("Response code is %v", writer.Code)
+ t.Errorf("Response code is %v", writer.Code)
再度 go test
実行
go test
結果が以下のように test 成功 になれば OK
PASS
ok github.com/mushahiroyuki/gowebprog/ch08/18dependency_injection 0.013s
- local で DB (postgres) が動作していなくても test が成功 する
HandlerFunc
と Handler
の コード例: Go Playground
参考サイト: init関数のふしぎ #golang - Qiita 参照
以下のように
main
パッケージに書くとmain
関数より先に実行されます。
main
パッケージでない場合は、importするだけで呼び出されます。
ちなみに、ファイルに1つという決まりもなく、複数書けるようになっています。
参考サイト: Golangでtestingことはじめ(2)〜テストにおける共通処理〜 - DeNA Testing Blog 参照
テストファイル内に TestMain が存在している場合、 go test はTestMainのみ実行します。
testing.M の Run を呼ぶことで各テストケースが実行され
func TestMain(m *testing.M) {
setUp()
code := m.Run()
// 中略
}
func setUp() {
mux = http.NewServeMux()
mux.HandleFunc("/post/", handleRequest)
// 中略
}
type Post struct {
Id int `json:"id"`
// 中略
}
func main() {
// 中略
http.HandleFunc("/post/", handleRequest)
server.ListenAndServe()
}
// main handler function
func handleRequest(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = handleGet(w, r)
// 中略
}
// 中略
}
func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
// 中略
post, err := retrieve(id)
// 中略
}
var Db *sql.DB
func init() {
var err error
Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
// 中略
}
func retrieve(id int) (post Post, err error) {
post = Post{}
err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, /* 中略 */)
return
}
main or test から 呼び出し
|
v
+---------------+
| handleRequest |
+---------------+
|
v
+---------------+
| handleGet |
+---------------+
|
v
+---------------+
| retrieve | ---> var Db *sql.DB ---> PostgreSQL
+---------------+
課題: sql.DB
への依存関係 を 分離したい
↓↓↓
- 方針
sql.DB
への直接の依存 を 避けるsql.DB
を 別の struct でラップ- test では
sql.DB
を直接置き換えずに 外側の struct の テストダブル を使用
- test では
- どの関数 (
handleRequest
,handleGet
) もsql.DB
を直接呼び出さない
main
実装sql.DB
をPost
の一部として 注入
- test 実装
- テストダブル (
FakePost
) を使用
- テストダブル (
main or test から 呼び出し
|
| +----------------+ +----------------+
| | Post | | FakePost |
| | +------------+ | +----------------+
| | | sql.DB | |
| | +------------+ |
| +----------------+ ( main から sql.DB が Post の一部として 注入される )
v
+---------------+
| handleRequest |
+---------------+
|
| +----------------+ +----------------+
| | Post | | FakePost |
| | +------------+ | +----------------+
| | | sql.DB | |
| | +------------+ |
| +----------------+
|
| +----------------+
| | Post |
v | +------------+ |
+---------------+ invoke `fetch` method | | sql.DB | --------> PostgreSQL
| | ---------------------> | +------------+ |
| | (main) +----------------+
| handleGet |
| | invoke `fetch` method +----------------+
| | ---------------------> | FakePost |
+---------------+ (test) +----------------+
書籍の図は一部間違い有り(削除済みの retrieve
関数が載っている)
type Text interface {
fetch(id int) (err error)
// 中略
}
main
用の実装 (Post
) と テストダブル (FakePost
) が両方満たす interface を定義
type Post struct {
Db *sql.DB // ここがポイント
Id int `json:"id"`
// 中略
}
func (post *Post) fetch(id int) (err error) {
err = post.Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, /* 中略 */)
return
}
sql.DB
の ポインタ型 をPost
の一部として 定義
type FakePost struct {
Id int
// 中略
}
func (post *FakePost) fetch(id int) (err error) {
post.Id = id
return
}
- テストダブル の方には
sql.DB
は含まれない
func TestGetPost(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/post/", handleRequest(&FakePost{})) // テストダブル を DI
// 中略
}
func main() {
// connect to the Db
var err error
db, err := sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
// 中略
http.HandleFunc("/post/", handleRequest(&Post{Db: db})) // sql.DB を Post の一部として DI
server.ListenAndServe()
}
// main handler function
func handleRequest(t Text) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = handleGet(w, r, t)
// 中略
}
// 中略
}
}
func handleGet(w http.ResponseWriter, r *http.Request, post Text) (err error) {
// 中略
err = post.fetch(id)
// 中略
}
handleRequest
の 引数 の シグニチャー を変更func(ResponseWriter, *Request)
->func(Text)
func(Text)
はHandlerFunc
型 を満たさない
- しかし
handleRequest
の 戻り値 がHandlerFunc
型func handleRequest(t Text) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ... } }
- よって 以下の 呼び出し の 結果 は
HandlerFunc
型 となる- (test code)
handleRequest(&FakePost{})
- (
main
関数)handleRequest(&Post{Db: db})
- (test code)
→ ch03/10chain_handlerfunc/server.go
func log(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 中略
h(w, r)
}
}
http.HandleFunc("/hello", log(hello))
-> ch03/10chain_handlerfunc/server.go
の疑似コード例 (一部改修): Go Playground
var b http.Handler = log3(log2("hoge", log(hello)))
入れ子が深くなる. その対応法が以下のサイトに ↓
→ Middleware (Advanced) - Go Web Examples
type Middleware func(http.HandlerFunc) http.HandlerFunc
func Logging() Middleware {
return func(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 中略
f(w, r)
}
}
}
func Method(m string) Middleware {
// 中略
}
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for _, m := range middlewares {
f = m(f)
}
return f
}
func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
http.ListenAndServe(":8080", nil)
}
Chain
関数 の 第2引数 (...Middleware
) は 『「関数型」の「可変長引数」』
→ 第6回(2020/02/05) 発表メモ で解説した Functional Option Pattern
参照
-> ch03/10chain_handlerfunc/server.go
の疑似コード例 (Chain
利用): Go Playground
var b http.Handler = Chain(hello, Log(), Log2("hoge"), Log3())
「ch03/10chain_handlerfunc/server.go
の疑似コード例」 の 入れ子
var b http.Handler = log3(log2("hoge", log(hello)))
と比べてみると、 Chain
関数を利用した例は 可変長引数 でスッキリしている
ローカル環境の動作手順の例:
- How to Write Go Code - The Go Programming Language の手順で Go modules の適用
mkdir hello # Alternatively, clone it if it already exists in version control.
cd hello
go mod init github.com/user/hello
echo
の github の Installation 手順を実行
go get github.com/labstack/echo/v4
- 任意のファイル (例:
main.go
) を作成 してecho
の github の Example の内容をコピー
cat << EOF > main.go
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", hello)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}
// Handler
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
EOF
go run
で実行 して http://localhost:1323/ に アクセス
go run main.go
- アクセスする度 に ターミナル に log 出力 されている事を確認
{"time":"2020-02-16T05:47:09.244109+09:00","id":"","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"(中略)","status":200,"error":"","latency":6936,"latency_human":"6.936µs","bytes_in":0,"bytes_out":13}
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
Ctrl + C
で停止e.Use(middleware.Logger())
を コメントアウト してから 再度 実行- アクセスする度 に ターミナル に log 出力 されない 事を確認
type MiddlewareFunc func(HandlerFunc) HandlerFunc
この HandlerFunc
は http.HandlerFunc
ではなく echo.HandlerFunc
(= func(echo.Context) error
)
middleware.Logger()
の godoc, middleware.Recover()
の godoc
func Logger() echo.MiddlewareFunc
func Recover() echo.MiddlewareFunc
func (e *Echo) Use(middleware ...MiddlewareFunc)
// Use adds middleware to the chain which is run after router.
func (e *Echo) Use(middleware ...MiddlewareFunc) {
e.middleware = append(e.middleware, middleware...)
}
type (
// Echo is the top-level framework instance.
Echo struct {
// -- 中略 --
middleware []MiddlewareFunc
// -- 中略 --
}
// -- 中略 --
)
ServeHTTP
, applyMiddleware
の実装
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// -- 中略 --
h := NotFoundHandler
if e.premiddleware == nil {
e.findRouter(r.Host).Find(r.Method, getPath(r), c)
h = c.Handler()
h = applyMiddleware(h, e.middleware...)
} else {
h = func(c Context) error {
e.findRouter(r.Host).Find(r.Method, getPath(r), c)
h := c.Handler()
h = applyMiddleware(h, e.middleware...)
return h(c)
}
h = applyMiddleware(h, e.premiddleware...)
}
// Execute chain
if err := h(c); err != nil {
e.HTTPErrorHandler(err, c)
}
// -- 中略 --
}
// -- 中略 --
func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}