Open46

『Goプログラミング実践入門』をやっていく会

mohiramohira

Goプログラミング実践入門

お知らせ

読書仲間も募集中です!

「一緒に本を進めようぜ!」的な気持ちが湧いたら、https://twitter.com/mohirara まで連絡もらえると嬉しいです(DMでもメンションでもなんでも!)

公式情報

https://www.amazon.co.jp/dp/B06XKPNVWV
https://github.com/mushahiroyuki/gowebprog

写経リポジトリ

https://github.com/mohira/go-web-programming-book

mohiramohira

各章の雑まとめ、興味あるとこないところ

1章

  • 概観の話
  • 何かしらのWebプログラミングやったことあるなら、さらりと読み流すくらいでよさそう

第2章

  • 驚きの写経ビリティの低さ!
  • あとまわし

第3章

  • サーバー側の実装の入門的なやーつ
    • リクエストの処理とかはやらない(それは4章)
  • ポイントっぽいところ(ここ押さえれば無敵な気がする)
    • マルチプレクサを理解する
      • 他の言語などでRouterとかDispatcherと呼ばれるやつでよさげ
      • リクエストURLと何かしらのメソッド(ハンドラ)を紐付ける担当
    • 似たような名前の構造体やインタフェース、関数を区別する
    • ServeMuxを理解するとサードパーティ(gorilla/mux)もスイスイ使える

第4章

  • リクエストを受け取ったあとの話
  • HTTPのことがわかっていると早いと思う
    • HTTPのことは書籍にはそれほど書いてない
    • curlとかhttpコマンドが使えると便利(特にcurl)
  • net/httpパッケージの強さをちょっと体感できる
  • go docでひたすら調べていくといい感じ(この章に限らないけど)

第5章

  • テンプレートエンジン
    • あんまりテンション上がらないのであとまわし
    • HTMLにテンションが上がらないだけっぽい
  • コード生成にはかなり使えそう
  • text/templateとインタフェースが同じっっぽいので、やる価値は高いと思う(ただ、HTMLにテンションが上がらないw)

第6章 データの記憶

  • メモリで保持
  • テキストファイルで保持
  • バイナリファイルで保持
  • データベースで保持、そしてSQL
    • DBとSQLは8章(テスト)でも使う
  • GoでのSQL
  • GoのORM
    • これは時代が変わっているので、他のORMありそうな気がする
    • 質問&調査ポイント

第7章 XMLとJSON

  • XMLの利用
  • JSONの利用
    • JSONは8章(テスト)でも使う
    • そして、知りたいところ
    • embedとの違いが気になるところ

第8章 テスト

  • テスト。もっとも興味がある章の1つ。
  • Webプログラミング要素としては、HTTPハンドラらへん、DB操作、JSON操作でいけそうな雰囲気
    • 逆に言えば、それ以外のところ(テンプレートエンジンとかXMLとか)はSKIPでよさそう

第9章 ゴルーチン

  • いっちばん興味があるところ
  • 並行処理を理解する上で、Webプログラミングを題材にするのはめっちゃ有益だと思うから

第10章 デプロイ

  • 本がちょっと古め(日本語版は2017年)なので、現代との差異が気になるところ
  • とりあえず写経するかーという印象
mohiramohira

第3章 net/http

  • net/httpライブラリを使ってサーバーを起動する章
  • 次の4章では、リクエスト内容の処理が中心になるらしい

net/httpライブラリ内の構造体の棲み分け図解(p.60 図3-1)

mohiramohira

http.Server構造体を直接使うパターン

  • 別にhttp.ListenAndServe()使えばええやんって話だけど、まあ物は試しで。
$ go run chap02/3.1/server_minmal.go
$ curl localhost
404 page not found
package main

import "net/http"

// http.ListenAndServe() じゃなくて http.Server を直接使う場合
func main() {
	server := http.Server{Addr: "", Handler: nil}

	err := server.ListenAndServe()
	if err != nil {
		panic(err)
	}
}
mohiramohira

Q. リスト3.4の実験をしたくね? 3.2.2全然わからん問題

  • SSL証明書とかそのへんわかってない
  • cert.pemの使い方とかもよくわからん
  • ので、雑にスキップ

code

package main

