はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Golang】net/httpライブラリの使い方メモ①

はじめに

今回はGoの標準ライブラリであるnet/httpの使い方を備忘録代わりに残しておきたいと思います。

Package http provides HTTP client and server implementations.

// DeepL翻訳
httpパッケージは、HTTPクライアントとサーバーの実装を提供します。


pkg.go.dev

簡単なWebサーバー作成

package main

import (
	"net/http"
)

func main() {
	// addr : ネットワークアドレス (""の場合はポート80)
	http.ListenAndServe("", nil)
}


設定を加えたWebサーバーを作る場合はServer構造体を利用すると良いでしょう。

package main

import (
	"net/http"
)

func main() {
	// Server構造体を生成する
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: nil,
	}

	// Serverを立てる
	server.ListenAndServe()
}

ハンドラ

ハンドラ(Handlerとは、ServeHTTPというメソッドを持ったインターフェイスのことです。

ServeHTTP(http.ResponseWriter, *http.Request)

複数のハンドラによってリクエストを処理する記述方法。

package main

import (
	"fmt"
	"net/http"
)

type HelloHandler struct{}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello!")
}

type WorldHandler struct{}

func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(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()
}

ハンドラ関数

ハンドラ関数はハンドラのように振る舞う関数のことです。ハンドラ関数はメソッドServeHTTPと同じシグネチャを持ちます。

package main

import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello!")
}

func world(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "World!")
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}

	http.HandleFunc("/hello", hello)
	http.HandleFunc("/world", world)

	server.ListenAndServe()
}

ServeMux

ServeMuxHTTPリクエストのマルチプレクサ(多重通信の入口となるもの)です。HTTPリクエストを受け取って、URLに応じて適切なハンドラに転送します。

HttpRouter

HttpRouter is a lightweight high performance HTTP request router (also called multiplexer or just mux for short) for Go.

In contrast to the default mux of Go's net/http package, this router supports variables in the routing pattern and matches against the request method. It also scales better.

// DeepL翻訳
HttpRouterは、Go用の軽量で高性能なHTTPリクエストルータ(マルチプレクサ、略してmuxとも呼ばれます)です。
Goのnet/httpパッケージのデフォルトのmuxとは対照的に、このルーターはルーティングパターンの変数をサポートし、リクエストメソッドに対してマッチします。また、スケールも良くなっています。

github.com

パターンマッチングで変数を利用した例。

package main

import (
	"fmt"
	"net/http"

	"github.com/julienschmidt/httprouter"
)

func hello(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	fmt.Fprintf(w, "Helllo, %s\n", p.ByName("name"))
}

func main() {
	mux := httprouter.New()
	mux.GET("/hello/:name", hello)

	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: mux,
	}
	server.ListenAndServe()
}

Request

構造体Requestは、サーバーが受信したHTTPリクエストまたはクライアントが送信するHTTPリクエストを表します。

A Request represents an HTTP request received by a server or to be sent by a client.

pkg.go.dev

構造体URL解析されたURLを表します。

A URL represents a parsed URL (technically, a URI reference).

pkg.go.dev

URLの一般形式は以下の通り。

scheme://[userinfo@]host/path[?query][#fragment]
scheme:opaque[?query][#fragment]

fragmentはサーバに送信される前にブラウザによって除去されます。

ヘッダー読み取りサンプル

package main

import (
	"fmt"
	"net/http"
)

func headers(w http.ResponseWriter, r *http.Request) {
	h := r.Header
	fmt.Fprintln(w, h)

	fmt.Fprintln(w, h["Accept-Encoding"])
	fmt.Fprintf(w, h.Get("Accept-Encoding"))
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/headers", headers)
	server.ListenAndServe()
}

ボディ読み取りサンプル

import (
	"fmt"
	"net/http"
)

func body(w http.ResponseWriter, r *http.Request) {
	len := r.ContentLength
	body := make([]byte, len)
	r.Body.Read(body)
	fmt.Fprintln(w, string(body))
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/body", body)
	server.ListenAndServe()
}

フォームデータ解析のサンプル

package main

import (
	"fmt"
	"net/http"
)

func process(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintln(w, r.Form)
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/process", process)
	server.ListenAndServe()
}

ParseFormでリクエストの解析をしてから、Formフィールドにアクセスする必要があります。

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>Go Web Programming</title>
    </head>
    <body>
        <form action=http://127.0.0.1:8080/process?hello=world&thread=123 method="post" enctype="application/x-www-form-urlencoded">
            <input type="text" name="hello" value="sau sheong"/>
            <input type="text" name="post" value="456"/>
            <input type="submit"/>
        </form>
    </body>
</html>
  • POSTリクエスト
  • URLはhttp://localhost:8080/process?hello=world&thread=123
  • HTMLフォームのキーと値のペアが2つ
動作させた様子
PostForm

フォームのキーバリューのペアが必要ですが、URLのペアは必要でない場合にはPostFormを利用します。

func process(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintln(w, r.Form)
	fmt.Fprintln(w, r.PostForm)
}
FromValue と PostFormValue

FromValuePostFormValueを用いると簡単にキーと値のペアにアクセスすることができます。これらを実行すると1番目の値だけが取得されます。ただしParseFormを呼び出す必要がないことに注意してください。

func process(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, r.FormValue("hello"))
	fmt.Fprintln(w, r.PostFormValue("hello"))
}
ファイル

