【読書記録】API を作りながら進む Go 中級者への道(さきさん文庫)
正誤
- p.13 脚注 3 の PDF のリンクをクリックしてメルカリの記事へ飛ぶと、URL が
engineer-ing
に飛んでしまう。https://engineer-ing.mercari.com/blog/entry/2019-09-11-170000/ を開けない。改行のハイフン-
がいらないかも。正確なリンクは https://engineering.mercari.com/blog/entry/2019-09-11-170000/ 。
ブログAPI の仕様
- ブログ記事を投稿するリクエストを受ける
- 投稿一覧ページに表示させるデータを要求するリクエストを受ける
- ある投稿のデータを要求するリクエストを受ける
- ある投稿にいいねをつけるリクエストを受ける
- ある投稿に対してコメントをするリクエストを受ける
Go の用語
-
パッケージ
- 同一のディレクトリ内にまとめられた、変数、定数、関数定義の集合
-
モジュール
- パッケージの集合
- パッケージの集合を「Go のモジュール」として認識させるためには
go.mod
ファイルを作成する必要がある。
go.mod
ファイルの役割
- モジュールのルートディレクトリを示す
- モジュール内で利用しているパッケージ、モジュールの依存関係を記録する。
go.mod
ファイルを見れば、「このモジュールは〇〇というパッケージ、モジュールに依存している」ということが明確になる。
go.sum
ファイル
- Go パッケージのバージョン、それに対応するチェックサムの管理を行い、ビルド再現性の担保を行う
p56 ~ 57
- gorilla/mux をインストールした後の
go.sum
ファイル
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
「パッケージ名・バージョンの後にh1:xxxxxx~といった謎の文字列が続く」という行が、今回は
2 つできました。この謎の文字列h1:xxxxxx~はチェックサムと呼ばれるもので、ビルド再現性の担保のために使われる情報です。
このコードを別の場所でも同様に動かすためには、当然依存パッケージであるgorilla/mux も同じものをダウンロードしてくる必要があります。「確実に同じ依存パッケージを使っているのか」ということを担保するためにチェックサムの情報が必要で、これにより「どこで動かしても、同じ動きをさせることができる」ビルド再現性が実現できます。
標準パッケージ
net/http パッケージ
HTTP メソッド(HTTP リクエストメソッド)
HTTP ステータスコードの定義
src/net/http/status.go
関連:gorilla/mux
メンテナ募集してる...
log パッケージ
log.FatalXXX
系の関数はエラーの内容を出力後、os.Exit
関数(システムコール syscall.Exit
)で異常ステータスと共にプロセスを終了する。
log.FatalXXX
// Fatal is equivalent to Print() followed by a call to os.Exit(1).
func Fatal(v ...any) {
std.Output(2, fmt.Sprint(v...))
os.Exit(1)
}
// Fatalf is equivalent to Printf() followed by a call to os.Exit(1).
func Fatalf(format string, v ...any) {
std.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}
// Fatalln is equivalent to Println() followed by a call to os.Exit(1).
func Fatalln(v ...any) {
std.Output(2, fmt.Sprintln(v...))
os.Exit(1)
}
気になった点
- 図の挿入位置
- 多分 Web 開発少しでもやってた人じゃないと厳しそうだなと思った
gorilla/mux について
GitHub のリポジトリタイトルより
A powerful HTTP router and URL matcher for building Go web servers with 🦍
日本語訳
ゴリラで Go の Web サーバを構築するための強力な HTTP ルーター、URL マッチャー
ルーターによるルーティング
- ルーティング
- Web サーバが受け取った HTTP リクエストを、どのハンドラに渡して処理させるかを決めるか操作
- ルーター
- ルーティング操作を担うもの(実体としては Go の構造体)
http.HandleFunc と r.HandleFunc の違いは?
mux.NewRouter
周りのコード
mux/mux.go
// NewRouter returns a new router instance.
func NewRouter() *Router {
return &Router{namedRoutes: make(map[string]*Route)}
}
// Router registers routes to be matched and dispatches a handler.
//
// It implements the http.Handler interface, so it can be registered to serve
// requests:
//
// var router = mux.NewRouter()
//
// func main() {
// http.Handle("/", router)
// }
//
// Or, for Google App Engine, register it in a init() function:
//
// func init() {
// http.Handle("/", router)
// }
//
// This will send all incoming requests to the router.
type Router struct {
// Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler
// Configurable Handler to be used when the request method does not match the route.
MethodNotAllowedHandler http.Handler
// Routes to be matched, in order.
routes []*Route
// Routes by name for URL building.
namedRoutes map[string]*Route
// If true, do not clear the request context after handling the request.
//
// Deprecated: No effect, since the context is stored on the request itself.
KeepContext bool
// Slice of middlewares to be called after a match is found
middlewares []middleware
// configuration shared with `Route`
routeConf
}
// 中略...
// NewRoute registers an empty route.
func (r *Router) NewRoute() *Route {
// initialize a route with a copy of the parent router's configuration
route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes}
r.routes = append(r.routes, route)
return route
}
// 中略...
// HandleFunc registers a new route with a matcher for the URL path.
// See Route.Path() and Route.HandlerFunc().
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
*http.Request)) *Route {
return r.NewRoute().Path(path).HandlerFunc(f)
}
mux/route.go
type Route struct {
// Request handler for the route.
handler http.Handler
// If true, this route never matches: it is only used to build URLs.
buildOnly bool
// The name used to build URLs.
name string
// Error resulted from building a route.
err error
// "global" reference to all named routes
namedRoutes map[string]*Route
// config possibly passed in from `Router`
routeConf
}
// 中略
// Handler --------------------------------------------------------------------
// Handler sets a handler for the route.
func (r *Route) Handler(handler http.Handler) *Route {
if r.err == nil {
r.handler = handler
}
return r
}
// HandlerFunc sets a handler function for the route.
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
return r.Handler(http.HandlerFunc(f))
}
// GetHandler returns the handler for the route, if any.
func (r *Route) GetHandler() http.Handler {
return r.handler
}
// 中略...
// addRegexpMatcher adds a host or path matcher and builder to a route.
func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
if r.err != nil {
return r.err
}
if typ == regexpTypePath || typ == regexpTypePrefix {
if len(tpl) > 0 && tpl[0] != '/' {
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
}
if r.regexp.path != nil {
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
}
}
rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
strictSlash: r.strictSlash,
useEncodedPath: r.useEncodedPath,
})
if err != nil {
return err
}
for _, q := range r.regexp.queries {
if err = uniqueVars(rr.varsN, q.varsN); err != nil {
return err
}
}
if typ == regexpTypeHost {
if r.regexp.path != nil {
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
return err
}
}
r.regexp.host = rr
} else {
if r.regexp.host != nil {
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
return err
}
}
if typ == regexpTypeQuery {
r.regexp.queries = append(r.regexp.queries, rr)
} else {
r.regexp.path = rr
}
}
r.addMatcher(rr)
return nil
}
// 中略...
// Path -----------------------------------------------------------------------
// Path adds a matcher for the URL path.
// It accepts a template with zero or more URL variables enclosed by {}. The
// template must start with a "/".
// Variables can define an optional regexp pattern to be matched:
//
// - {name} matches anything until the next slash.
//
// - {name:pattern} matches the given regexp pattern.
//
// For example:
//
// r := mux.NewRouter()
// r.Path("/products/").Handler(ProductsHandler)
// r.Path("/products/{key}").Handler(ProductsHandler)
// r.Path("/articles/{category}/{id:[0-9]+}").
// Handler(ArticleHandler)
//
// Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request).
func (r *Route) Path(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, regexpTypePath)
return r
}
ここまでのまとめ
mux.Router.HandleFunc
の処理の流れを追ってみると、以下の通りになる。
-
mux.Router
-
mux.Router.HandleFunc(path, f)
-
mux.Router.NewRoute().Path(path).HandlerFunc(f)
(NewRoute
実行時にRouter.routes
に*Route
が格納される)-
mux.Route.Handler(http.HandlerFunc(f))
(ここでやっとnet/http
パッケージが出てくる。また、ここで*mux.Route
が返り、mux.Router.HandleFunc(path, f)
の返り値が*Route
であることが分かる。ただし、今回の実装例では、main スコープでこの返り値は使っていない。)
-
-
-
ルーター mux.Router
は HandleFunc
実行時にパスとハンドラから各ルーティング先 mux.Route
を登録する。そして、mux.Router.HandleFunc
から http.HandleFunc
へと処理が接続される。
次は http.HandlerFunc
、http.HandleFunc
の中身を見ていく。
FIXME: net/http → gorilla/mux の順で説明した方が分かりやすいと思う
http.HandlerFunc、http.HandleFunc は何をやっている?
TODO
http.ListenAndServe の第 2 引数の意味は?関数内でどういう処理が行われている?
http.ListenAndServe 関数の第二引数というのは、実は「サーバーの中で使うルータを指定する」部分なのです。ここにルータが渡されずnil だった場合には、Go のHTTP サーバーがデフォルトで持っているルータが自動的に採用されます。
DefaultServeMux というのがGo のデフォルトルータで、実態はnet/http パッケージで定義されているhttp.ServeMux 型のグローバル変数です。
Q&A
-
db.Close
、rows.Close
はなぜ必要か?原理から説明せよ。 - トランザクションを張った際、エラー時に
tx.Rollback()
を実行しないまま return したらどうなる?tx.Commit()
までたどり着かないから DB は更新されないとは思うけど、内部的にどうなるの?- 更新情報がメモリに確保されたまま関数抜けるとメモリリークする?流石によくあることだからと実装上勝手にリソース開放されるようにしてくれてる?とかとか
ログ
12/5
最後のコミットから約 1 ヶ月経ってた...