import "net/http"

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

	// cert.pem: SSL証明書
	// key.pem: 秘密鍵
	err := server.ListenAndServeTLS("cert.pem", "key.pem")
	if err != nil {
		panic(err)
	}
}
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"net"
	"os"
	"time"
)

func main() {
	max := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, _ := rand.Int(rand.Reader, max)

	subject := pkix.Name{
		Organization:       []string{"Manning Publications Co."},
		OrganizationalUnit: []string{"Books"},
		CommonName:         "Go Web Programming",
	}

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject:      subject,
		NotBefore:    time.Now(),
		NotAfter:     time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
	}

	pk, _ := rsa.GenerateKey(rand.Reader, 2048)

	derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk)

	certOut, _ := os.Create("cert.pem")
	_ = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	_ = certOut.Close()

	keyOut, _ := os.Create("key.pem")
	_ = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
	_ = keyOut.Close()
}

http

$ curl http://127.0.0.1:8080
Client sent an HTTP request to an HTTPS server.
$ http -v http://127.0.0.1:8080
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: 127.0.0.1:8080
User-Agent: HTTPie/2.4.0



HTTP/1.0 400 Bad Request

Client sent an HTTP request to an HTTPS server.

とりあえずcurlhttpのエラーを見る

$ curl https://127.0.0.1:8080
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
$ curl -E ./cert.pem https://127.0.0.1:8080
curl: (58) unable to set private key file: './cert.pem' type PEM

ノリでたたく

$ http --cert ./cert.pem https://127.0.0.1:8080

http: error: SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8080): Max retries exceeded with url: / (Caused by SSLError(SSLError(9, '[SSL] PEM lib (_ssl.c:4048)'))) while doing a GET request to URL: https://127.0.0.1:8080/

$ http --cert-key ./cert.pem https://127.0.0.1:8080

http: error: SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8080): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1123)'))) while doing a GET request to URL: https://127.0.0.1:8080/

ガン無視接続で強引につなぐ

$ curl -k https://127.0.0.1:8080
404 page not found

$ curl --insecure https://127.0.0.1:8080
404 page not found
mohiramohira

パスのことを考慮しない感じになっているのおもろい

仕組みはこう。

URLのマッチングをしてないから、ぜーんぶ、MyHandlerに処理が流れるってだけ。

でも、1つのハンドラしか使えないのはあんまり役立たないよな。

package main

import (
	"fmt"
	"net/http"
)

type MyHandler struct{}

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

func main() {
	handler := MyHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,
	}
	err := server.ListenAndServe()
	if err != nil {
		panic(err)
	}
}
$ curl 127.0.0.1:8080
Hello World!
$ curl 127.0.0.1:8080/foo/bar
Hello World!
$ curl 127.0.0.1:8080/anything/at/all
Hello World!
mohiramohira

そういえば、http.DefaultServeMuxがちゃんと動くわけ

Handlernilのときは、http.DefaultServeMuxになるわけですが、http.DefaultServeMuxhttp.Hanlderインタフェースを満たしていたわけでした

package main

import "net/http"

func main() {
	_ = http.ListenAndServe("", nil) // nilなので http.DefaultServeMux になる
}
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}
mohiramohira

Q. ってか、「マルチプレクサ」ってなんだっけ?

なんだっけね?

mohiramohira

1つのハンドラじゃあ大したことできないから、マルチプレクサにする。ハンドラ登録しようぜシステムを採用する

もし1つのハンドラでいろんなことやろうとしたら、ハンドラが膨れてつらいよね。そりゃそうだ。
用途に応じて、ハンドラをわけたいよね。

ってわけで、HandlerはDefaultServeMuxにしといて、そのうえで、ハンドラを登録するスタイルにする。

mohiramohira

http.HandleHandlerを登録、http.HandleFuncはハンドラ関数を登録する

http.Handle

// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
package main

import (
	"fmt"
	"net/http"
)

type HelloHandler struct{}

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


func main() {
	hello := HelloHandler{}

	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: nil,
	}

	http.Handle("/hello", &hello)

	server.ListenAndServe()
}

http.HandleFunc

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
package main

import (
	"fmt"
	"net/http"
)

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

	http.HandleFunc("/hello", hello)

	err := server.ListenAndServe()
	if err != nil {
		panic(err)
	}
}

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

