Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Golang】io.Pipeのr/wブロック

io.Pipe

io パッケージの関数 io.Pipe()io.Writer を実装した PipeReaderio.Reader を実装した PipeWriter ポインタを返します。 PipeWriter.Write() で書き込みを行うと、その内容を PipeReader.Read() で読みこむことができます。

pr, pw := io.Pipe()

go func(w io.Writer) {
    s := []byte("string")
    if _, err := w.Write(s); err != nil {
        t.Error(err)
    }
}(pw)

b := make([]byte, 1024)
if _, err := pr.Read(b); err != nil {
    t.Error(err)
}
fmt.Println(b) // -> [115 116 114 105 110 103 0 ... ]

PipeReader PipeWriter 共々内部的には同じ pipe 型ポインタを持っていて、これを介してコンテンツを読み書きしています。

pipe.go
// A PipeReader is the read half of a pipe.
type PipeReader struct {
    p *pipe
}
...
// A PipeWriter is the write half of a pipe.
type PipeWriter struct {
    p *pipe
}

io.Pipe のr/wブロック

PipeReader.Read() PipeWriter.Write() は互いの操作をブロックします。すなわち PipeReader.Read()PipeWriter.Write() が呼ばれるまでブロックされ、 PipeWriter.Write() もまた PipeReader.Read() が呼ばれるまでブロックされます。そのため上記の例ではgoroutineで非同期に PipeWriter.Write() を呼び、 PipeReader.Read() がブロックされることを防いでいます。

ではgoroutineを使用せず同期的に PipeReader.Read() あるいは PipeWriter.Write() を呼び出すとそのままプログラムの実行がstopするのか?と思いきやfatal errorで終了します。

pr, pw := io.Pipe()

s := []byte("string")
if _, err := pw.Write(s); err != nil {
    panic(err)
}

b := make([]byte, 1024)
if _, err := pr.Read(b); err != nil {
    panic(err)
}
fmt.Println(b)
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
io.(*pipe).Write(0xc0000201e0, 0xc00001c09a, 0x6, 0x6, 0x0, 0x0, 0x0)
        /usr/local/go/src/io/pipe.go:94 +0x1e5
io.(*PipeWriter).Write(...)
        /usr/local/go/src/io/pipe.go:163

これは pipe が内部で保持するチャネルが入力待ちの状態に陥り、main goroutine含む全てのgoroutineがstopしてしまうために発生します。

pipe.go
func (p *pipe) Write(b []byte) (n int, err error) {
    select {
    case <-p.done:
        return 0, p.writeCloseError()
    default:
        p.wrMu.Lock()
        defer p.wrMu.Unlock()
    }

    for once := true; once || len(b) > 0; once = false {
        select {
        case p.wrCh <- b: // ここでp.wrChが出力可能になるまでブロック
            nw := <-p.rdCh
            b = b[nw:]
            n += nw
        case <-p.done:
            return n, p.writeCloseError()
        }
    }
    return n, nil
}

...

func (p *pipe) Read(b []byte) (n int, err error) {
    select {
    case <-p.done:
        return 0, p.readCloseError()
    default:
    }

    select {
    case bw := <-p.wrCh: // ここで入力可能になるまでブロック
        nr := copy(b, bw)
        p.rdCh <- nr
        return nr, nil
    case <-p.done:
        return 0, p.readCloseError()
    }
}

PipeReader.Read() PipeWriter.Write() がブロックされるのは、内部のチャネルのブロックによるものでした。 こうして見ると、 io.Pipe 関数はチャネルによるgoroutine間の []byte I/Oを抽象化するWrapperのようにも感じます。

nits

中間にバッファを介せば、一応同期的にr/w処理を分けることができます。バッファ分のメモリ容量を必要としますが...

buf := bytes.Buffer{}

s := []byte("string")
if _, err := buf.Write(s); err != nil {
    t.Error(err)
}

b := make([]byte, 1024)
if _, err := buf.Read(b); err != nil {
    t.Error(err)
}
fmt.Println(b)

