20200301のGoに関する記事は7件です。

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()

解析するフラグとusagemain()の外で定義されています
フラグは-min_confidence-set_exit_status2種類のみで、コマンドラインの入力にオプションがあった場合、minConfidencesetExitStatusにポインタが格納されます

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. 引数がファイルの場合
  4. 1,2,3どれにも当てはまらない場合

1,2の場合はディレクトリ指定モード、3の場合はファイル指定モード、4の場合はパッケージ指定モードに入ります
分岐先で、dirsRun || filesRun || pkgsRunに1を代入してます
解析した引数はargsappendします

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.go
func 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)

pkgsmain()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)
    }
}

解析結果の出力は解析関数内で行います

まとめ

思ってたよりコードが少なくて驚きました
flagpathなど、標準パッケージが優秀なのは有り難いですね
次はcliツール自作をやってみたいです

Qiita書きながらのコードリーディングはきっちり理解しようとする気概が出てくるので良いなと思いました
解析部分の記事は気が向いたら書きます

参考
https://mattn.kaoriya.net/software/lang/go/20171024130616.htm

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データ分析基盤構築入門 サンプルアプリエラー

データ分析基盤構築入門のサンプルアプリケーションを、Dockerで立ち上げる際にエラーが出て、うまく立ち上がらなかったので、色々ソリューションを試しました。

その中で、2020/3/1現在のソリューションを書きます。

エラー内容

$ docker-compose up --build

dockerで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: articles

goアプリケーションコンテナに入ってあげてマイグレーション実行すると、さらにエラーが出ます。

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 1

3.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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin vs Go 処理時間を比較してみる

1. はじめ

最近はどこもかしこもGoばっかり!
SE(笑)で化石のようなJavaを扱った経験を持つ人間としてはKotlinの方がしっくりくるのですが、なんだか肩身が狭いです。

そこで今回はGoとKotlinで同じようなコードを書いてみて処理時間という点でどれだけ差があるか実験してみました。

2. 内容

・Kotlinはコンパイラでjarにした後、Amazon Corretto8のランタイムで動かす
・Goは公式でリリースされてる最新のものを使用(Go 1.14)

1回目

・10万の要素を持つ配列に乱数で値をひたすら入れてみる
・とりあえずこれだけ

main.kt
import 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.go
package 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))
}

【結果】

Kotlin:19ms
Go :1.0418ms
main.kt - 20200301 - 1発目.png
main.go - 20200301 - 1発目.png

2回目

・consoleに配列の内容を全出力というものを足すとどうだ??
.kt、.goともに前述のコードのコメント部分復活

    //.kt
    listData.forEach {
        println("it:" + it)
    }

    //.go
    for _, value := range listData {
        fmt.Printf("it:%d \n" , value)
    }

【結果】

Kotlin:19.76s
Go :17.41s
main.kt - 20200301 - 2発目.png
main.go - 20200301 - 2発目.png

おわり

上記の実験ではどちらもGoが圧勝する内容でした。
単純なロジックだと勝負にならないくらいの差ですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.go
package 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が返してくるエラーを無視しないというのも大事な教訓ですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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を返します。

さいごに

最後まで読んで頂き、ありがとうございました。
「もっとこうした方が良いよ」等のツッコミがあれば、コメント欄でお待ちしております!

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 HandlerFunc

type HandlerFunc func(ResponseWriter, *Request)

HandlerFunc 型は通常の関数を HTTP ハンドラとして扱えるようにするためのアダプターです。fが適切なシグネチャを持っている関数であれば、HanderFunc(f) はハンドラと振る舞うことができ、関数fを呼び出します。

func (HandlerFunc) ServeHTTP

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

ServeHTTPはf(w, r)を呼び出します。

HandlerFuncServeHTTP のように自身の関数を呼び出すことで 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 ListenAndServe

func ListenAndServe(addr string, handler Handler) error

HTTP サーバを起動する関数です。以下は 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 Handle

func 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 HandleFuncfunc 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 Handlertype 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

ServerListenAndServe は以下です。

// 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{})
    }
}

serverHandlerServer へのポインタを保持しているのみです。定義されているメソッドも 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

ServeMuxHandler メソッドは以下のようになっていて、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サーバのリクエストを処理してハンドラを呼び出して、レスポンスを返す実装についてざっと見てみました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 go

TinyGo

$ brew tap tinygo-org/tools
$ brew install tinygo
$ brew tap osx-cross/avr
$ brew install avr-gcc avrdude
$ go get -u tinygo.org/x/drivers

BOSSA

BOSSAからMAC用パッケージをダウンロードしてインストール

実行コード

main.go
package 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

実行結果

IMG_20200301_004751.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む