ハンドラを呼び出すごとに、そのハンドラを呼び出す

  • THE高階関数という感じ
  • めちゃくちゃやりそうな処理
  • http.HandlerFuncインタフェースを使うとスッキリ! (というかその存在を知っておくべきだね)

http.HandlerFunc を使わないと、こういう定義になって非常に見づらい

func log(h func(w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
	// ...
}

code

package main

import (
	"fmt"
	"net/http"
	"reflect"
	"runtime"
)

func hello(w http.ResponseWriter, r *http.Request) {
	_, _ = fmt.Fprint(w, "Hello!\n")
}

// 呼び出されたハンドラをロギングする
// http.HandlerFunc インタフェースの定義はコレ
// type HandlerFunc func(ResponseWriter, *Request)
func log(h http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// https://pkg.go.dev/runtime#FuncForPC
		name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
		fmt.Println("Handler function called - " + name)

		h(w, r)
	}
}

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

	http.HandleFunc("/hello", log(hello))
	_ = server.ListenAndServe()
}

runtime.FuncForPC

https://pkg.go.dev/runtime#FuncForPC

mohiramohira

高階関数連打

  • 関数も型なので、そのへんのなれが必要ですな
  • 無名関数を返すのではなく、http.HandlerFunc型で包む感じ
package main

import (
	"fmt"
	"net/http"
)

// HelloHandler は http.Handler インターフェイスを満たしている
type HelloHandler struct{}

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

// ハンドラの実行をロギングする
func log(h http.Handler) http.Handler {
	// http.HandlerFunc型で包む
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("Handler called - %T\n", h)
		h.ServeHTTP(w, r)
	})
}

// ハンドラを実行する前にユーザーの権限を確認する
func protect(h http.Handler) http.Handler {
	// http.HandlerFunc型で包む
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// ここで認証かなにかのコードを実行するとする
		h.ServeHTTP(w, r)
	})
}

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

	hello := HelloHandler{}
	http.Handle("/hello", protect(log(hello)))

	_ = server.ListenAndServe()
}
mohiramohira

やっぱ「マルチプレクサ」って何?

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

ふむ?

もともとあるHTTPの考え方の話? それをGoではマルチプレクサって表現している?

なんとなくRouterっぽい感じがするけど、どうなんだろう?

先人の知恵

https://qiita.com/huji0327/items/c85affaf5b9dbf84c11e

https://qiita.com/yNavS4Ki/items/5b7a0c7c41eb8da8f12a

https://simple-minds-think-alike.hatenablog.com/entry/golan-ggin-request-handle

mohiramohira

このへんの関数やデータ型やインタフェースを理解すれば見通しがかなり良くなりそう

  • http.Handler
  • http.HandlerFunc
  • http.HandleFunc
  • http.ServeMux
  • http.DefaultServeMux
  • http.ListenAndServe

名前も似ているしややこしや〜

mohiramohira

マルチプレクサ(multiplexer)は、ルーターっぽい感じ。あるいはディスパッチャ。

https://golang.org/pkg/net/http/#ServeMux

ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.
(オレオレ訳)
ServerMuxはHTTPリクエストのマルチプレクサですよ。
とんできた各リクエストURLと登録済みのURLパターンと照合するよ。
そんでもって、URLをもっとも近いパターンのハンドラーを召喚するよ。

mohiramohira

ServeMuxDefaultServeMuxの違いを抑えよう

ひとことでいえば?

// http/server.go

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
mohiramohira

今まで黙っていたけど、HandleFuncで特に何も指定してないのに動く理由

// こう書いてないのがちょっと不思議だった
http.HandleFunc(MyMux, "/hello", hello)

結局、HandleFuncを呼び出していただけ!
んで、そんときにDefaultServeMux.HandleFuncだからうまいこといくわけよ。

code

package main

import (
	"fmt"
	"net/http"
)

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

func main() {
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: nil, // nilなので、 http.DefaultServeMux ってこと
	}

	http.HandleFunc("/hello", hello)
	server.ListenAndServe()
}

そんで、http.HandleFunc()の中身を読むと...

// HandleFuncを実行したら、結局DefaultServerMuxのHandleFuncが呼ばれている

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