参考文献

golang.org

【Golang】Unicode上複数コードからなる文字をruneで扱う場合の挙動

検証環境

Mac OS Catalina 10.15.6
Go1.15.6

tl;dr

Unicode上でn個のコードからなる1文字を []rune に変換すると、[]rune スライス長は n になります。

前置き

Goにおける文字列型 string の値は、 []byte スライスの値としても扱うことができます。 例えば以下のように string[]byte にキャスト可能な他、

s := "こんにちは、Golang"
b := []byte(s) // []byte配列用にmemory allocate され、バイト列もcopyされる

string の値に要素アクセスすると対応する位置の byte 値を取得します。

s := "こんにちは、Golang"
b := s[0] // 227

文字列の長さを len 関数で取得すると、 []byte スライスとしての長さが返ってきます。

s := "こんにちは、Golang"
length := len(s) // 24

しかし実際には、本来の文字列一文字一文字についてアクセスしたい場合が多いと思います。(例えば こんにちは、Golang を12文字の「意味のある文字の配列」として扱いたい、など) Golangでは rune 型を使用し、この問題を解消します。

rune

文字列を []byte スライスとして扱う場合、それは元の各文字に割り当てられたUnicode(ISO 10646)をそれぞれUTF-8 encodingに従いバイト列に変換したものになります。

s := "こんにちは、Golang"
b := s[0]
fmt.Println(b)  // -> 227 
fmt.Printf("%v\n", []byte("こ")) // -> [227 129 147]
fmt.Printf("%+q\n", "こ") // -> "\u3053" (Unicode)
fmt.Printf("%q\n", "こ") // -> "12371" (Unicode code point)

Unicodeの単位で文字列の各文字にアクセスするには、文字列を []rune スライスにキャストします。

s := "こんにちは、Golang"
r := []rune(s)
fmt.Println(r[0]) // -> 12371
fmt.Println(string(r[0])) // -> こ

あるいは for で文字列に対して rangeによる要素アクセスを実行すると、文字列要素に rune 単位でアクセスすることができます。

s := "こんにちは、Golang"
for _, r := range s {
    fmt.Print(string(r))
} 
// -> こんにちは、Golang

複数コードからなる文字をruneとして扱う場合

先程「Unicodeの単位」と述べましたが、Unicodeには複数のcodeで一つの文字を表現するパターンもあります。 tech.sanwasystem.com

例えば、日本語の漢字のうち微妙に表現が異なる漢字郡は「異体字セレクタ」として扱われます。異体字セレクタでは同じ意味の似た文字に対して同じコードを与え、さらにそれらを識別するためのコードも与えることで(計2つ)、一意な文字を表現します。

ja.wikipedia.org

以下の例は2つのcodeからなる漢字 邊󠄆( U+908A U+E0106 )を []rune に変換したものです。この漢字、右上の「自」が「白」になっているパターンの「わた」なんですがブログ上の表記ではわからないかもです(上記文献を参照)。

wata := "邊󠄄"
rWata := []rune(wata)
fmt.Printf("%+q\n", rWata) // -> ['\u908a' '\U000e0104']
fmt.Println(len(rWata)) // -> 2

出力例のように、異体字セレクタ文字列を []rune に変換すると。長さ2となることがわかります。

fmt.Println(rWata[0]) // 37002
fmt.Println(string(rWata[0])) // -> 邊󠄄
fmt.Println(string(rWata[1])) // -> (空文字)
fmt.Printf("%+q\n",rWata[0]) // -> '\u908a'

先頭の文字を出力すると、ベースとなる文字が出現します。

このように文字列を rune で扱う場合も、Unicode上で複数codeからなる文字を扱う場合には想定する文字列長と異なってしまうので注意が必要です。

参考文献

blog.golang.org

tech.sanwasystem.com

ja.wikipedia.org

自前のブログからはてなブログに戻しました

あけましておめでとうございます。

