Open10

【読書記録】API を作りながら進む Go 中級者への道(さきさん文庫)

nukopynukopy

ブログAPI の仕様

  • ブログ記事を投稿するリクエストを受ける
  • 投稿一覧ページに表示させるデータを要求するリクエストを受ける
  • ある投稿のデータを要求するリクエストを受ける
  • ある投稿にいいねをつけるリクエストを受ける
  • ある投稿に対してコメントをするリクエストを受ける
nukopynukopy

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 も同じものをダウンロードしてくる必要があります。「確実に同じ依存パッケージを使っているのか」ということを担保するためにチェックサムの情報が必要で、これにより「どこで動かしても、同じ動きをさせることができる」ビルド再現性が実現できます。

nukopynukopy

標準パッケージ

net/http パッケージ

https://pkg.go.dev/net/http

HTTP メソッド(HTTP リクエストメソッド)

https://developer.mozilla.org/ja/docs/Web/HTTP/Methods

https://github.com/golang/go/blob/master/src/net/http/method.go

HTTP ステータスコードの定義

  • src/net/http/status.go

https://github.com/golang/go/blob/master/src/net/http/status.go

関連:gorilla/mux

https://github.com/gorilla/mux

メンテナ募集してる...

https://github.com/gorilla/mux/issues/659

nukopynukopy

log パッケージ

https://pkg.go.dev/log

https://github.com/golang/go/blob/master/src/log/log.go

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)
}
nukopynukopy

気になった点

  • 図の挿入位置
  • 多分 Web 開発少しでもやってた人じゃないと厳しそうだなと思った
nukopynukopy

gorilla/mux について

GitHub のリポジトリタイトルより

A powerful HTTP router and URL matcher for building Go web servers with 🦍

日本語訳

ゴリラで Go の Web サーバを構築するための強力な HTTP ルーター、URL マッチャー

https://github.com/gorilla/mux

ルーターによるルーティング

  • ルーティング
    • 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.RouterHandleFunc 実行時にパスとハンドラから各ルーティング先 mux.Route を登録する。そして、mux.Router.HandleFunc から http.HandleFunc へと処理が接続される。

次は http.HandlerFunchttp.HandleFunc の中身を見ていく。

FIXME: net/http → gorilla/mux の順で説明した方が分かりやすいと思う

http.HandlerFunc、http.HandleFunc は何をやっている?

TODO

http.ListenAndServe の第 2 引数の意味は?関数内でどういう処理が行われている?

https://pkg.go.dev/net/http#ListenAndServe

http.ListenAndServe 関数の第二引数というのは、実は「サーバーの中で使うルータを指定する」部分なのです。ここにルータが渡されずnil だった場合には、Go のHTTP サーバーがデフォルトで持っているルータが自動的に採用されます。

DefaultServeMux というのがGo のデフォルトルータで、実態はnet/http パッケージで定義されているhttp.ServeMux 型のグローバル変数です。

https://github.com/golang/go/blob/master/src/net/http/server.go#L2304-L2307

nukopynukopy

Q&A

  • db.Closerows.Close はなぜ必要か?原理から説明せよ。
  • トランザクションを張った際、エラー時に tx.Rollback() を実行しないまま return したらどうなる?tx.Commit() までたどり着かないから DB は更新されないとは思うけど、内部的にどうなるの?
    • 更新情報がメモリに確保されたまま関数抜けるとメモリリークする?流石によくあることだからと実装上勝手にリソース開放されるようにしてくれてる?とかとか
nukopynukopy

ログ

12/5

最後のコミットから約 1 ヶ月経ってた...