っていうかね、HandleFuncのDocにそのまま書いてあるよね!!!

HandleFunc registers the handler function for the given pattern in the DefaultServeMux.

mohiramohira

マルチプレクサの要件(≒Handlerとして要件)は、ServeHTTPを実装していればいいだけだから、いろんなマルチプレクサがあるよ

インタフェースを理解すれば、するりと使えて便利。

https://pkg.go.dev/github.com/julienschmidt/httprouter

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

やっぱルーターだった。そして、マルチプレクサでもある。で、マルチプレクサの略称がmuxらしいね。

code

package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	fmt.Fprintf(w, "hello, %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, // Handlerインタフェースは ServeHTTP 実装してりゃOK
	}

	server.ListenAndServe()
}

実行結果(予想通り)

$ curl localhost:8080/hello/Bob
hello, Bob

$ curl localhost:8080/hello/Tom
hello, Tom

$ curl localhost:8080/hello/
404 page not found
mohiramohira

マルチプレクサはgorilla/muxも有名らしい

https://pkg.go.dev/github.com/gorilla/mux

Package gorilla/mux implements a request router and dispatcher for matching incoming requests to their respective handler.

こっちはディスパッチャって言っているね。

$ go get github.com/gorilla/mux
package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"net/http"
)

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

func main() {
	router := mux.NewRouter()

	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: router,
	}

	router.HandleFunc("/hello", hello)
	server.ListenAndServe()
}
mohiramohira

ってか、標準のServeMuxを使ったパターンを先に抑えようねって感じだったわ

package main

import (
	"fmt"
	"net/http"
)

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

	myMux.HandleFunc("/hello", hello)
	server.ListenAndServe()
}

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

そもそも、Handlerインタフェース(ServeHTTP())を実装した構造体を直接渡すパターンもあるんだった

  • 3.6でやった話だけど、いま見ると全然違うな
    • というか、当時が全然わかってなかったという話
package main

import (
	"fmt"
	"net/http"
)

type MyHandler struct{}

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

func main() {
	myHandler := MyHandler{}

	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &myHandler,
	}

	server.ListenAndServe()
}
mohiramohira

HTTP2.0

package main

import (
	"fmt"
	"golang.org/x/net/http2"
	"net/http"
)

type MyHandler struct{}

func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!")
}

func main() {
	handler := MyHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,
	}
	http2.ConfigureServer(&server, &http2.Server{})
	server.ListenAndServeTLS("cert.pem", "key.pem")
}
$ curl -I --insecure https://localhost:8080
HTTP/2 200
content-type: text/plain; charset=utf-8
content-length: 12
date: Fri, 02 Jul 2021 14:05:52 GMT
mohiramohira

go docで調べると慣れが強化されそう

$ go doc net/http.HandleFunc
package http // import "net/http"

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
    HandleFunc registers the handler function for the given pattern in the
    DefaultServeMux. The documentation for ServeMux explains how patterns are
    matched.
$ go doc net/http.HandlerFunc
package http // import "net/http"

type HandlerFunc func(ResponseWriter, *Request)
    The HandlerFunc type is an adapter to allow the use of ordinary functions as
    HTTP handlers. If f is a function with the appropriate signature,
    HandlerFunc(f) is a Handler that calls f.

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
$ go doc net/http.Handle
package http // import "net/http"

func Handle(pattern string, handler Handler)
    Handle registers the handler for the given pattern in the DefaultServeMux.
    The documentation for ServeMux explains how patterns are matched.
mohiramohira

第4章

mohiramohira

リクエストヘッダを見よう

$ curl localhost:8080/headers -H "name:Bob" -H "age:25"
User-Agent [curl/7.64.1]
Accept [*/*]
Name [Bob]
Age [25]
$ http  localhost:8080/headers name:Bob age:25
HTTP/1.1 200 OK
Content-Length: 115
Content-Type: text/plain; charset=utf-8
Date: Sat, 03 Jul 2021 14:23:13 GMT

Connection [keep-alive]
Name [Bob]
Age [25]
User-Agent [HTTPie/2.4.0]
Accept-Encoding [gzip, deflate]
Accept [*/*]
package main

import (
	"fmt"
	"net/http"
)