2020年初めにブログを(半)自前運用1に移行したのですが、諸々面倒だったのでブログをはてなに戻すことにしました。
自前運用ブログの記事もこちらに移しました。

rennnosukesann.hatenablog.com

ついでにデザインテーマも変更しました。

自前運用のブログで面倒だったところ

以下面倒だった点です。
ちなみにブログはHugoでブログテンプレートを作り、Netlifyにホスティングする形式をとっていました。

画像挿入をMarkdownに記述+指定ディレクトリに置かなければいけない

Hugoにはブログ記事を編集するGUIもありましたが、はてブのほうがストレスが少なかったです。

プレビューが実際の内容と一致しない

エディタでmarkdownプレビュー見ても実際どう描画されるか不明なため、結局何回もdeployしてました。
ローカルで記事見るにもテンプレート使ったbuildが必要でした。

デプロイがたまにコケる

これはしょうがないです。Netlifyも無償枠だったので。。。

他マシン環境で執筆環境を作るのが少し面倒

ブログ用のリポジトリをクローンして、書いて、pushして・・・と、ちょっと面倒でした。

といったところです。見事にSaaSを使わないことによるデメリットを享受しています。

運用する前に上記面倒のいくつかは想定はしていたし、当初は自前ブログやってみるぞ意欲があったのでそのノリで初めてはみたのですが、思ったよりしんどいなと思ったので戻そうと思いました。

HugoとNetlify自体は良いツール・サービスでした。Hugoはブログテンプレートを多くの有志が作成した中から無料で使えるし、Netlifyは無償でホストを提供してくれる上にGithubと連携して勝手にCIしてくれます。それでも自前運用自体に上位のような手間があったので、やっぱりブログを細かくカスタマイズしたり、フロントを完全に自前で作りたい人などが自前運用に適しているのかもしれません。

今後

幸いにもはてなの方の拙ブログ(?)を今も読んでくださっている方がいらっしゃるようで、平日の多いときには400viewくらいにはなっています。

f:id:rennnosukesann:20210102134555p:plain

一年も放置したんだから流石にがっつり減るだろうと思っていたのですが、思ったよりview数の減りが少なくてびっくりしました(100くらいになると思ってた)。
ちなみに2年前に毎日ブログ更新していたときは平日概ね500-600viewくらいでした。

rennnosukesann.hatenablog.com

自分はブログをあくまで自分の学びのきっかけや備忘録のために書く多いのですが、それでも多くの人の役に立つような記事を残せたらなあと思う次第です。

ということで2021年もポツポツと記事を書いていくと思いますが、今年もどうぞよろしくお願いします。


  1. 結局ブログの格子はHTMLジェネレータだし、サーバー管理はホスティングサービスにまかっせきりなので..

【OpenAPI】PrismでOpenAPIドキュメントからモックサーバーを起動する

Prism

Stoplight 社が提供する1OpenAPI ドキュメントからモックサーバーを起動するツール。format は OpenAPI2/3 をサポートする。

Usage

Install

Prism は node 上で実行されるため、node 実行環境を用意してインストールする。

Installation

Docker

