今読んでいる技術書の内容で大事だと思った部分を少しずつまとめていきたいと思います。
今回読んでいる本はこちら
Goプログラミング実践入門
この書籍では、フレームワークに頼らず標準ライブラリでweb開発をすることができます。
個人的にフレームワークはブラックボックスが多すぎて面倒であまり好きではない派なので、これを理解できることで理解が深まるのではないかと考え購入しました。
この本ではプログラミング言語Goとその標準のライブラリだけを使って、
ゼロからWebアプリケーションを開発するのに必要な事柄を解説します。
ほかのライブラリやその他のトピック(Webアプリケーションのテストやデプロイなど)について解説するページもありますが、Go言語の標準ライブラリのみを用いたWeb開発を解説することがこの本の主目的です。
本記事では、3章で重要だと思った考えや理解しづらかった箇所を説明しています。
HTTPでサーバーを立てる
まず、3章では以下のように書かれています。
net/httpライブラリはhttpサーバーを起動する機能を提供しており、このHTTPサーバーもリクエストを受けてそれらに対応するレスポンスを返すものです。さらに、マルチプレクサのためのインターフェースとデフォルトのマルチプレクサも提供しています。
ここでマルチプレクサという用語が出てきたので調べてみると、
マルチプレクサとは、一般的に、ふたつ以上の入力をひとつの信号として出力する機械である
と出てきました。
簡単にまとめると、様々なリクエストを受け付け、それをハンドラに流す役割があるということですかね。つまり、設定したパスにアクセスが来たら、それを紐づいたハンドラ(サーバーリクエスト時に発火するやつ)に流す役割です。
例えば、最もシンプルなサーバーのコードを書いてみると以下の通りとなります。
package main
import(
"net/http"
)
func main(){
http.ListenAndServe("", nil)
}
ここでListenAndServe
と出てきていますが、定義をみると
- 第一引数:addr(ホスト:ポート),
- 第二引数:handler
とすることでサーバーを起動できるようです。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
そして、ListenAndServe
はServer
構造体のメソッドとしても用意されています。
したがって、サーバー構造体でフィールドに任意の値を設定すれば、その設定でサーバーを起動できるようになります。
以下のコードを簡単に解説すると、
func (srv *Server) ListenAndServe() error {
// サーバーがシャットダウン中であれば、ErrServerClosedを返して処理を終了します。
if srv.shuttingDown() {
return ErrServerClosed
}
// サーバーのアドレスが指定されていない場合、デフォルトで:http(ポート80)を使用します。
addr := srv.Addr
if addr == "" {
addr = ":http"
}
// 指定されたアドレスでTCPリスナーを作成し、エラーが発生した場合はそれを返します。
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
// Serveメソッドを呼び出し、作成したリスナーを渡してサーバーを起動します。
return srv.Serve(ln)
}
まとめると以下の通りです。
ListenAndServeメソッドは、指定されたアドレスでTCP接続をリッスンし、Serveメソッドを使用して受信したリクエストを処理します。Serveメソッドは、リスナーをラップしてクローズし、HTTP/2を設定し、新しい接続を受け入れて処理します。エラーが発生した場合は適切にハンドリングし、必要に応じて再試行します。
*TCPをリッスンするとは、ネットワーク上で特定のポート番号を監視し、クライアントからの接続要求を待ち受けることを指します。
こちらは http.Server
構造体のフィールドを表にまとめたものです。
大量にありますが、重要なのはAddr
とHandler
です。(どうでもいいですがAddrはAddressの略なんですね。)
フィールド名 | 説明 |
---|---|
Addr |
サーバーがリッスンするTCPアドレスを指定します。形式は "host:port" です。空の場合、":http"(ポート80)が使用されます。 |
Handler |
HTTPリクエストを処理するハンドラ。nilの場合は http.DefaultServeMux が使用されます。 |
DisableGeneralOptionsHandler |
true の場合、"OPTIONS *" リクエストをハンドラに渡します。そうでない場合は200 OKとContent-Length: 0を返します。 |
TLSConfig |
ServeTLS や ListenAndServeTLS によって使用されるTLS構成を指定します。これらのメソッドはこの値をクローンします。 |
ReadTimeout |
リクエスト全体(ボディを含む)を読み取るための最大持続時間を指定します。0または負の値の場合、タイムアウトはありません。 |
ReadHeaderTimeout |
リクエストヘッダを読み取るための時間を指定します。0の場合、ReadTimeout の値が使用されます。両方が0の場合、タイムアウトはありません。 |
WriteTimeout |
レスポンスの書き込みのタイムアウトまでの最大持続時間を指定します。0または負の値の場合、タイムアウトはありません。 |
IdleTimeout |
keep-alivesが有効な場合、次のリクエストを待つ最大時間を指定します。0の場合、ReadTimeout の値が使用されます。両方が0の場合、タイムアウトはありません。 |
MaxHeaderBytes |
リクエストヘッダのキーと値を解析する際の最大バイト数を制御します。0の場合、DefaultMaxHeaderBytes が使用されます。 |
TLSNextProto |
ALPNプロトコルアップグレードが発生したときに提供されたTLS接続の所有権を引き継ぐ関数を指定します。HTTP/2のサポートは自動的には有効になりません。 |
ConnState |
クライアント接続の状態が変化したときに呼び出されるコールバック関数を指定します。詳細は ConnState 型と関連定数を参照してください。 |
ErrorLog |
接続を受け入れる際のエラー、ハンドラの予期しない動作、基盤となるファイルシステムエラーのためのオプションのロガーを指定します。nilの場合、標準ロガーが使用されます。 |
BaseContext |
このサーバー上のリクエストに対してベースコンテキストを返す関数を指定します。nilの場合、context.Background() がデフォルトになります。 |
ConnContext |
新しい接続 c に対して使用するコンテキストを修正する関数を指定します。提供された ctx はベースコンテキストから派生したもので、ServerContextKey 値を持ちます。 |
inShutdown |
サーバーがシャットダウン中かどうかを示す原子的なブール値。 |
disableKeepAlives |
keep-alivesを無効にする原子的なブール値。 |
nextProtoOnce |
setupHTTP2_* の初期化をガードするための同期オブジェクト。 |
nextProtoErr |
http2.ConfigureServer が使用された場合の結果を示すエラー。 |
mu |
同期用のミューテックス。 |
listeners |
サーバーのリスナーを管理するマップ。 |
activeConn |
サーバーのアクティブな接続を管理するマップ。 |
onShutdown |
シャットダウン時に実行する関数のスライス。 |
listenerGroup |
リスナーグループを管理する同期オブジェクト。 |
これらを踏まえた上でServer
構造体を用意し、Addr
とHandler
フィールドを設定してみます。とりあえず、Addrをローカルホストの8080番ポート、Handlerをnilで設定しました。
これで8080番ポートでTCPがリッスンされ、アクセスがあると何も返さない(厳密には404)サーバーを立てることができました。Handlerがnilの場合、デフォルトのHTTPマルチプレクサであるhttp.DefaultServeMuxが使用されます。
package main
import(
"net/http"
)
func main(){
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil,
}
server.ListenAndServe()
}
試しにgo run main.go
でサーバーを起動し、curl http://127.0.0.1:8080
と投げると404
が返却されました。
%curl http://127.0.0.1:8080
404 page not found
HTTPSでサーバーを立てる
次にサーバーをHTTPSで立てます。
といってもHTTPとそこまで大きく変わらず、SSL証明書と秘密鍵を用意し、ListenAndServeTLS
で渡すだけです。
以下がSSL証明書と秘密鍵の生成コードです。解説はこれだけで長くなるので、別途まとめたいと思います。
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"os"
"time"
)
func generateCert() {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalf("Failed to generate private key: %v", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour)
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
log.Fatalf("Failed to generate serial number: %v", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Example Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
}
certOut, err := os.Create("cert.pem")
if err != nil {
log.Fatalf("Failed to open cert.pem for writing: %v", err)
}
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
keyOut, err := os.Create("key.pem")
if err != nil {
log.Fatalf("Failed to open key.pem for writing: %v", err)
}
privBytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
log.Fatalf("Failed to marshal private key: %v", err)
}
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
keyOut.Close()
}
さて、ハンドラとハンドラ関数の話に戻ります
ここまで、HTTPとHTTPSでサーバーを立てる方法を見てきましたが、ただ404を返却するだけのサーバーで今のままでは何の役にも立ちません。そこで、次は8080番にリクエストが来たら定番のHello World!
を返すように実装して行きます。
リクエストの処理
リクエストを処理するためにはHandler
が重要です。先ほどはnilに設定していましたが、こちらを正しく設定することで正しく処理を行えるようになります。
以下がHandlerインターフェースの定義です。つまり、Go言語におけるハンドラとはServeHTTPメソッドを持ったインターフェースのことを指します。
別の言い方をすれば、ハンドラとは「HTTPリクエストを受け取って、それに対するHTTPレスポンスの内容をコネクションに書き込む」関数のことです。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
そして、このServeHTTPメソッドは以下の2つを引数にとります。
- ResponseWriterインターフェース
- Request構造体へのポインタ
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
これらを踏まえて、Handlerを作成します。
以下のように構造体を用意し、それにServeHTTP
メソッドを紐付けます。そして引数は上記のインターフェースと同様にw ResponseWriter, r *Request
とします。
あとは、main関数の中でMyHandler
構造体のインスタンスを作成し、Handler
に渡すことでServer構造体に設定できました。
package main
import (
"fmt"
"net/http"
)
type MyHandler struct{}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
fmt.Fprint(w, "Hello, World!")
}
func main(){
handler := MyHandler{}
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: &handler,
}
err := server.ListenAndServe()
if err != nil{
fmt.Printf("something went wrong %s", err)
}
}
ここで一旦サーバーを起動し、リクエストを投げてみます。
すると、無事Hello, World!
と返ってきました。
%curl http://127.0.0.1:8080
Hello, World!
なぜServeHTTPメソッドを紐付けた構造体を渡すと、そのメソッドが実行されるのか
これは余談ですが、気になったのでコードを追ってみました。
まず、server.ListenAndServe()
を実行するとsrv.Serve(ln)
が返却されます。このServeメソッドを追ってみるとgo c.serve(connCtx)
となっており、新しい接続が受け入れられるたびにこの部分で新しいゴルーチンが作成され接続の処理が開始されます。
さらに、このc.serve
のメソッドを追ってみると、serverHandler{c.server}.ServeHTTP(w, w.req)
という記述があり、中身は以下の通りとなっています。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
つまり、流れをまとめると
- server.ListenAndServe() が呼び出される。
- ListenAndServe() メソッド内で srv.Serve(ln) が呼び出される。
- Serve() メソッド内で、無限ループで新しい接続が受け入れられる。
- 新しい接続が受け入れられると、新しいゴルーチンが作成され conn.serve(connCtx) が実行される。
- conn.serve(connCtx) メソッド内で serverHandler{c.server}.ServeHTTP(w, req) が呼び出される。
- serverHandler.ServeHTTP(rw, req) メソッド内で、適切なハンドラが選択され handler.ServeHTTP(rw, req) が実行される。
これにより、登録されたハンドラの ServeHTTP メソッドが実行され、リクエストが処理されるようです。長いので簡潔にまとめると以下の通りです。
http.Server の Handler フィールドに渡した構造体の ServeHTTP メソッドが、指定された Addr にリクエストが来たときに実行される。
ついでにServeHTTPの引数をそれぞれ表でまとめてみました。
ReponseWriterの各メソッド
メソッド | 説明 |
---|---|
Header() Header |
レスポンスヘッダーマップを返す。レスポンスヘッダーを設定するために使用される。 |
Write([]byte) (int, error) |
レスポンスのボディ部分にデータを書き込む。バイト配列を受け取り、そのデータをクライアントに送信する。 |
WriteHeader(statusCode int) |
指定されたステータスコードでレスポンスのヘッダーを送信する。ステータスコードを設定するために使用される。 |
Request構造体の各フィールド
フィールド | 説明 |
---|---|
Method |
HTTPメソッド(GET, POST, PUTなど)を指定します。クライアントリクエストの場合、空文字列はGETを意味します。 |
URL |
要求されているURI(サーバーリクエストの場合)またはアクセスするURL(クライアントリクエストの場合)を指定します。 |
Proto |
サーバーリクエストのプロトコルバージョンを指定します。 |
ProtoMajor |
プロトコルのメジャーバージョン(例:1)。 |
ProtoMinor |
プロトコルのマイナーバージョン(例:0)。 |
Header |
リクエストヘッダーを含みます。 |
Body |
リクエストのボディ。クライアントリクエストの場合、nilはボディがないことを意味します。 |
GetBody |
クライアントリクエストでリダイレクトが必要な場合に、ボディの新しいコピーを返すための関数。 |
ContentLength |
関連するコンテンツの長さを記録します。-1は長さが不明であることを示します。 |
TransferEncoding |
外側から内側への転送エンコーディングのリストを示します。 |
Close |
応答後に接続を閉じるかどうかを示します。 |
Host |
サーバーリクエストの場合、URLが探されるホストを指定します。クライアントリクエストの場合、送信するHostヘッダーをオーバーライドします。 |
Form |
解析されたフォームデータ(URLフィールドのクエリパラメーターとPATCH, POST, PUTのフォームデータを含む)。 |
PostForm |
PATCH, POST, PUTのボディパラメーターから解析されたフォームデータ。 |
MultipartForm |
解析されたマルチパートフォーム(ファイルアップロードを含む)。 |
Trailer |
リクエストボディの後に送信される追加ヘッダーを指定します。 |
RemoteAddr |
リクエストを送信したネットワークアドレス。 |
RequestURI |
クライアントがサーバーに送信したリクエストターゲットの未修正のRequest-Line。 |
TLS |
リクエストが受信されたTLS接続に関する情報を記録します。 |
Cancel |
クライアントリクエストがキャンセルされるべきことを示すチャネル。 |
Response |
このリクエストが作成される原因となったリダイレクト応答。 |
ctx |
クライアントまたはサーバーのコンテキスト。 |
pat |
ServeMuxによってマッチされたリクエストのパターン。 |
matches |
パターンのワイルドカードに対応する値。 |
otherValues |
ワイルドカードと一致しないPathValueの設定に使用される値。 |
複数のハンドラを設定してみる
現段階では127.0.0.1:8080
にリクエストを投げるとHello, World
が返ってくるだけなのでこれまた使えません。したがって次にリクエスト処理を2つの方法で行い比較してみます。
複数のハンドラによるリクエストの処理
package main
import (
"fmt"
"net/http"
)
type HelloHandler struct{}
type WorldHandler struct{}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
fmt.Fprint(w, "Hello!")
}
func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
fmt.Fprint(w, "World!")
}
func main(){
hello := HelloHandler{}
world := WorldHandler{}
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.Handle("/hello", &hello)
http.Handle("/world", &world)
server.ListenAndServe()
}
サーバーを立ててそれぞれリクエストを投げるときちんと想定通りに返ってきています。
%curl http://127.0.0.1:8080/hello
Hello!
%curl http://127.0.0.1:8080/world
World!
ここで、以前と変更された点は以下の2点です。
- Server構造体のHandlerフィールドに何も渡していないこと
-
http.Handle
を使用していること
変更点の補足説明
「Server構造体のHandlerフィールドに何も渡していない」とは、つまりDefaultServerMux
をハンドラとして使用すると同義です。
ServeMux構造体は以下のようになっています。
type ServeMux struct {
mu sync.RWMutex
tree routingNode
index routingIndex
patterns []*pattern // TODO: remove if possible
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
そして、http.Handle
では、この構造体に渡されたパスやハンドラを登録します。
...
mux.tree.addPattern(pat, handler)
mux.index.addPattern(pat)
mux.patterns = append(mux.patterns, pat)
ハンドラ関数
続いてハンドラ関数を用いて上記のコードを書き換えてみたいと思います。
こちらの方がスッキリして書きやすいですね。
こちらでは、http.Handle
ではなくhttp.HandleFunc
を使用しています。
異なる点は自分でハンドラーを用意して、ServeHTTP
メソッドを用意しない点です。
http.HandlerFunc
の場合は引数を(w http.ResponseWriter, r *http.Request)とした関数を用意しそれとパスを渡す
だけで完了します。
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request){
fmt.Fprint(w, "Hello!")
}
func world(w http.ResponseWriter, r *http.Request){
fmt.Fprint(w, "World!")
}
func main(){
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/hello", hello)
http.HandleFunc("/world", world)
server.ListenAndServe()
}
HandleFunc
とHandle
メソッドはほとんどやっていることは同じです。
どちらもパスとハンドラをDefaultServerMux
に登録するのですが、異なる点は渡した関数をHandlerFunc型に変換し、その変換されたハンドラを登録する
という点です。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if use121 {
DefaultServeMux.mux121.handleFunc(pattern, handler)
} else {
DefaultServeMux.register(pattern, HandlerFunc(handler))
}
}
まとめると、以下の通り。ここで再掲だがハンドラとはServeHTTPメソッドを持つこと。
HandlerFunc
に型変換するとすでに用意されているため自動的にインターフェースを満たすこととなる。
メソッド | 概要 | 使用方法 | 補足 |
---|---|---|---|
HandleFunc |
パスと関数を登録する。ただし関数は ResponseWriter と *Request を引数に取る。 |
http.HandleFunc("/path", handlerFunc) |
渡した関数は HandlerFunc 型に変換され、自動的に「ServeHTTPメソッドを持つ」というインターフェースを満たす。 |
Handle |
パスとハンドラ(ServeHTTP メソッドを持つ構造体)を登録する。 |
http.Handle("/path", &handler) |
渡したハンドラはそのまま登録される。 |
まとめ
Go言語でサーバーやルーティングの管理方法など理解することができました。簡単なサーバーであれば本当に簡単に立てることができるんだなと驚きました。裏側で色々とやってくれているので、この辺りも今後理解を深めたいと思います。