func headers(w http.ResponseWriter, r *http.Request) {
	for k, v := range r.Header {
		fmt.Fprintf(w, "%s %s\n", k,v)
	}
}

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

	http.HandleFunc("/headers", headers)
	server.ListenAndServe()
}
mohiramohira

net/http.Request.Formnet/http.Request.PostForm

一言で、URLクエリパラメータもセットで取るかどうかだよ。

$ go doc net/http.request.Form
package http // import "net/http"

type Request struct {
    // Form contains the parsed form data, including both the URL field's query
    // parameters and the PATCH, POST, or PUT form data. This field is only
    // available after ParseForm is called. The HTTP client ignores Form and uses
    // Body instead.
    Form url.Values

    // ... other fields elided ...
}

Form contains the parsed form data, including both the URL field's query
parameters and the PATCH, POST, or PUT form data.

URLのクエリパラメータも取ってこれるよ〜。

This field is only available after ParseForm is called.

Request.ParseForm() を先に呼ばないとアカンぜよ。

The HTTP client ignores Form and uses Body instead.

$ go doc net/http.request.PostForm
package http // import "net/http"

type Request struct {
    // PostForm contains the parsed form data from PATCH, POST or PUT body
    // parameters.
    // 
    // This field is only available after ParseForm is called. The HTTP client
    // ignores PostForm and uses Body instead.
    PostForm url.Values

    // ... other fields elided ...
}

PostForm contains the parsed form data from PATCH, POST or PUT body
parameters.

PostFormFormと違って、URLのクエリパラメータは取らないよ〜。

This field is only available after ParseForm is called. The HTTP client

こっちは、同じやつ。

ignores PostForm and uses Body instead.

mohiramohira

Request.ParseFormしないとこうなるよ

Request.ParseFormの解説

$ go doc net/http.request.ParseForm
package http // import "net/http"

func (r *Request) ParseForm() error
    ParseForm populates r.Form and r.PostForm.

    For all requests, ParseForm parses the raw query from the URL and updates
    r.Form.

    For POST, PUT, and PATCH requests, it also reads the request body, parses it
    as a form and puts the results into both r.PostForm and r.Form. Request body
    parameters take precedence over URL query string values in r.Form.

    If the request Body's size has not already been limited by MaxBytesReader,
    the size is capped at 10MB.

    For other HTTP methods, or when the Content-Type is not
    application/x-www-form-urlencoded, the request Body is not read, and
    r.PostForm is initialized to a non-nil, empty value.

    ParseMultipartForm calls ParseForm automatically. ParseForm is idempotent.

ParseForm populates r.Form and r.PostForm.

r.Formr.PostFormにデータを流し込むよ。

For all requests, ParseForm parses the raw query from the URL and updates
r.Form.

すべてのリクエストに対して、URLクエリパラメータを解析して、r.Formを更新する(値をセットかな?)

For POST, PUT, and PATCH requests, it also reads the request body, parses it
as a form and puts the results into both r.PostForm and r.Form.

POST, PUT, PATCHリクエストの場合、リクエストボディも読むよ。
そんで、r.PostFormr.Formに値をセットする感じ。

Request body parameters take precedence over URL query string values in r.Form.

リクエストボディは、URLのクエリストリングよりも優先されてr.Formにセットされるよ。

要は、クエリパラメータと、リクエストボディで、キーがかぶった場合はリクエストボディを優先するってこと。

<!-- クエリパラメータにも name -->
<form action="http://127.0.0.1:8080/process?username=Bob" method="POST">
  <!-- POSTのBodyにも name -->
    <input type="text" name="username" value="Ken"/>
    <input type="submit"/>
</form>

If the request Body's size has not already been limited by MaxBytesReader,the size is capped at 10MB.

限界あるよ。へ〜。

For other HTTP methods, or when the Content-Type is not
application/x-www-form-urlencoded, the request Body is not read, and
r.PostForm is initialized to a non-nil, empty value.

他のHTTPメソッド(POST, PUT, PATCH以外)あるいは、Content-Typeapplication/x-www-form-urlencodedの場合、リクエストボディは読まれないよ。
そんで、r.Postformnilじゃない、空の値に初期化されるよ。