node サーバーコンテナを立ててその中にインストールする例( 一応 Prism コンテナイメージをそのまま起動することはできる

FROM node:14.14

WORKDIR /openapi

COPY ./openapi.yml .

# install stoplight/prism
RUN npm install -g @stoplight/prism-cli

# exec - specify host as 0.0.0.0 to connect from docker host.
CMD ["prism", "mock", "openapi.yml", "--host", "0.0.0.0"]

Prism インストール後、 prism mock [openapi doc] でモックサーバーを起動する。

OpenAPI ドキュメント上で定義されたパスのうち、Path Parameter の指定はランダムに変化する。

$ docker build -t rennnosuke/prism-test .
$ docker run -it -p 4010:4010 rennnosuke/prism-test
[10:57:16 AM] › [CLI] …  awaiting  Starting Prism…
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/pet
[10:57:16 AM] › [CLI] ℹ  info      PUT        http://0.0.0.0:4010/pet
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/pet/findByStatus?status=pending,sold,available,available,sold,sold,pending,sold,pending,available,sold,pending,sold,available,available,available,pending,sold,available,pending
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/pet/findByTags?tags=doloribus,consequuntur,expedita,tempora,temporibus,nemo,adipisci,molestiae,reprehenderit,voluptatibus,laboriosam,sed,pariatur,culpa,dicta,illum,qui,repellat,adipisci,incidunt
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/pet/762
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/pet/447
[10:57:16 AM] › [CLI] ℹ  info      DELETE     http://0.0.0.0:4010/pet/951
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/pet/576/uploadImage
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/store/inventory
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/store/order
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/store/order/4
[10:57:16 AM] › [CLI] ℹ  info      DELETE     http://0.0.0.0:4010/store/order/141
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/user
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/user/createWithArray
[10:57:16 AM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/user/createWithList
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/user/login?username=non&password=provident
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/user/logout
[10:57:16 AM] › [CLI] ℹ  info      GET        http://0.0.0.0:4010/user/placeat
[10:57:16 AM] › [CLI] ℹ  info      PUT        http://0.0.0.0:4010/user/qui
[10:57:16 AM] › [CLI] ℹ  info      DELETE     http://0.0.0.0:4010/user/ex
[10:57:16 AM] › [CLI] ▶  start     Prism is listening on http://0.0.0.0:4010
$ curl -XGET -s -D "/dev/stderr" -H "Content-type: application/json" http://0.0.0.0:4010/store/inventory
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 29
Date: Sun, 18 Oct 2020 11:01:52 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"property1":0,"property2":0}

tips

動的レスポンス生成

Prism ではレスポンスの値を動的に生成できる。

x-faker プロパティを Schema プロパティ上に定義することで、レスポンス中の値がランダム生成されるようになる。

指定方法は Faker.js に従う。

公式より引用)

Pet:
  type: object
  properties:
    id:
      type: integer
      format: int64
    name:
      type: string
      x-faker: name.firstName
      example: doggie
    photoUrls:
      type: array
      items:
        type: string
        x-faker: image.imageUrl
$ curl http://127.0.0.1:4010/pets/123 -H "Prefer: dynamic=true"
{
  "id": 12608726,
  "name": "Addison",
  "photoUrls": [
    "http://lorempixel.com/640/480",
    "http://lorempixel.com/640/480",
    "http://lorempixel.com/640/480",
    "http://lorempixel.com/640/480"
  ]
}

Definition Engine

Prism モックサーバーがリクエストを受け取ったときのレスポンス決定ルール。 Mock をより精密に動作させるために、また OpenAPI ドキュメントの項目を充実するために使える。

参考文献


  1. OpenAPI ドキュメントの編集ツールstoplight studioも便利

【AWS】IAMユーザーにAWS上のリソースに対するSSL/TLS通信を強制する

メモ。

特定のユーサーに AWS 上のリソースへの操作権限を与えたいが、操作のためのリクエストは SSL/TLS 通信に限定したい場合がある。例えば、S3 への API 経由のファイルアップロードなどをユーザーに許可する際、それを HTTPS に限定したいときなどがある。

SSL/TLS 通信制約は、IAM ポリシーに下記のような条件 Condition を追加することで達成できる。

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "iam:*AccessKey*",
    "Resource": "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/*",
    "Condition": {
      "Bool": {
        "aws:SecureTransport": "true"
      }
    }
  }
}

Condition.Bool["aws:SecureTransport"] プロパティを true にすると、ユーザーからの暗号化されていないプレーンな HTTP によるアクセスが拒否される。

参考文献

IAM ポリシーエレメント: 変数およびタグ - AWS Identity and Access Management

【Golang】Goで形態素解析する - mecab-golang

MeCab とは

MeCab: Yet Another Part-of-Speech and Morphological Analyzer