multipart/form-data&FormFileメソッドを使った例。

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func process(w http.ResponseWriter, r *http.Request) {
	file, _, err := r.FormFile("uploaded")
	if err == nil {
		data, err := ioutil.ReadAll(file)
		if err == nil {
			fmt.Fprintln(w, string(data))
		}
	}
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/process", process)
	server.ListenAndServe()
}
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>Go Web Programming</title>
    </head>
    <body>
        <form action=http://127.0.0.1:8080/process?hello=world&thread=123 method="post" enctype="multipart/form-data">
            <input type="text" name="hello" value="sau sheong"/>
            <input type="text" name="post" value="456"/>
            <input type="file" name="uploaded">
            <input type="submit"/>
        </form>
    </body>
</html>
ResponseWriter

ResponseWriterハンドラがHTTPレスポンスを生成するために利用するインターフェイスです。

type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}

http package - net/http - Go Packages

Writeメソッドはバイト配列を受け取って、HTTPレスポンスのボディに書き込みを行います。

func writeExample(w http.ResponseWriter, r *http.Request) {
	str := `
		<html>
			<head><title>Go Web Programming</title></head>
			<body><h1>Hello, World!</h1></body>
		</html>
	`
	w.Write([]byte(str))
}

WriteHeaderはHTTPレスポンスの返すステータスコードを書き込みます。

func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(501)
	fmt.Fprintln(w, "Not Implemented")
}

Jsonをレスポンスとして返すようなサンプル。

type Post struct {
	User    string
	Threads []string
}

func jsonExample(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "applcation/json")
	post := &Post{
		User:    "Hanachiru",
		Threads: []string{"1番目", "2番目", "3番目"},
	}
	json, _ := json.Marshal(post)
	w.Write(json)
}

クッキー

クッキーはクライアントに保管される情報であり、サーバーからHTTPレスポンスメッセージを通して送られるものです。クライアントがHTTPリクエストをサーバーに送信するたびにクッキーも一緒に送られます。

Go言語にはCookie構造体が存在します。

type Cookie struct {
	Name  string
	Value string
	Path       string 
	Domain     string
	Expires    time.Time
	RawExpires string 
	MaxAge   int
	Secure   bool
	HttpOnly bool
	SameSite SameSite
	Raw      string
	Unparsed []string
}

- The Go Programming Language

  • セッションクッキー:Expiresが設定されていないもの。ブラウザが閉じられるとブラウザから削除される。
  • 永続性クッキー:セッションクッキー以外。期限切れや除去されない限り維持される。

クッキーを送信するサンプルはこちら。(ExpiresMaxAgeを設定していないのでセッションクッキー)

func setCookie(w http.ResponseWriter, r *http.Request) {
	c1 := http.Cookie{
		Name:     "first_cookie",
		Value:    "Go Web Programming",
		HttpOnly: true,
	}
	c2 := http.Cookie{
		Name:     "second_cookie",
		Value:    "Manning Publications Co",
		HttpOnly: true,
	}
	http.SetCookie(w, &c1)
	http.SetCookie(w, &c2)
}
Cookieを調べる

Chromeのディベロッパーツールのアプリケーション -> ストレージ -> Cookieの中をみると、クッキーの情報を知ることができます。

逆にクッキーを取得するサンプルコードはこちら。

func getCookie(w http.ResponseWriter, r *http.Request) {
	c1, err := r.Cookie("first_cookie")
	if err != nil {
		fmt.Fprintln(w, "Cannot get the first cookie")
	}
	cs := r.Cookies()
	fmt.Fprintln(w, c1)
	fmt.Fprintln(w, cs)
}

フラッシュメッセージ

ページを再表示したときいんはメッセージが残らないようにする「フラッシュメッセージ」をセッションクッキーを用いて実装したいと思います。

func setMessage(w http.ResponseWriter, r *http.Request) {
	msg := []byte("Hello, World!")
	c := http.Cookie{
		Name:  "flash",
		Value: base64.URLEncoding.EncodeToString(msg),
	}
	http.SetCookie(w, &c)
}

func showMessage(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("flash")
	if err != nil {
		if err == http.ErrNoCookie {
			fmt.Fprintln(w, "メッセージがありません")
		}
	} else {
		rc := http.Cookie{
			Name:    "flash",
			MaxAge:  -1,
			Expires: time.Unix(1, 0),
		}
		http.SetCookie(w, &rc)
		val, _ := base64.URLEncoding.DecodeString(c.Value)
		fmt.Fprintln(w, string(val))
	}
}

同じ名前のクッキーを設定すると、新しいものに置き換えられます。また負数のMaxAgeと過去の時点を表すExpiresにより、新しいクッキーが削除されます。