ParseMultipartForm calls ParseForm automatically. ParseForm is idempotent.

ParseMultipartFormは、ParseFormを自動的に呼ぶよ。で、ParseFormは冪等だよ。

mohiramohira

4.2.3のMultipartFormの実験はスキップした

コードを書くのだるかってん。

mohiramohira

Q. POSTリクエストのエンコード方式は一回調べるといいかも?

『Real World HTTP』の2.2あたりとか

mohiramohira

http.ResponseWriter.WriteHeaderの話

http.ResponseWriter.WriteHeaderは、そもそもhttp.ResponseWriterインタフェースのメソッドという話を思い出すところから

$ go doc net/http.ResponseWriter | grep -v '//'
type ResponseWriter interface {
	Header() Header

	Write([]byte) (int, error)

	WriteHeader(statusCode int)
}
    A ResponseWriter interface is used by an HTTP handler to construct an HTTP
    response.

    A ResponseWriter may not be used after the Handler.ServeHTTP method has
    returned.

WriteHeader sends an HTTP response header with the provided
status code.

レスポンスヘッダにステータスコードを与えるやーつ。

If WriteHeader is not called explicitly, the first call to Write
will trigger an implicit WriteHeader(http.StatusOK).

明示的にWriteHeaderが呼ばれない場合は、勝手にhttp.StatusOK(要は、200な)をしてくれる。これは、Write()が呼ばれたことをトリガーとして、暗黙的に呼ばれるわけな。

Thus explicit calls to WriteHeader are mainly used to
send error codes.

勝手に呼ばれるわけですが、逆に言うと、明示的に使うのはどういうとき? って話がありますな。それは「エラーコード」(というか、エラーを表現するステータスコード)を贈りたいとき。

The provided code must be a valid HTTP 1xx-5xx status code.

ってか、ステータスコードのvalidってどれよ?

これは、panic。

func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(123456789) //  http: panic serving 127.0.0.1:52373: invalid WriteHeader code 123456789
}

これも、panic。

func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(0) //  http: panic serving 127.0.0.1:52373: invalid WriteHeader code 0
}

でも、999はセーフ。

func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(999)
}

Only one header may be written. Go does not currently
support sending user-defined 1xx informational headers,
with the exception of 100-continue response header that the
Server sends automatically when the Request.Body is read.

なんやようわからん

mohiramohira

ところで、具象データ型は、net/http.response.Response

で、全然わからんけど、

func (w *response) WriteHeader(code int) {
	if w.conn.hijacked() {
		caller := relevantCaller()
		w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		return
	}

  // ここで、 superfluous のエラーメッセージがあるやーつ
	if w.wroteHeader {
		caller := relevantCaller()
		w.conn.server.logf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		return
	}
	checkWriteHeaderCode(code)
	w.wroteHeader = true
	w.status = code

	if w.calledHeader && w.cw.header == nil {
		w.cw.header = w.handlerHeader.Clone()
	}

	if cl := w.handlerHeader.get("Content-Length"); cl != "" {
		v, err := strconv.ParseInt(cl, 10, 64)
		if err == nil && v >= 0 {
			w.contentLength = v
		} else {
			w.conn.server.logf("http: invalid Content-Length of %q", cl)
			w.handlerHeader.Del("Content-Length")
		}
	}
}
$ go doc -u net/http.response|grep -v '//' |pbcopy

type Response struct {
	Header Header
	Body io.ReadCloser
	ContentLength int64
	TransferEncoding []string
	Close bool
	Uncompressed bool
	Trailer Header
	Request *Request
	TLS *tls.ConnectionState
}
    Response represents the response from an HTTP request.

    The Client and Transport return Responses from servers once the response
    headers have been received. The response body is streamed on demand as the
    Body field is read.

func (r *Response) Cookies() []*Cookie
func (r *Response) Location() (*url.URL, error)
func (r *Response) ProtoAtLeast(major, minor int) bool
func (r *Response) Write(w io.Writer) error
func (r *Response) bodyIsWritable() bool
func (r *Response) closeBody()
func (r *Response) isProtocolSwitch() bool
type response struct {
	conn             *conn
	reqBody          io.ReadCloser

	canWriteContinue atomicBool
	writeContinueMu  sync.Mutex

	cw chunkWriter

	handlerHeader Header

	closeAfterReply bool

	requestBodyLimitHit bool

	trailers []string


	dateBuf   [len(TimeFormat)]byte
	clenBuf   [10]byte
	statusBuf [3]byte

	closeNotifyCh  chan bool
}
    A response represents the server side of an HTTP response.