自然言語処理界隈では ChaSen と並び有名な形態素解析エンジン。
形態素解析とは、テキストデータを文法や単語の品詞情報(辞書)を元に言語の最小単位(形態素)へと分割し、各形態素の品詞などを判別すること。

ex. 裾野は長し赤城山

裾野 名詞,一般,*,*,*,*,裾野,スソノ,スソノ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
長し 形容詞,自立,*,*,形容詞・アウオ段,文語基本形,長い,ナガシ,ナガシ
赤城山 名詞,固有名詞,地域,一般,*,*,赤城山,アカギヤマ,アカギヤマ

mecab-golang

Go から mecab を使用するライブラリ。mecab の go wrapper。 なので使用する場合は mecab-golang とは別に、MeCab や辞書をインストールする必要がある。

Install

Mecab のインストール

mecab をインストール。
mecab を扱うには、MeCab形態素解析に使用する辞書が別途必要になる。
mecab-ipadic はその MeCab 用の辞書の一つであり、日本語の単語情報(読み方や品詞など)を掲載している。1

$ brew install mecab
$ brew install mecab-ipadic

mecab-ipadic-NEologd のインストール

mecab-ipadic-NEologd

Web 上から逐一新語を登録してくれる辞書 mecab-ipanic では判断できない新語を(全てではないが)サポートする。 ↓ こんなかんじに、例えば名詞をより細かい品詞に分割して解釈してくれる。

default system dictionary      | mecab-ipadic-NEologd
ピット 星 人             | ピット 星人
この 世界 の 片隅 に          | この世界の片隅に
辛 坊                   | 辛坊
東海大 市原 望 洋          | 東海大市原望洋
方言 ラジオ 体操               | 方言 ラジオ体操
コイ キング              | コイキング
はだし の ゲン              | はだしのゲン
あさ パラ             | あさパラ
雪 組                   | 雪組

-n で最新 ver 辞書をインストール

$ git clone git@github.com:neologd/mecab-ipadic-neologd.git
$ cd mecab-ipadic-neologd
$ ./bin/install-mecab-ipadic-neologd -n

mecab-golang のインストール

$ export CGO_LDFLAGS="-L/{libフォルダへのパス}/lib -lmecab -lstdc++"
$ export CGO_CFLAGS="-I/{includeフォルダへのパス}/include"
$ go get github.com/bluele/mecab-golang

Usage

bluele/mecab-golang)サンプルコードを参考にした。

package main

import (
    "fmt"
    "strings"

    "github.com/bluele/mecab-golang"
)

const BOSEOS = "BOS/EOS"

func parseToNode(m *mecab.MeCab, text string) error {

    tg, err := m.NewTagger()
    if err != nil {
        return err
    }
    defer tg.Destroy()

    lt, err := m.NewLattice(text)
    if err != nil {
        return err
    }
    defer lt.Destroy()

    node := tg.ParseToNode(lt)
    for {
        features := strings.Split(node.Feature(), ",")
        if features[0] != BOSEOS {
            fmt.Printf("%s %s\n",node.Surface(), node.Feature())
        }
        if node.Next() != nil {
            break
        }
    }
    return nil
}

func main() {
    m, err := mecab.New("-Owakati")
    if err != nil {
        panic(err)
    }
    defer m.Destroy()

    err = parseToNode(m, "すもももももももものうち")
    if err != nil {
        panic(err)
    }
}

解説

func main()

mecab.New()MeCab モデルオブジェクトを生成する。
引数には元々の MeCab 実行バイナリの引数に指定できるものを渡すことができる。ここでは形態素解析の結果(後述の Tagger::Parse で取得)を分かち書きで出力させるオプション -Owakati を指定している。
他のオプションはここを参照。

parseToNode() は第 2 引数にとる文字列を形態素解析・その結果を標準出力へ書き込む。

func parseToNode()

引数に取る MeCab モデルオブジェクトと文字列から、形態素解析の結果を出力する。

以下で説明する関数やメソッドは MeCab c++ ライブラリ準拠のもののため、MeCab 本家の C/C++ライブラリを読めば問題なさそう。2

