1
2

【Go】サーバーを立ててみる

Last updated at Posted at 2024-07-08

今読んでいる技術書の内容で大事だと思った部分を少しずつまとめていきたいと思います。

今回読んでいる本はこちら
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()
}

そして、ListenAndServeServer構造体のメソッドとしても用意されています。
したがって、サーバー構造体でフィールドに任意の値を設定すれば、その設定でサーバーを起動できるようになります。

以下のコードを簡単に解説すると、

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 構造体のフィールドを表にまとめたものです。
大量にありますが、重要なのはAddrHandlerです。(どうでもいいですがAddrはAddressの略なんですね。)

フィールド名 説明
Addr サーバーがリッスンするTCPアドレスを指定します。形式は "host:port" です。空の場合、":http"(ポート80)が使用されます。
Handler HTTPリクエストを処理するハンドラ。nilの場合は http.DefaultServeMux が使用されます。
DisableGeneralOptionsHandler true の場合、"OPTIONS *" リクエストをハンドラに渡します。そうでない場合は200 OKとContent-Length: 0を返します。
TLSConfig ServeTLSListenAndServeTLS によって使用される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構造体を用意し、AddrHandlerフィールドを設定してみます。とりあえず、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証明書と秘密鍵の生成コードです。解説はこれだけで長くなるので、別途まとめたいと思います。

generate_cert.go
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)という記述があり、中身は以下の通りとなっています。

server.go
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()
}

HandleFuncHandleメソッドはほとんどやっていることは同じです。
どちらもパスとハンドラを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言語でサーバーやルーティングの管理方法など理解することができました。簡単なサーバーであれば本当に簡単に立てることができるんだなと驚きました。裏側で色々とやってくれているので、この辺りも今後理解を深めたいと思います。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2