func (w *response) CloseNotify() <-chan bool
func (w *response) Flush()
func (w *response) Header() Header
func (w *response) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error)
func (w *response) ReadFrom(src io.Reader) (n int64, err error)
func (w *response) Write(data []byte) (n int, err error)
func (w *response) WriteHeader(code int)
func (w *response) WriteString(data string) (n int, err error)
func (w *response) bodyAllowed() bool
func (w *response) closedRequestBodyEarly() bool
func (w *response) declareTrailer(k string)
func (w *response) finalTrailers() Header
func (w *response) finishRequest()
func (w *response) needsSniff() bool
func (w *response) requestTooLarge()
func (w *response) sendExpectationFailed()
func (w *response) shouldReuseConnection() bool
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error)
mohiramohira

これだけでJSONサーバー書けるのめっちゃ楽なのでは!?

なんとなくnet/httpのすごさを感じた。これが世にいうnet/httpの素晴らしさなのか?

package main

import (
	"encoding/json"
	"log"
	"net/http"
)

type Post struct {
	User    string
	Threads []string
}

func jsonExample(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	post := &Post{
		User:    "Bob",
		Threads: []string{"1番目", "2番目", "3番目"},
	}

	jsonBytes, err := json.Marshal(post)
	if err != nil {
		log.Println(err)
	}

	_, _ = w.Write(jsonBytes)
}

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

	http.HandleFunc("/json", jsonExample)
	server.ListenAndServe()
}
mohiramohira

http.SetCookieの実験

$ go doc net/http.SetCookie
package http // import "net/http"

func SetCookie(w ResponseWriter, cookie *Cookie)
    SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
    The provided cookie must have a valid Name. Invalid cookies may be silently dropped.

The provided cookie must have a valid Name. Invalid cookies may be silently dropped

名前がないと、勝手に落ちるよ。まあ、「落ちる」っていうか、なかったことになるって感じ。

というわけで、試してみた。

package main

import (
	"net/http"
)

func setCookieHandler(w http.ResponseWriter, r *http.Request) {
	validCookie := http.Cookie{
		Name:     "first_cookie",
		Value:    "Go Web Programming",
		HttpOnly: true,
	}
	http.SetCookie(w, &validCookie)

	invalidCookie := http.Cookie{
		Value: "Nameがないと",
	}
	http.SetCookie(w, &invalidCookie)
}

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

	server.ListenAndServe()
}
// Set-Cookieヘッダは1つしかない!!!
$ curl -i localhost:8080/set_cookie
HTTP/1.1 200 OK
Set-Cookie: first_cookie="Go Web Programming"; HttpOnly
Date: Sun, 04 Jul 2021 14:36:43 GMT
Content-Length: 0

http.SetCookieの実装を見れば、その仕組もわかる

  • 空文字じゃなかったら(Stringerの実装はややこしかったのでスルーした)、Set-Cookieヘッダに書き込む
  • 文字列評価が空だったら、何もしない!!!
$ go doc -src  net/http.SetCookie
package http // import "net/http"

// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped.
func SetCookie(w ResponseWriter, cookie *Cookie) {
        if v := cookie.String(); v != "" {
                w.Header().Add("Set-Cookie", v)
        }
}
mohiramohira

クッキーの取得

素でCookieを取得する

  • 素直にヘッダーに書き込む
  • 文字列スライスなのでちょっと不便
func getCookieHandler(w http.ResponseWriter, r *http.Request) {
	h := r.Header["Cookie"]

	fmt.Fprintln(w, h)
	// 単なる文字列スライスなので操作には不便
	fmt.Fprintf(w,"%T\n",h ) // []string
}

http.Request.Cookieだと楽にとれるね

  • 実装もVery素直
$ go doc -src http.Request.Cookie
package http // import "net/http"