MeCab::NewTagger()

辞書オブジェクト Tagger を生成する。

MeCab::NewLattice()

解析に必要なローカル変数を含むオブジェクト Lattice を生成する。
生成時に解析対象となる文字列を渡す。

Tagger::ParseToNode()

Lattice を引数に取ることで、 Lattice が保持する文字列を解析し、木構造のオブジェクト Node として解析結果を返す。

ちなみに parseToNode() 内では BOS/EOS( BOS は beginning of sentence で文頭、EOS は end of sentence で文末) 品詞タグのついた形態素情報以外を出力するようにしている。

Node::Feature()

Node の解析結果を出力する。
例えば分かち書き指定の場合こんな感じ。

名詞,一般,*,*,*,*,すもも,スモモ,スモモ
Node::Surface()

単語の品詞を出力する。

実行結果

すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

独自フォーマットで形態素解析の結果を取得する

Tagger::ParseToNode で取得する Node からは特定のフォーマットで結果が出力できるが、 mecab.New() でフォーマットを指定することで、 Tagger::Parse の文字列結果を変更することができる。

分かち書き

m, err := mecab.New("-Owakati" )
すもも も もも も もも の うち

フォーマット

-F オプションを使用すると、下記の出力フォーマットを使用して自由に出力を切り替えられる。 出力フォーマット

ex.
m, err := mecab.New("-F%m(素性ID:%h)\\n")

3

すもも(素性ID:38)
も(素性ID:16)
もも(素性ID:38)
も(素性ID:16)
もも(素性ID:38)
の(素性ID:24)
うち(素性ID:66)
EOS

参考文献

MeCab: Yet Another Part-of-Speech and Morphological Analyzer

mecab-ipadic-neologd/README.ja.md at master · neologd/mecab-ipadic-neologd

MeCab: Yet Another Japanese Dependency Structure Analyzer

bluele/mecab-golang: A golang wrapper for mecab.

IPADIC(IPA 辞書)とはなにものか? - ぱらめでぃうす


  1. 元々は別の形態素解析エンジンである ChaSen に使用された辞書を、MeCab 用に更に変更を加えたもの。ipa とついているのは、その品詞フォーマットが情報処理振興事業協会(IPA)で設定された IPA 品詞体系(THiMCO97)に基づいているため。参考:http://parame.mwj.jp/blog/0209

  2. この C/C++ライブラリドキュメントでは Tagger によるシングルスレッド環境向けサンプルと Tagger/Model(NewMecab 返戻値型に対応)/Lattice によるマルチスレッド環境向けサンプルがある。mecab-golang では Go を使用する以上、goroutine によるマルチスレッド環境の可能性を前提にライブラリが組まれていると思われ、そのため Tagger::Parse/ParseToNode は Lattice を引数に取っている。

  3. 素性 ID - 品詞/活用/読みに割り振られた MeCab 内部 ID

【Golang】Golang:logパッケージを読む

log パッケージを読みました。

src/log/log.go - The Go Programming Language

log

Go の log はロギング処理に使用するパッケージ。
log パッケージはテストコードを除けば log.go のみで構成されており、log.go も 400 数行からなるシンプルな構成となっている。
sysloglog パッケージ配下に置かれているが、それとは分けて話す )

log パッケージ概要

// Package log implements a simple logging package. It defines a type, Logger,
// with methods for formatting output. It also has a predefined 'standard'
// Logger accessible through helper functions Print[f|ln], Fatal[f|ln], and
// Panic[f|ln], which are easier to use than creating a Logger manually.
// That logger writes to standard error and prints the date and time
// of each logged message.
// Every log message is output on a separate line: if the message being
// printed does not end in a newline, the logger will add one.
// The Fatal functions call os.Exit(1) after writing the log message.
// The Panic functions call panic after writing the log message.
package log
  • ロギングに使用する Logger 型と、それに紐づくメソッド(Formatter など)を定義している
  • Logger を明示的に生成してロギングするか、ヘルパ=関数経由で標準 Logger によるロギングができる
    • ヘルパー関数を使用すると、標準 Logger がログメッセージに日付・時刻を付加する
  • Logger 経由のロギングは以下の特徴を持つ
    • 1 ログにつき 1 行(カスタム Logger でなければ)
    • Fatal 系関数はロギング後 os.Exit(1)を呼ぶ
    • Panic 系関数はロギング後 panic を呼ぶ

