- 投稿日:2020-03-01T22:39:08+09:00
golintのcliの実装を読んでみる
Goでのプログラミングに欠かせない静的解析ツールgolintのソースコードリーディングをしてみました
リポジトリ: https://github.com/golang/lint
(2020/3/1 時点)ディレクトリ構成
. ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── golint │ ├── golint.go │ ├── import.go │ └── importcomment.go ├── lint.go ├── lint_test.go ├── misc/ └── testdata/パッケージが、ルートディレクトリ直下のlintパッケージとgolint/以下のmainパッケージの2つに分かれています
lint.go
静的解析のロジックはここに書かれていますgolint/golint.go
cliツールの実装が書かれていますgolint/import.go
golint.goで使う補助関数が書かれていますgolint/importcomment.go
go バージョン 1.12 以上の場合に追加するコメントが記述されています
build tagの設定で1.12以上の時のみビルド対象になりますmisc/
vimやemacs等のエディター用のスクリプトが入っていますcli実装部分
golint/golint.goに
main()があります
main()を読むと流れがわかります
コマンドラインの引数やフラグの処理は、標準パッケージのflagを使って実装されていますmain()func main() { flag.Usage = usage flag.Parse() if flag.NArg() == 0 { lintDir(".") } else { // dirsRun, filesRun, and pkgsRun indicate whether golint is applied to // directory, file or package targets. The distinction affects which // checks are run. It is no valid to mix target types. var dirsRun, filesRun, pkgsRun int var args []string for _, arg := range flag.Args() { if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-len("/...")]) { dirsRun = 1 for _, dirname := range allPackagesInFS(arg) { args = append(args, dirname) } } else if isDir(arg) { dirsRun = 1 args = append(args, arg) } else if exists(arg) { filesRun = 1 args = append(args, arg) } else { pkgsRun = 1 args = append(args, arg) } } if dirsRun+filesRun+pkgsRun != 1 { usage() os.Exit(2) } switch { case dirsRun == 1: for _, dir := range args { lintDir(dir) } case filesRun == 1: lintFiles(args...) case pkgsRun == 1: for _, pkg := range importPaths(args) { lintPackage(pkg) } } } if *setExitStatus && suggestions > 0 { fmt.Fprintf(os.Stderr, "Found %d lint suggestions; failing.\n", suggestions) os.Exit(1) } }フラグ定義・解析
flag.Usageにhelpのコマンドライン出力関数を格納して、flag.Parseでコマンドラインの入力を定義されたフラグに解析しますflag.Usage = usage flag.Parse()解析するフラグと
usageはmain()の外で定義されています
フラグは-min_confidenceと-set_exit_status2種類のみで、コマンドラインの入力にオプションがあった場合、minConfidence、setExitStatusにポインタが格納されますvar ( minConfidence = flag.Float64("min_confidence", 0.8, "minimum confidence of a problem to print it") setExitStatus = flag.Bool("set_exit_status", false, "set exit status to 1 if any issues are found") suggestions int )
usageはgolintの使用方法をstderrで出力する関数です
flag.PrintDefaults()は定義されているフラグの説明を表示してくれますfunc usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\tgolint [flags] # runs on package in current directory\n") fmt.Fprintf(os.Stderr, "\tgolint [flags] [packages]\n") fmt.Fprintf(os.Stderr, "\tgolint [flags] [directories] # where a '/...' suffix includes all sub-directories\n") fmt.Fprintf(os.Stderr, "\tgolint [flags] [files] # all must belong to a single package\n") fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() }usage出力Usage of golint: golint [flags] # runs on package in current directory golint [flags] [packages] golint [flags] [directories] # where a '/...' suffix includes all sub-directories golint [flags] [files] # all must belong to a single package Flags: -min_confidence float minimum confidence of a problem to print it (default 0.8) -set_exit_status set exit status to 1 if any issues are foundコマンドライン引数解析
コマンドラインの引数が無い場合はカレントディレクトリのファイルをlint対象にします
引数の数をflag.NArg()でとって条件分岐させてますif flag.NArg() == 0 { lintDir(".") }引数がある場合、引数解析用の変数を定義します
golintはディレクトリ指定、ファイル指定、パッケージ指定の3つのモードがあります
dirsRun,filesRun,pkgsRunはモード指定のための変数です
argsはコマンドライン引数を入れるスライスですvar dirsRun, filesRun, pkgsRun int var args []string
flag.Args()で引数のリストをスライスで取得し、解析ループを回します
引数が以下の4つのケースのどれに当てはまるのかをみます
- 引数の末尾に
/...が付いている場合- 引数がディレクトリの場合
- 引数がファイルの場合
- 1,2,3どれにも当てはまらない場合
1,2の場合はディレクトリ指定モード、3の場合はファイル指定モード、4の場合はパッケージ指定モードに入ります
分岐先で、dirsRun || filesRun || pkgsRunに1を代入してます
解析した引数はargsにappendしますfor _, arg := range flag.Args() { if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-len("/...")]) { dirsRun = 1 for _, dirname := range allPackagesInFS(arg) { args = append(args, dirname) } } else if isDir(arg) { dirsRun = 1 args = append(args, arg) } else if exists(arg) { filesRun = 1 args = append(args, arg) } else { pkgsRun = 1 args = append(args, arg) } }ケース1の場合は
allPackagesInFS()を使って、引数のディレクトリパス内にあるパッケージディレクトリを全てとってきます
allPackagesInFS()はgolint/import.goに実装されています
matchPackagesInFS()をラップしてますねimport.gofunc allPackagesInFS(pattern string) []string { pkgs := matchPackagesInFS(pattern) if len(pkgs) == 0 { fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) } return pkgs } func matchPackagesInFS(pattern string) []string { // Find directory to begin the scan. // Could be smarter but this one optimization // is enough for now, since ... is usually at the // end of a path. i := strings.Index(pattern, "...") dir, _ := path.Split(pattern[:i]) // pattern begins with ./ or ../. // path.Clean will discard the ./ but not the ../. // We need to preserve the ./ for pattern matching // and in the returned import paths. prefix := "" if strings.HasPrefix(pattern, "./") { prefix = "./" } match := matchPattern(pattern) var pkgs []string filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { if err != nil || !fi.IsDir() { return nil } if path == dir { // filepath.Walk starts at dir and recurses. For the recursive case, // the path is the result of filepath.Join, which calls filepath.Clean. // The initial case is not Cleaned, though, so we do this explicitly. // // This converts a path like "./io/" to "io". Without this step, running // "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io // package, because prepending the prefix "./" to the unclean path would // result in "././io", and match("././io") returns false. path = filepath.Clean(path) } // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". _, elem := filepath.Split(path) dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { return filepath.SkipDir } name := prefix + filepath.ToSlash(path) if !match(name) { return nil } if _, err = build.ImportDir(path, 0); err != nil { if _, noGo := err.(*build.NoGoError); !noGo { log.Print(err) } return nil } pkgs = append(pkgs, name) return nil }) return pkgs }まず
"..."を取り除き、i := strings.Index(pattern, "...") dir, _ := path.Split(pattern[:i])
"./"先頭にが付いている場合、prefixとして保存しますprefix := "" if strings.HasPrefix(pattern, "./") { prefix = "./" }
matchPattern()で文字列が一致するか判定するfuncを生成します
正規表現で一致判定する関数を返してますねfunc matchPattern(pattern string) func(name string) bool { re := regexp.QuoteMeta(pattern) re = strings.Replace(re, `\.\.\.`, `.*`, -1) // Special case: foo/... matches foo too. if strings.HasSuffix(re, `/.*`) { re = re[:len(re)-len(`/.*`)] + `(/.*)?` } reg := regexp.MustCompile(`^` + re + `$`) return func(name string) bool { return reg.MatchString(name) } }カレントディレクトリのパス
dirと判別関数matchを使って、解析対象ディレクトリのリストアップをします
ファイルを探す処理は、filepath.Walk()を使うことで簡単に実装できます
filepath.Walk()は、func(path string, fi os.FileInfo, err error) errorの形式の関数をファイルごとに実行させることが出来ますディレクトリ以外を除外
↓
dirとpathが同じ場合、pathを綺麗に
↓
.xxx、_xxxおよびtestdataを除外
↓
パッケージとしてimportできるか確認
↓
一致判定といった一連の流れを関数にして渡しています
// ディレクトリ以外を除外 if err != nil || !fi.IsDir() { return nil } // `dir`とpathが同じ場合、pathを綺麗に if path == dir { path = filepath.Clean(path) } // `.xxx`、`_xxx`および`testdata`を除外 _, elem := filepath.Split(path) dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { return filepath.SkipDir } // 一致判定 name := prefix + filepath.ToSlash(path) if !match(name) { return nil } // パッケージとしてimportできるか確認 if _, err = build.ImportDir(path, 0); err != nil { if _, noGo := err.(*build.NoGoError); !noGo { log.Print(err) } return nil } pkgs = append(pkgs, name)
pkgsがmain()のargsに入ります解析モード確認
main()に戻ります
コマンドライン引数にディレクトリとファイルが混在している場合はエラーにしますif dirsRun+filesRun+pkgsRun != 1 { usage() os.Exit(2) }静的解析
argsを静的解析関数に渡します
ディレクトリ指定、ファイル指定、パッケージ指定それぞれ解析関数がありますswitch { case dirsRun == 1: for _, dir := range args { lintDir(dir) } case filesRun == 1: lintFiles(args...) case pkgsRun == 1: for _, pkg := range importPaths(args) { lintPackage(pkg) } }解析結果の出力は解析関数内で行います
まとめ
思ってたよりコードが少なくて驚きました
flagやpathなど、標準パッケージが優秀なのは有り難いですね
次はcliツール自作をやってみたいですQiita書きながらのコードリーディングはきっちり理解しようとする気概が出てくるので良いなと思いました
解析部分の記事は気が向いたら書きます参考
https://mattn.kaoriya.net/software/lang/go/20171024130616.htm
- 投稿日:2020-03-01T16:56:13+09:00
データ分析基盤構築入門 サンプルアプリエラー
データ分析基盤構築入門のサンプルアプリケーションを、Dockerで立ち上げる際にエラーが出て、うまく立ち上がらなかったので、色々ソリューションを試しました。
その中で、2020/3/1現在のソリューションを書きます。
エラー内容
$ docker-compose up --builddockerでelasticsearch fluentd goアプリケーションを立ち上げると、fluentdコンテナの立ち上げでエラーが出ます。
ソリューション
1.fluentdのtd-agent-gemのバージョンをあげる。
blog-sample/Dockerfile-fluentdの中身を以下のように変更します。FROM debian:jessie ENV DEBIAN_FRONTEND=noninteractive RUN apt-get -qq update && apt-get install --no-install-recommends -y curl ca-certificates sudo build-essential libcurl4-gnutls-dev RUN curl -L https://toolbelt.treasuredata.com/sh/install-debian-jessie-td-agent3.sh | sh RUN /usr/sbin/td-agent-gem install fluent-plugin-elasticsearch fluent-plugin-record-reformer EXPOSE 24224 CMD exec td-agent -c /fluentd/etc/$FLUENTD_CONF -p /fluentd/plugins $FLUENTD_OPT具体的には、4行目でインストールしているtd-agent-gemのバージョンを2 -> 3に変更します。
参考: td-agent2からtd-agent3へバージョンアップしました - Qiita
2. 1の対応をすると、全てのコンテナが立ち上がった。localhost:80にアクセスすると、マイグレーションされていないので、エラーが出る。
no such table: articlesgoアプリケーションコンテナに入ってあげてマイグレーション実行すると、さらにエラーが出ます。
go get github.com/rubenv/sql-migrate/... package math/bits: unrecognized import path "math/bits" (import path does not begin with hostname) Makefile:13: recipe for target '/go/bin/sql-migrate' failed make: *** [/go/bin/sql-migrate] Error 13.goのversionをあげる
1.8 -> 1.10に
go - Cannot find package math/bits - Stack Overflow
4.マイグレーションを実行すると、マイグレーション実行される
再びgoアプリケーションコンテナに入ってあげて、マイグレーションを実行してあげると、マイグレーションが正常に実行される。
go get github.com/rubenv/sql-migrate/... /go/bin/sql-migrate up -env=development Applied 2 migrations
- 投稿日:2020-03-01T15:55:13+09:00
Kotlin vs Go 処理時間を比較してみる
1. はじめ
最近はどこもかしこもGoばっかり!
SE(笑)で化石のようなJavaを扱った経験を持つ人間としてはKotlinの方がしっくりくるのですが、なんだか肩身が狭いです。そこで今回はGoとKotlinで同じようなコードを書いてみて処理時間という点でどれだけ差があるか実験してみました。
2. 内容
・Kotlinはコンパイラでjarにした後、Amazon Corretto8のランタイムで動かす
・Goは公式でリリースされてる最新のものを使用(Go 1.14)1回目
・10万の要素を持つ配列に乱数で値をひたすら入れてみる
・とりあえずこれだけmain.ktimport kotlin.random.Random fun main(args: Array<String>) { val startTime = System.currentTimeMillis() var listData: Array<Int> = Array(100000){it} for (i in listData) { listData[i] = Random.nextInt(100000) + 100 } /* listData.forEach { println("it:" + it) } */ val endTime = System.currentTimeMillis() println("開始時刻:" + startTime + " ms") println("終了時刻:" + endTime + " ms") println("処理時間:" + (endTime - startTime) + " ms") println("listData要素数:" + listData.size) }main.gopackage main import ( "fmt" "time" "math/rand" ) func main() { startTime := time.Now() listData := [100000] int{} for index := range listData { listData[index] = rand.Intn(100000) + 100 } /* for _, value := range listData { fmt.Printf("it:%d \n" , value) } */ endTime := time.Now() fmt.Printf("開始時刻:%v \n" , startTime) fmt.Printf("終了時刻:%v \n" , endTime) fmt.Printf("処理時間:%v \n", (endTime.Sub(startTime))) fmt.Printf("listData要素数:%d \n" , len(listData)) }【結果】
2回目
・consoleに配列の内容を全出力というものを足すとどうだ??
.kt、.goともに前述のコードのコメント部分復活//.kt listData.forEach { println("it:" + it) } //.go for _, value := range listData { fmt.Printf("it:%d \n" , value) }【結果】
おわり
上記の実験ではどちらもGoが圧勝する内容でした。
単純なロジックだと勝負にならないくらいの差ですね。
- 投稿日:2020-03-01T15:25:33+09:00
Open状態のファイルに対して、Linuxだとos.Renameに成功するが、Windowsだとエラーになる
環境情報
この記事は以下の環境で稼働確認を実施しました。
- Windows
- Microsoft Windows [Version 10.0.19041.84]
- go version go1.13.5 windows/amd64
- Linux
- Ubuntu 18.04 LTS (Bionic Beaver)
- go version go1.13.8 linux/amd64
事象
Open状態のファイルに対して、
os.Renameを呼び出すと、LinuxとWindowsでは結果が異なります。具体的にはLinuxではos.Renameに成功しますが、Windowsではエラーになります。以下はOpen状態のファイルに対して、
os.Renameを利用するコマンドです。main.gopackage main import "os" func main() { fp, err := os.Open("old.txt") if err != nil { panic(err) } defer fp.Close() err = os.Rename("old.txt", "new.txt") if err != nil { panic(err) } }これをLinux(Ubuntu)環境でこのコードを実行すると、
os.Renameが想定通り動作することが分かります。$ ls -ltr total 0 -rwxrwxrwx 1 developer developer 214 Feb 23 22:01 main.go -rwxrwxrwx 1 developer developer 0 Feb 23 22:05 old.txt $ go run main.go $ ls -ltr total 0 -rwxrwxrwx 1 developer developer 214 Feb 23 22:01 main.go -rwxrwxrwx 1 developer developer 0 Feb 23 22:05 new.txt一方、Windows環境でこのプログラムを実行すると、panicが発生しました。
C:\qiita>go run main.go panic: rename old.txt new.txt: The process cannot access the file because it is being used by another process. goroutine 1 [running]: main.main() C:/qiita/main.go:14 +0x131 exit status 2原因
Linux
Linuxの場合、
os.Renameの実体はrenameatになります。これは以下のようにstraceを利用すすことで確かめることができます。$ go build -o main $ strace -o strace.log ./main
renameatは相対パスでファイル名の変更を行うAPIで、そのmanページを見ると以下の通りに記述されており、Open状態のファイルでも名称変更ができることがわかります。_Open file descriptors for oldpath are also unaffected.
Windows
「Goならわかるシステムプログラミング 第10回 - ファイルシステムと、その上のGo言語の関数たち(1)」 によると、Windowsでの
os.Renameの実体はMoveFileExというWin32 apiとのことです。MoveFileExのドキュメントを見たところ、このAPIは対象ファイルの削除権限を要するとのことでした。To delete or rename a file, you must have either delete permission on the file or delete child permission in the parent directory.
ここでGo言語本体のソースコードを見たところ、ファイルのオープンには
CreateFileというWin32 APIを利用しているようです。CreateFileは開いたファイルの共有状態を指定するのですが、Go言語ではFILE_SHARE_DELETEを指定していません (該当のソースコードはこのあたり)つまり削除権限なしの状態でオープンされているファイルに対して、削除権限を要するリネーム処理を行っているわけで、エラーになるのは当然といえます。
対策
作成したプログラムがさまざまなプラットフォーム・OSで利用されることが分かっている場合は、
os.Renameの前には必ずファイルを閉じておくことが大事です。余談1
CreateFileを呼び出す際にFILE_SHARE_DELETEを付与して、WindowsでもLinuxと同じ挙動になるようにしてほしいという要望は実際あるようです(該当のIssueはこれ)。ただこの記事を読むと、単にFILE_SHARE_DELETEを付与すればよいだけでもないらしく、なかなか難しい問題をはらんでいることがが分かります。余談2
とあるライブラリを利用しているときに、ドキュメントの記述と実際の挙動が一致しなかったことが、今回の問題に行き着いた個人的なきっかけになります。ちなみにそのライブラリのコードは次のようになっていました。
fp, err := os.Open("old.txt") DoSomething(fp) os.Rename("old.txt", "new.txt")Windowsでは
os.Renameがエラーを返すのですが、そのエラーをハンドリングせずに捨てており、結果として奇妙な動作になっていたのでした。APIが返してくるエラーを無視しないというのも大事な教訓ですね。
- 投稿日:2020-03-01T12:26:52+09:00
[Golang]slice内で特定の要素があるか調べる、contains関数を作ってみた(解説付)
はじめに
Golangの標準パッケージで、PHPやJavaScriptでいうところの
contain関数のようなものがなかったので、作ってみました。
気軽にコピペして使って頂けると嬉しいです。今回のコード
以下、今回共有したい本題です。
コピペで使えるコード全文
package main import ( "fmt" "reflect" ) func main(){ list := []uint64{1, 2, 3} // ここに対象のスライスを入れる target := uint64(3) // ここに対象の要素を入れる result, err := contains(target, list) fmt.Println(result, err) // -> true <nil> } func contains(target interface{}, list interface{}) (bool, error) { switch list.(type) { default: return false, fmt.Errorf("%v is an unsupported type", reflect.TypeOf(list)) case []int: revert := list.([]int) for _, r := range revert { if target == r { return true, nil } } return false, nil case []uint64: revert := list.([]uint64) for _, r := range revert { if target == r { return true, nil } } return false, nil case []string: revert := list.([]string) for _, r := range revert { if target == r { return true, nil } } return false, nil } return false, fmt.Errorf("processing failed") }動作確認はこちらでも行えます。
関数部分
func contains(target interface{}, list interface{}) (bool, error) { switch list.(type) { default: return false, fmt.Errorf("%v is an unsupported type", reflect.TypeOf(list)) case []int: revert := list.([]int) for _, r := range revert { if target == r { return true, nil } } return false, nil case []uint64: revert := list.([]uint64) for _, r := range revert { if target == r { return true, nil } } return false, nil case []string: revert := list.([]string) for _, r := range revert { if target == r { return true, nil } } return false, nil } return false, fmt.Errorf("processing failed") }関数の使い方
引数
- 第一引数には、検索したい要素を渡して下さい。
- 第二引数には、対象のsliceを渡して下さい。
- sliseで渡せる型は、
[]int、[]uint64、[]stringに対応しています。
- 他の型は、
case文の中で上記の型と同じパターンで追加してもらえると対応できるかと思います。返り値
- 第一返り値は、boolです。
- 対象のsliceに対して、検索したい要素が存在すればtrue、なければfalseが返ります。
- 第二返り値は、errorです。
- エラーメッセージ:
hoge is an unsupported type
- 渡されたsliseの型が対応していないことを表します。
- エラーメッセージ:
processing failed
- それ以外の予期しない理由で、処理に失敗したことを表します。
他の型も使いたい場合
- 上記の通り、関数内のcase文で使いたい型のパターンを追記してもらえると対応できるかと思います。
関数のざっくり解説
- 第二引数のsliceを、型判別する
[]int、[]uint64、[]stringであれば、型アサーションでinterface型からそれぞれの型に戻します。
- それ以外の型であれば、「その型に対応してません」といった旨のエラーを出します。
- 第一引数のtargetと、型アサーションしたsliseをforで回しながら、1つずつ比較します。
- 同じ要素があれば、その時点でtrueを返します。
さいごに
最後まで読んで頂き、ありがとうございました。
「もっとこうした方が良いよ」等のツッコミがあれば、コメント欄でお待ちしております!参考サイト
- 投稿日:2020-03-01T08:45:44+09:00
Goのnet/httpの実装をちょっと読んでみる
はじめに
Goは標準パッケージとしてHTTPサーバが組み込まれており、
net/httpパッケージを用いると簡単にHTTPサーバを動かすことができます。今回はnet/httpパッケージの一部(HTTPサーバの内容)の実装を読むことで、HTTPサーバが動く裏側をざっと見てみたいと思います。困ったら 公式ドキュメント を見ましょう。なお、読んでいるコード Go1.13 のものです。
Doc
まずざっと公式ドキュメントに書いてあるサンプルとドキュメントを眺めて、仕様をおさらいしておきます。
type HandlerハンドラはHTTPリクエストに対してレスポンスを返します。
ServeHTTPはレスポンスヘッダーとデータをResponseWriterに書き込んでからreturnする必要があります。リクエストが終了したことを示すシグナルを返します。ServeHTTP呼び出しの完了後または完了と同時にResponseWriterを使用したり、Request.Bodyを読み取ることは無効です。
HTTPクライアントのソフトウェア、HTTPプロトコルバージョン、およびクライアントとGoで実装されたサーバー間のミドルウェアによっては、ResponseWriterに書き込んだ後にRequest.Bodyから読み取ることができない場合があります。 丁寧にハンドラを扱う場合は、最初にRequest.Bodyを読み取り、次にレスポンスを返す必要があります。
リクエストボディを読み取る場合を除き、ハンドラは提供されたリクエストを変更しないでください。
ServeHTTPがパニックになった場合、サーバー(ServeHTTPの呼び出し元)は、パニックの影響がアクティブなリクエストとは無関係であると仮定します。パニックを回復し、サーバーのエラーログにスタックトレースを記録し、HTTPプロトコルに応じてネットワーク接続を閉じるか、HTTP/2 RST_STREAMを送信します。ハンドラを中断して、クライアントには中断されたレスポンスが表示されますが、サーバーがエラーを記録しないようにするには、ErrAbortHandlerを用いてパニックを起こします。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ServeHTTP(ResponseWriter, *Request)を実装していればHandlerになることができます。
type HandlerFunctype HandlerFunc func(ResponseWriter, *Request)HandlerFunc 型は通常の関数を HTTP ハンドラとして扱えるようにするためのアダプターです。fが適切なシグネチャを持っている関数であれば、HanderFunc(f) はハンドラと振る舞うことができ、関数fを呼び出します。
func (HandlerFunc) ServeHTTPfunc (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }ServeHTTPはf(w, r)を呼び出します。
HandlerFuncはServeHTTPのように自身の関数を呼び出すことでHandlerを満たしています。
type ServeMux
ServeMuxは HTTP リクエストの URL と対応するハンドラを保持するマルチプレクサです。HTTP リクエストが来たときに、対応するハンドラを呼び出します。以下は GoDoc の翻訳です。デフォルトだと
DefaultServeMuxという変数がServeMux型として定義されていて、このマルチプレクサがListenAndServeが呼ばれたときに用いられます。ServeMuxはHTTPリクエストのマルチプレクサです。要求されたリクエストのURLと登録されているURLのパターンのリストと比較し、URLに最も一致するパターンのハンドラを呼び出します。
パターン名は「/favicon.ico」といったルート化されたパスや「/images/」(末尾のスラッシュに注意)といったルート化されたサブツリーとして指定されます。
パターンは短いパターンよりも長いパターンが優先されます。そのため「/images/」と「/images/thumbnails/」というパターンがハンドラとして登録されている場合、「/images/thumbnails/」から始まるリクエストパスに対しては、後者のハンドラが呼び出されます。前者は「/images/」というサブツリーに含まれる任意のパスに対して、ハンドラが呼び出されます。スラッシュで終わるパターンはルートのサブツリーとして定まるため、「/」というパターンは単に「/」というURLのパスにマッチするだけでなく、他の登録されていないパターンとマッチしないすべてのパスとマッチします。
サブツリーが登録されており、末尾がスラッシュなしでサブツリーのルートを指定するリクエストを受信した場合、ServeMuxはそのリクエストをサブツリーのルートにリダイレクトします(末尾のスラッシュを追加します)。この動作は、末尾にスラッシュなしのパスを登録することで上書きできます。たとえば、「/images/」を登録すると、「/images」が別に登録されていない限り、ServeMuxは「/images」に対するリクエストを「/images/」にリダイレクトします。
パターンはオプションでホスト名で始まり、そのホスト上のURLのみに一致するように制限できます。ホスト名が指定されているパターンは一般的なパターンよりも優先されるため、ハンドラは「 http://www.google.com/ 」のリクエストを引き継ぐことなく、2つのパターン「/codesearch」と「codesearch.google.com/」を登録できます。
ServeMuxはURLリクエストパスやホストヘッダーのサニタイズ、ポート番号の除去、
.や..要素や繰り返されるスラッシュを含むリクエストのリダイレクト、URLの正規化も行います。Examples
func ListenAndServefunc ListenAndServe(addr string, handler Handler) errorHTTP サーバを起動する関数です。以下は GoDoc の翻訳です。
ListenAndServeは、TCPネットワークアドレス
addrをListenし、handlerでServeを呼び出して、リクエストされたコネクション要求を処理します。受け付けられたコネクションは、TCPキープアライブを有効にするように構成されています。ハンドラの引数は通常
nilです。この場合、DefaultServeMuxが使用されます。
ListenAndServeは常に、nil以外のエラーを返します。
ListenAndServeを使う実装例package main import ( "io" "log" "net/http" ) func main() { // Hello world, the web server helloHandler := func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "Hello, world!\n") } http.HandleFunc("/hello", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
func Handlefunc Handle(pattern string, handler Handler)
Handleは、指定されたパターンとハンドラの対応をDefaultServeMuxに登録します。ServeMuxのドキュメントでは、パスに対応するパターンの選択方法について記載されています。package main import ( "fmt" "log" "net/http" "sync" ) type countHandler struct { mu sync.Mutex // guards n n int } func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mu.Lock() defer h.mu.Unlock() h.n++ fmt.Fprintf(w, "count is %d\n", h.n) } func main() { http.Handle("/count", new(countHandler)) log.Fatal(http.ListenAndServe(":8080", nil)) }
func HandleFunc
func HandleFuncはfunc Handleのラッパーです。func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFuncは、指定されたパターンのハンドラをDefaultServeMuxに登録します。ServeMuxのドキュメントでは、パスに対応するパターンの選択方法について記載されています。package main import ( "io" "log" "net/http" ) func main() { h1 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") } h2 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") } http.HandleFunc("/", h1) http.HandleFunc("/endpoint", h2) log.Fatal(http.ListenAndServe(":8080", nil)) }見通しを良くするために
同じ名前でも、型の名前なのか、関数の名前なのか、型に紐づくメソッド名なのかに注意する必要があります。また
Handleなのか、Handlerなのか、注意が必要です。上記の GoDoc の説明ではすでに登場していますが、HTTP サーバを利用するユーザとしては以下の 3 つの型を中心に考えると、構成が見やすくなりそうです。
type ServeMux
- URLと対応するハンドラを保持するマルチプレクサ。HTTPリクエストが来たときに、対応するハンドラを呼び出します。
- デフォルトで
DefaultServeMuxという変数が定義されていて、これを利用します。NewServeMux()でデフォルト以外のマルチプレクサを生成できます。
type Handler
- ハンドラです。
- 直感的には、リクエストに対してレスポンスを返す関数、と捉えて良いと思います。
- マルチプレクサにハンドラを登録します。
ServeHTTPメソッドを満たす型(func(ResponseWriter, *Request)型)がハンドラになることができます。
type HandlerFunc
type HandlerFunc func(ResponseWriter, *Request)- パスに対してハンドラを紐付ける 型 です。
type Handlerとtype HandlerFuncの関係性というか、なぜtype HandlerFuncが存在するか、という背景の理解は ruiu さんの記事が参考になりました。もう一つ例をあげてみよう。net/httpのHTTPハンドラのインターフェイスは次のように定義されている。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }つまりnet/httpのHTTPハンドラはどんな型でもいいのだけど、その型はServeHTTPという名前のメソッドを持っていなければいけなくて、第一引数がResponseWriter(HTTPレスポンスの出力先)、第二引数が*Request(HTTPリクエスト)ということになっている。この型の値をhttp.Handleに渡すだけであとはライブラリが勝手にこのメソッドを呼んでHTTPリクエストをハンドルしてくれることになっている。
では自分でHTTPハンドラを書きたくなった時にはどういうふうにすればよいのだろうか? そういうときには、まず自分でHTTPハンドラの型を定義して、それにServeHTTPメソッドを定義することになるだろう。Hello worldを表示するHelloHandlerを定義してみよう。
type HelloHandler struct {} func (HelloHandler) ServeHTTP(w ResponseWriter, r *Request) { w.Write([]byte("Hello world")) }しかしまたもや上のstructはちょっとおかしな感じがする。要素が1つもないのだからわざわざそんなものを用意する必要はなさそうだ。とはいえ型を定義しないとメソッドは定義できないし、一方であえて構造体の中に入れなければいけないデータというものも見当たらない。どうすればよいのだろうか?
この場合には自分の型を関数として定義して、ServeHTTPはそれを呼ぶようにすると、いろいろなものがうまく収まる。
type HelloHandler func(ResponseWriter, *Request) func (f HelloHandler) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }あとは関数を定義してこのHelloHandler型の変数に入れる(あるいは単にその場で変換する)だけでよい。これでめでたしめでたし。
かというと実はこれでもまだ十分にシンプルにはなっていない。
HelloHandler型はHello Worldを表示するという具体的な関数と一切関係がなくなって、ただ関数をラップしてServeHTTPメソッドを足すだけの型になってしまった。ほかのどんなハンドラ関数もこれでラップすることができる。だから別にHelloといった名前を付ける必要はなくて、もっと一般的な名前にしたほうがよさそうだ。
実は上のHelloHandlerと同じメソッドを持つ同じ型がnet/httpにHandlerFuncという名前ですでに定義されている。これを使えば、自前のハンドラはただの関数として定義して、使うときにHandlerFuncに変換するだけでよくなる。
// 自前のハンドラはただの関数 func hello(w ResponseWriter, r *Request) { w.Write([]byte("Hello world")) } func main() { // 型を変換する。直接http.HandlerFunc(hello)と書いてもよい var h http.HandlerFunc = hello http.Handle("/hello", h) }結局のところHTTPハンドラはただの関数にすぎない、というところまで単純化して書くことが可能になったわけだ。Goのシンプルな言語機能の組み合わせの妙というものを感じてもらえただろうか?
Implementation
ここからは標準パッケージの実装を見てみます。
1. HandleFunc 関数
1-1. HandleFunc
まず
HandleFunc関数を見てみます。HandleFunc関数はデフォルトでハンドラ関数をDefaultServeMux変数に登録するのでした。実装は以下のようになっています。DefaultServeMux変数のHandleFuncへのラッパーになっています。func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
DefaultServeMux変数とはどのような構造体になっているか確認すると、以下の実装からわかるように*ServeMux型の変数です。defaultServeMuxはゼロ値で初期化されていてDefaultServeMuxはその初期化された値へのポインタを保持しています。ServeMux構造体はリクエストのパスとハンドラをマッピングするmのフィールドが本質のように見えます。type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames } // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux type muxEntry struct { h Handler pattern string }1-2. ServeMux.HandleFunc
*ServeMux型がレシーバのHandleFunc関数は以下のようになっています。このメソッドもHandleメソッドへのラッパーになっています。// HandleFunc registers the handler function for the given pattern. func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } // handler を HandlerFunc 型に変換して Handle を呼び出す // 変換(Conversions)の仕様は https://golang.org/ref/spec#Conversions を参照 mux.Handle(pattern, HandlerFunc(handler)) }1-3. ServeMux.Handle
では
ServeMux型のHandleメソッドの実装を見てみると、以下のようになっています。この実装でServeMux構造体のフィールドに値をセットしています。// Handle registers the handler for the given pattern. // If a handler already exists for pattern, Handle panics. func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() if pattern == "" { panic("http: invalid pattern") } if handler == nil { panic("http: nil handler") } if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } if mux.m == nil { mux.m = make(map[string]muxEntry) } // パスとハンドラの構造体 muxEntry の値を map へ登録 e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e // TODO: 存在する理由がよくわからず if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } // TODO: よくわかっていない if pattern[0] != '/' { mux.hosts = true } }2. ListenAndServe 関数
次に
ListenAndServeを見てみます。2-1. ListenAndServe
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }ここで生成している
Server構造体で以下で、HTTPサーバとして動作するときのパラメータを定義する構造体です。ListenAndServeではListenするアドレスとハンドラのみを初期化するようになっています。構造体のコメントにもあるようにHandler変数はnilであればhttp.DefaultServeMuxがセットされます。// A Server defines parameters for running an HTTP server. // The zero value for Server is a valid configuration. type Server struct { Addr string // TCP address to listen on, ":http" if empty Handler Handler // handler to invoke, http.DefaultServeMux if nil TLSConfig *tls.Config ReadTimeout time.Duration ReadHeaderTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration MaxHeaderBytes int TLSNextProto map[string]func(*Server, *tls.Conn, Handler) ConnState func(net.Conn, ConnState) ErrorLog *log.Logger BaseContext func(net.Listener) context.Context ConnContext func(ctx context.Context, c net.Conn) context.Context disableKeepAlives int32 // accessed atomically. inShutdown int32 // accessed atomically (non-zero means we're in Shutdown) nextProtoOnce sync.Once // guards setupHTTP2_* init nextProtoErr error // result of http2.ConfigureServer if used mu sync.Mutex listeners map[*net.Listener]struct{} activeConn map[*conn]struct{} doneChan chan struct{} onShutdown []func() }2-2. Server.ListenAndServe
ServerのListenAndServeは以下です。// ListenAndServe always returns a non-nil error. After Shutdown or Close, // the returned error is ErrServerClosed. func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } // TCP のリスナーを生成 ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) }2-3. Server.Serve
続いて
srv.Serve(ln)として実装されているServeメソッドを見てみます。// Serve accepts incoming connections on the Listener l, creating a // new service goroutine for each. The service goroutines read requests and // then call srv.Handler to reply to them. // // HTTP/2 support is only enabled if the Listener returns *tls.Conn // connections and they were configured with "h2" in the TLS // Config.NextProtos. // // Serve always returns a non-nil error and closes l. // After Shutdown or Close, the returned error is ErrServerClosed. func (srv *Server) Serve(l net.Listener) error { if fn := testHookServerServe; fn != nil { fn(srv, l) // call hook with unwrapped listener } origListener := l l = &onceCloseListener{Listener: l} defer l.Close() if err := srv.setupHTTP2_Serve(); err != nil { return err } if !srv.trackListener(&l, true) { return ErrServerClosed } defer srv.trackListener(&l, false) var tempDelay time.Duration // how long to sleep on accept failure baseCtx := context.Background() if srv.BaseContext != nil { baseCtx = srv.BaseContext(origListener) if baseCtx == nil { panic("BaseContext returned a nil context") } } ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { // リスナーの接続要求を待つ(ブロッキング) rw, e := l.Accept() if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } if cc := srv.ConnContext; cc != nil { ctx = cc(ctx, rw) if ctx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return // ゴルーチンで処理 go c.serve(ctx) } }2-4. conn.serve
リクエストをAcceptすると、ゴルーチンとして動作する、リクエストを処理するメソッドを呼び出します。呼び出した後は再度
l.Accept()として次のリクエストを待ち受けます。リクエストを処理する
serveメソッドです。いろいろ処理があって長いのですが、わかりやすく色々省くと以下のように見ることができます。conn.readRequestのメソッドを追っていくと RFC に沿って HTTP リクエストの構文を解析する処理が見れるのですが、(説明しきれないので)ここでは省きます。func (c *conn) serve(ctx context.Context) { // ... for { w, err := c.readRequest(ctx) // ... serverHandler{c.server}.ServeHTTP(w, w.req) // ... } }メソッドの完全な実装は以下です。長いです。
// Serve a new connection. func (c *conn) serve(ctx context.Context) { c.remoteAddr = c.rwc.RemoteAddr().String() ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) defer func() { if err := recover(); err != nil && err != ErrAbortHandler { const size = 64 << 10 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf) } if !c.hijacked() { c.close() c.setState(c.rwc, StateClosed) } }() if tlsConn, ok := c.rwc.(*tls.Conn); ok { if d := c.server.ReadTimeout; d != 0 { c.rwc.SetReadDeadline(time.Now().Add(d)) } if d := c.server.WriteTimeout; d != 0 { c.rwc.SetWriteDeadline(time.Now().Add(d)) } if err := tlsConn.Handshake(); err != nil { // If the handshake failed due to the client not speaking // TLS, assume they're speaking plaintext HTTP and write a // 400 response on the TLS conn's underlying net.Conn. if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) { io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n") re.Conn.Close() return } c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err) return } c.tlsState = new(tls.ConnectionState) *c.tlsState = tlsConn.ConnectionState() if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) { if fn := c.server.TLSNextProto[proto]; fn != nil { h := initNPNRequest{ctx, tlsConn, serverHandler{c.server}} fn(c.server, tlsConn, h) } return } } // HTTP/1.x from here on. ctx, cancelCtx := context.WithCancel(ctx) c.cancelCtx = cancelCtx defer cancelCtx() c.r = &connReader{conn: c} c.bufr = newBufioReader(c.r) c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) for { w, err := c.readRequest(ctx) if c.r.remain != c.server.initialReadLimitSize() { // If we read any bytes off the wire, we're active. c.setState(c.rwc, StateActive) } if err != nil { const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n" switch { case err == errTooLarge: // Their HTTP client may or may not be // able to read this if we're // responding to them and hanging up // while they're still writing their // request. Undefined behavior. const publicErr = "431 Request Header Fields Too Large" fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr) c.closeWriteAndWait() return case isUnsupportedTEError(err): // Respond as per RFC 7230 Section 3.3.1 which says, // A server that receives a request message with a // transfer coding it does not understand SHOULD // respond with 501 (Unimplemented). code := StatusNotImplemented // We purposefully aren't echoing back the transfer-encoding's value, // so as to mitigate the risk of cross side scripting by an attacker. fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders) return case isCommonNetReadError(err): return // don't reply default: publicErr := "400 Bad Request" if v, ok := err.(badRequestError); ok { publicErr = publicErr + ": " + string(v) } fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr) return } } // Expect 100 Continue support req := w.req if req.expectsContinue() { if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { // Wrap the Body reader with one that replies on the connection req.Body = &expectContinueReader{readCloser: req.Body, resp: w} } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() return } c.curReq.Store(w) if requestBodyRemains(req.Body) { registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead) } else { w.conn.r.startBackgroundRead() } // HTTP cannot have multiple simultaneous active requests.[*] // Until the server replies to this request, it can't read another, // so we might as well run the handler in this goroutine. // [*] Not strictly true: HTTP pipelining. We could let them all process // in parallel even if their responses need to be serialized. // But we're not going to implement HTTP pipelining because it // was never deployed in the wild and the answer is HTTP/2. // // serverHandler を生成して ServeHTTP を呼び出す serverHandler{c.server}.ServeHTTP(w, w.req) w.cancelCtx() if c.hijacked() { return } w.finishRequest() if !w.shouldReuseConnection() { if w.requestBodyLimitHit || w.closedRequestBodyEarly() { c.closeWriteAndWait() } return } c.setState(c.rwc, StateIdle) c.curReq.Store((*response)(nil)) if !w.conn.server.doKeepAlives() { // We're in shutdown mode. We might've replied // to the user without "Connection: close" and // they might think they can send another // request, but such is life with HTTP/1.1. return } if d := c.server.idleTimeout(); d != 0 { c.rwc.SetReadDeadline(time.Now().Add(d)) if _, err := c.bufr.Peek(4); err != nil { return } } c.rwc.SetReadDeadline(time.Time{}) } }
serverHandlerはServerへのポインタを保持しているのみです。定義されているメソッドもServeHTTPだけです。実際の処理は*Server型のsrv変数が実装しているServeHTTPに移譲しています。// serverHandler delegates to either the server's Handler or // DefaultServeMux and also handles "OPTIONS *" requests. type serverHandler struct { srv *Server }2-5. serverHandler.ServeHTTP
マルチプレクサ(指定しなかった場合は
DefaultServeMux)からリクエストに対応するハンドラを取得して、そのハンドラのServeHTTPを呼び出しています。func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler // ここで Server のハンドラが nil の場合に DefaultServeMux をセットしている if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) }2-5-1. ServeMux.ServeHTTP
serverHandler.ServeHTTPの一番最後の部分の中身を確認します。handler.ServeHTTP(rw, req)少し戻って
ServeMux型はServeHTTPを実装しているのでHandlerインターフェースを満たしています。この
ServeMux.ServeHTTPメソッドでは*ServeMuxに登録されているハンドラをリクエストから取得して、そのハンドラのServeHTTPを呼び出します。// 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) }2-5-2. ServeMux.Handler
ServeMuxのHandlerメソッドは以下のようになっていて、Handlerインターフェースを返すメソッドです。ざっくりServeMuxで保持しているハンドラを取得するためのメソッドです。func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { // CONNECT requests are not canonicalized. if r.Method == "CONNECT" { // If r.URL.Path is /tree and its handler is not registered, // the /tree -> /tree/ redirect applies to CONNECT requests // but the path canonicalization does not. if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok { return RedirectHandler(u.String(), StatusMovedPermanently), u.Path } return mux.handler(r.Host, r.URL.Path) } // All other requests have any port stripped and path cleaned // before passing to mux.handler. host := stripHostPort(r.Host) path := cleanPath(r.URL.Path) // If the given path is /tree and its handler is not registered, // redirect for /tree/. if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok { return RedirectHandler(u.String(), StatusMovedPermanently), u.Path } if path != r.URL.Path { _, pattern = mux.handler(host, path) url := *r.URL url.Path = path return RedirectHandler(url.String(), StatusMovedPermanently), pattern } return mux.handler(host, r.URL.Path) }2-5-3. ServeMux.handler
// handler is the main implementation of Handler. // The path is known to be in canonical form, except for CONNECT methods. func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock() // Host-specific pattern takes precedence over generic ones // // ServeMux が保持しているmap からパスに対応するハンドラを取得 if mux.hosts { h, pattern = mux.match(host + path) } if h == nil { h, pattern = mux.match(path) } if h == nil { h, pattern = NotFoundHandler(), "" } return }2-5-4. ServeMux.match
上記のメソッドの中で呼び出されている
matchメソッドです。パスからハンドラを探索して、呼び出し元に返します。// Find a handler on a handler map given a path string. // Most-specific (longest) pattern wins. func (mux *ServeMux) match(path string) (h Handler, pattern string) { // Check for exact match first. v, ok := mux.m[path] if ok { return v.h, v.pattern } // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" }まとめ
net/httpのHTTPサーバのリクエストを処理してハンドラを呼び出して、レスポンスを返す実装についてざっと見てみました。
- 投稿日:2020-03-01T00:51:52+09:00
Adafruit Trinket M0でTinyGo
はじめに
Goで組み込み向けプログラミングができるTinyGoが盛り上がってきているようなので試してみた。
今回実行したのはAdafruit Trinket M0の搭載LEDを利用したLチカ。動作環境
- OS: macOS 10.15.3
- Go: 1.13.8
- TinyGo: 0.12.0
- マイコン: Adafruit Trinket M0
セットアップ
Go
$ brew update $ brew install goTinyGo
$ brew tap tinygo-org/tools $ brew install tinygo $ brew tap osx-cross/avr $ brew install avr-gcc avrdude $ go get -u tinygo.org/x/driversBOSSA
BOSSAからMAC用パッケージをダウンロードしてインストール
実行コード
main.gopackage main import ( "machine" "time" ) func main() { led := machine.LED led.Configure(machine.PinConfig{Mode: machine.PinOutput}) for { led.Low() time.Sleep(time.Millisecond * 500) led.High() time.Sleep(time.Millisecond * 500) } }ビルド&書き込み
パターン1. Macでビルド&書き込み
MacにインストールしたTinyGoを使ってビルドから書き込みまで一気に実行
$ tinygo flash -target trinket-m0 ./main.goパターン2. Dockerでビルド&Macから書き込み
Mac上でDockerを使ってビルドして、書き込みはMacから実行
Trinket M0はUSBマスストレージとして扱えるので、ビルドで生成されたUF2ファイルをドラッグ&ドロップで書き込み$ docker run --rm -v $(pwd):/src -w /src tinygo/tinygo:0.12.0 \ tinygo build -o /src/blinky.uf2 -size=short -target trinket-m0 ./main.go実行結果