// Cookie returns the named cookie provided in the request or
// ErrNoCookie if not found.
// If multiple cookies match the given name, only one cookie will
// be returned.
func (r *Request) Cookie(name string) (*Cookie, error) {
        for _, c := range readCookies(r.Header, name) {
                return c, nil
        }
        return nil, ErrNoCookie
}
$ go doc -src http.ErrNoCookie      
package http // import "net/http"

// ErrNoCookie is returned by Request's Cookie method when a cookie is not found.
var ErrNoCookie = errors.New("http: named cookie not present")

http.Request.Cookiesだと、*Cookieのスライスとれるので、これも便利

$ go doc http.Request.Cookies
package http // import "net/http"

func (r *Request) Cookies() []*Cookie
    Cookies parses and returns the HTTP cookies sent with the request.
mohiramohira

未解決: http.SetCookieの前に、Fprintlnを実行するとクッキーが保存されないの何で?

なにかミスっているか?

package main

import (
	"fmt"
	"log"
	"net/http"
)

func nazoCookieHandler(w http.ResponseWriter, r *http.Request) {
	// 少なくとも fmt.Fprintln() するとCookieがセットされない
	// 他の原因もありそう
	fmt.Fprintln(w, "何かが原因でCookieがセットされない")

	c := http.Cookie{
		Name:  "name",
		Value: "Bob",
	}
	http.SetCookie(w, &c)
}

func kakuninCookieHandler(w http.ResponseWriter, r *http.Request) {
	c2, err := r.Cookie("name")
	if err != nil {
		fmt.Fprintln(w, err)
	} else {
		fmt.Fprintln(w, c2)
	}
}

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

	http.HandleFunc("/nazo", nazoCookieHandler)
	http.HandleFunc("/kakunin", kakuninCookieHandler)

	err := server.ListenAndServe()
	if err != nil {
		log.Println(err)
	}
}
mohiramohira

第5章

テンプレートエンジンはなんかテンション上がらないので、ひとまずスルー

mohiramohira

第6章

  • 6.2まで
    • メモリで管理
      • 構造体とかmapとか
    • ファイルで読み書き(io.Writerio.Readerという感じ)
    • encoding/csvの使い方
    • encoding/gob の使い方
mohiramohira

ioutil.Writefieじゃなくてos.Writefileを使おうな

$ go doc ioutil.Writefile
package ioutil // import "io/ioutil"

func WriteFile(filename string, data []byte, perm fs.FileMode) error
    WriteFile writes data to a file named by filename. If the file does not
    exist, WriteFile creates it with permissions perm (before umask); otherwise
    WriteFile truncates it before writing, without changing permissions.

    As of Go 1.16, this function simply calls os.WriteFile.
$ go doc os.Writefile    
package os // import "os"

func WriteFile(name string, data []byte, perm FileMode) error
    WriteFile writes data to the named file, creating it if necessary. If the
    file does not exist, WriteFile creates it with permissions perm (before
    umask); otherwise WriteFile truncates it before writing, without changing
    permissions.
mohiramohira

ioutil.Readfileじゃなくて、os.Readfileを使おうな

$ go doc ioutil.Readfile
package ioutil // import "io/ioutil"

func ReadFile(filename string) ([]byte, error)
    ReadFile reads the file named by filename and returns the contents. A
    successful call returns err == nil, not err == EOF. Because ReadFile reads
    the whole file, it does not treat an EOF from Read as an error to be
    reported.

    As of Go 1.16, this function simply calls os.ReadFile.
$ go doc os.Readfile
package os // import "os"

func ReadFile(name string) ([]byte, error)
    ReadFile reads the named file and returns the contents. A successful call
    returns err == nil, not err == EOF. Because ReadFile reads the whole file,
    it does not treat an EOF from Read as an error to be reported.
mohiramohira

bytes.Buffernew関数使わず、ゼロ値でよくない?

$ go doc bytes.Buffer
package bytes // import "bytes"

type Buffer struct {
        // Has unexported fields.
}
    A Buffer is a variable-sized buffer of bytes with Read and Write methods.
    The zero value for Buffer is an empty buffer ready to use.
mohiramohira

Kindle版のリスト6.6は、typoあるっぽい

  • 書籍はなおってた
// 誤: 引数がない!
posts, err := Posts()
posts, err := Posts(10)