Logger 構造体

// A Logger represents an active logging object that generates lines of
// output to an io.Writer. Each logging operation makes a single call to
// the Writer's Write method. A Logger can be used simultaneously from
// multiple goroutines; it guarantees to serialize access to the Writer.
type Logger struct {
    mu     sync.Mutex // ensures atomic writes; protects the following fields
    prefix string     // prefix on each line to identify the logger (but see Lmsgprefix)
    flag   int        // properties
    out    io.Writer  // destination for output
    buf    []byte     // for accumulating text to write
}

Logger 構造体は log パッケージが提供するロギング機能を実現するための構造体。

  • mu: sync.Mutex 値。goroutine による並行処理上でも排他制御しつつロギングするために使用する。
  • prefix : ログ行の先頭に付与する文字列。ただし、 Lmsgprefixflag に指定するとメッセージの先頭に付与される。
  • flag : ログの接頭辞を制御するフラグ。指定できるフラグは以下。ビットフラグになっており、 OR 演算で結合して複数指定できる。 ex. Ldate | Lmicroseconds
  • out: io.Writer 実装型の値。ログの出力先。
  • buf: ログ書き込み文字列のバッファ。
const (
    Ldate         = 1 << iota     // ローカルタイムゾーンの日時を次のフォーマットで表示: 2009/01/23
    Ltime                         // ローカルタイムゾーンの時刻を次のフォーマットで表示: 01:23:23
    Lmicroseconds                 // ローカルタイムゾーンの時刻を次のフォーマットで表示: 01:23:23.123123.  assumes Ltime.
    Llongfile                     // パス/ファイル名と行数を表示: /a/b/c/d.go:23
    Lshortfile                    // ファイル名と行数を表示、Llongfileを上書きする: d.go:23.
    LUTC                          // Ldate, Ltimeが設定されていた場合、タイムゾーンをUTCとする
    Lmsgprefix                    // prefixをログ先頭ではなくメッセージ先頭に移す
    LstdFlags     = Ldate | Ltime
)

Logger を明示的に作成して使用する

log パッケージには Logger 値参照を取得するヘルパー関数 New が提供されている。
引数の詳細は上記参照。

// New creates a new Logger. The out variable sets the
// destination to which log data will be written.
// The prefix appears at the beginning of each generated log line, or
// after the log header if the Lmsgprefix flag is provided.
// The flag argument defines the logging properties.
func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}

Logger public メソッド

l := log.New(os.Stdout, "prefix: ", log.Ldate | log.Lmicroseconds | log.LUTC)
l.Println("Hello, Logger.")

Logger が使用できる出力系関数は以下。

func (l *Logger) Printf(format string, v ...interface{})
func (l *Logger) Print(v ...interface{})
func (l *Logger) Println(v ...interface{})
func (l *Logger) Fatal(v ...interface{})
func (l *Logger) Fatalf(format string, v ...interface{})
func (l *Logger) Fatalln(v ...interface{})
func (l *Logger) Panic(v ...interface{})
func (l *Logger) Panicf(format string, v ...interface{})
func (l *Logger) Panicln(v ...interface{})

これらの関数全ては、内部で Logger::Output 関数を呼び出している。

Logger::Output

Logger::Output は、各種出力系関数から呼ばれる汎用出力関数。
呼び出すごとに1行のログを出力する。

// Output writes the output for a logging event. The string s contains
// the text to print after the prefix specified by the flags of the
// Logger. A newline is appended if the last character of s is not
// already a newline. Calldepth is used to recover the PC and is
// provided for generality, although at the moment on all pre-defined
// paths it will be 2.
func (l *Logger) Output(calldepth int, s string) error {
// ...
}

Loggersync.Mutex を使用して排他制御をしているため、ロギングはスレッドセーフ(goroutine セーフ?)。

   l.mu.Lock()
    defer l.mu.Unlock()
    // ...
    _, err := l.out.Write(l.buf)
    return err

Logger.flagLshortfile Llongfile が設定されている場合、ファイル名・行数を取得するため、ロックを外して runtime.Caller を呼ぶ。

   if l.flag&(Lshortfile|Llongfile) != 0 {
        // Release lock while getting caller info - it's expensive.
        l.mu.Unlock()
        var ok bool
        _, file, line, ok = runtime.Caller(calldepth)
        if !ok {
            file = "???"
            line = 0
        }
        l.mu.Lock()
    }

Callerruntime パッケージの関数で、ゴルーチンスタック中の関数呼び出し情報を取得できる。この関数呼び出しの情報の中にロギング時のファイル・行数情報が含まれていて、 Output ではそれを使う。

package runtime

import "runtime/internal/sys"

// Caller reports file and line number information about function invocations on
// the calling goroutine's stack. The argument skip is the number of stack frames
// to ascend, with 0 identifying the caller of Caller.  (For historical reasons the
// meaning of skip differs between Caller and Callers.) The return values report the
// program counter, file name, and line number within the file of the corresponding
// call. The boolean ok is false if it was not possible to recover the information.
func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
    rpc := make([]uintptr, 1)
    n := callers(skip+1, rpc[:])
    if n < 1 {
        return
    }
    frame, _ := CallersFrames(rpc).Next()
    return frame.PC, frame.File, frame.Line, frame.PC != 0
}

フラグ指定による接頭辞を付与するために、 formatHeader を呼ぶ。

   l.buf = l.buf[:0]
    l.formatHeader(&l.buf, now, file, line)

formatHeaderLogger.flags ビットフラグに従い接頭辞を指定する。
接頭辞は以下の優先順位で付与される。

  • Logger.prefix に指定した文字列
  • 日時・時刻
  • ファイル名・行数
  • LmsgprefixLogger.prefix に設定した場合 )Logger.prefix に指定した文字列
// formatHeader writes log header to buf in following order:
//   * l.prefix (if it's not blank and Lmsgprefix is unset),
//   * date and/or time (if corresponding flags are provided),
//   * file and line number (if corresponding flags are provided),
//   * l.prefix (if it's not blank and Lmsgprefix is set).
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
// ...
}

あとは Logger.out に文字列を書き込む。
文字列末尾に改行が含まれていなければ追加する。

   l.buf = append(l.buf, s...)
    if len(s) == 0 || s[len(s)-1] != '\n' {
        l.buf = append(l.buf, '\n')
    }
    _, err := l.out.Write(l.buf)
    return err

Logger をヘルパー関数経由で使用する

log パッケージクライアントは Logger 型の値を生成しなくても、下記関数でロギング機能を使用することはできる。

func Printf(format string, v ...interface{})
func Print(v ...interface{})
func Println(v ...interface{})
func Fatal(v ...interface{})
func Fatalf(format string, v ...interface{})
func Fatalln(v ...interface{})
func Panic(v ...interface{})
func Panicf(format string, v ...interface{})
func Panicln(v ...interface{})

これらの関数は共通のローカル Logger 値である std を使用する。
std の出力先は os.Stderr であり、接頭辞として日付・時刻がつく。

var std = New(os.Stderr, "", LstdFlags)

上記関数群は内部で std.Output を呼び、ロギング処理を実行する。
std 経由で Logger メソッドを呼び出さない理由は不明(ユースケース別にあえて分けた?)。

// Printf calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Printf.
func Printf(format string, v ...interface{}) {
    std.Output(2, fmt.Sprintf(format, v...))
}

参考文献

src/log/log.go - The Go Programming Language