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

GO1.11.9のコンテナでgo testしようとするとexec: "gcc": executable file not found in $PATH と怒られる

Goの公式DockerImageでgo testしたいのに exec: "gcc": executable file not found in $PATH と怒られる問題。

ググって下記にたどり着く
https://github.com/golang/go/issues/27303

goenv で環境変数を確かめてみると CGO_ENABLED="1" になっていた。

export CGO_ENABLED=0 でこいつを無効にしてやると go test が通るようになった。

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

Windows10 GO言語環境構築してみた

はじめに

今回はWindows用インストーラーからダウンロードしようとしたが.msiが何故か動かなかったため、zipファイルから環境構築することにした。

対象者

Windows10でGo言語環境構築したい人

設定環境

Windows10 64-bit
インストールするバージョン:go1.12.4

1.ダウンロード

C:に(C:\Go)となるようにGoフォルダを作る。
「公式サイト」からgo1.12.4.windows-amd64.zipをダウンロード。ダウンロードしたzipを展開し、中のデータを(C:\GO)のフォルダに移動させる。
例えば、binフォルダだと、(C:\Go\bin)となるはずである。

2. PATHを通す

「システム」→「システムの詳細設定」→「環境変数」から以下のようにユーザー環境変数にGOPATHを新規」ボタンから追加。

次に、ユーザー環境変数のPathの欄を選択し「編集」ボタンから編集する。「新規」ボタンから「%GOPATH%bin」を追加。

GOPATH2.png

4.確認

PowerShellで「go version」で以下のように表示できれば正常にインストールできている。
go_version.png

参考サイト

「婿入りエンジニア、ブログ書くーGolang開発環境構築 – Windows10 / VisualStudioCode」
「Windows10でGO言語環境の構築」

最後に

最後まで閲覧いただきありがとうございました。まだ、知らないことが多いので不備等ありましたらご教授のほどよろしくお願いいたします。

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

Excelの全角数値を半角数値に変換する

経緯

前職SIerの自称・技術力高い上司(50代)が、顧客から「全角数値を半角数値に変換できないか?」という要望に対して、「技術的に難しい」と回答したらしい。

自称・技術力高い上司が「全角から半角に変換できない」というからには、おそらく難しいのだろうと思ったのであるが、(不慣れな)Go言語を用いて対応可能かを検証してみた。

やること

同じフォルダにエクセル(.xlsxのみ)を保存して実行すると、copy_***.xlsxというファイルが作成され、全角数値だった箇所のみが半角数値に変換される

ソース

main.go
package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path"
    "regexp"
    "strings"
    "time"

    "github.com/tealeg/xlsx"
)

func main() {
    dt1 := time.Now()

    var files []string
    files = GetFiles() // カレントディレクトリのエクセルを取得
    for _, file := range files {
        fmt.Println("ファイル名", file)

        copyfile := "copy_" + file
        err := os.Link(file, copyfile) // ファイルのコピー
        if err != nil {
            fmt.Printf(err.Error())
        }
        excel, err1 := xlsx.OpenFile(copyfile) // コピーしたファイルを開く

        if err1 != nil {
            fmt.Printf(err1.Error())
        }

        for _, sheet := range excel.Sheets {
            fmt.Println("シート名", sheet.Name)
            for _, row := range sheet.Rows {
                for _, cell := range row.Cells {
                    v := cell.String()
                    val := ZenkakuToHnakaku(v)
                    // 変換不要なセルもあるが、とりあえず全て書き換え
                    // fmt.Println(val)
                    cell.Value = val
                    //cell.Value = fmt.Sprintf(val)
                }
            }
        }

        // ファイルを保存
        err = excel.Save(copyfile)
        if err != nil {
            fmt.Printf(err.Error())
        }

    }

    dt2 := time.Now()
    fmt.Println(dt2.Sub(dt1)) //Excelファイル変換時間

}

func GetFiles() []string {
    dir, _ := os.Getwd() //カレントディレクトリ
    //dir := "/home/.../" //フォルダを直接指定する場合
    files, err := ioutil.ReadDir(dir)
    if err != nil {
        panic(err)
    }

    var paths []string
    for _, file := range files {
        if !file.IsDir() && path.Ext(file.Name()) == ".xlsx" &&
            !regexp.MustCompile(`^~$*`).Match([]byte(file.Name())) {
            paths = append(paths, file.Name())
        }
    }
    return paths
}

func ZenkakuToHnakaku(str string) string {
    // Replacerを使って置換する
    r := strings.NewReplacer("0", "0", "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", "6", "6", "7", "7", "8", "8", "9", "9")
    resStr := r.Replace(str)
    return resStr
}

結論

簡単にできた。

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

goumlでgoプロジェクトのUML図を出力する

goプロジェクトの依存をグラフとして可視化できるツールとして kisielk/godepgraphpaulbuis/golistdepgraph がありますが、その粒度は少し荒いものでした。(パッケージ間の依存を解析)

そこに颯爽と登場したのが kazukousen/gouml です。

goプロジェクトを解析してumlファイルを出力してくれる素晴らしいツールで、実際に出力してみた図はこのような感じになります。UMLですね〜

sqGdbHwwgeOaDUR1555466459_1555466479.png

以下でコマンドの実行し、画像として保存するところまでを紹介していきます。

インストール

go get -u github.com/kazukousen/gouml/cmd/gouml

goumlを実行しumlファイルを出力する

gouml init

対象のディレクトリを指定する場合は -d or --dir を指定します。

gouml init -d ./src/

出力するumlファイル名を指定する場合は -o or --out を指定します。

gouml init -o xxxx

umlファイルをpngファイルに変換する

いくつか方法がありそうですが、今回はplantumlを使います。

brew install plantuml
brew install graphviz
brew install gnu-sed
cat xxxx.uml | gsed  '1i @startuml' | gsed '$a @enduml' > xxxx.pu
plantuml -tpng xxxx.pu

ここでxxxx.pngという画像が生成されているはずです。ご確認ください。

注1: 出力されるpngファイルについて、デフォルトでは縦横ともに4096に制限されています。

画像にuml図の全体が収まっていないときは以下の環境変数のことを思い出してください。

echo 'export PLANTUML_LIMIT_SIZE=32768' >> ~/.bash_profile

注2: 現状では特定の条件下でグラフがちょっと悲しい感じになることがあります。
ものすごい勢いで開発されており、以下の問題も爆速で解消されました。

僕が遭遇したのは、string型のグローバル変数が宣言されている場合で、これに対処するパッチを一応投げて置きました
スクリーンショット 2019-04-17 12.54.47.png

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

Go 言語 context パッケージ誕生の背景と使用方法

この記事は 私のホームページ とのマルチポストです

Go 言語を使って何を書くかといえば、なんだかんだでサーバプログラムを書くことが多いかと思います。

大抵の場合、ハンドラは goroutine で動かすことになります。また、ハンドラの中で goroutine 動かすケースも多々あります。
さて、例えばハンドラ内で外部 API を呼び出したり、マイクロサービスな関連サーバへリクエストをかけたりする際に、
それぞれのリクエストで AccessToken やタイムアウトの規定時間など、共通の値を用いるケースは多々あるかと思います。
また、 goroutine で平行で動かしているリクエストがある時、1つが失敗したら残りのリクエストも全部失敗させたいときなどもあり得るでしょう。

context パッケージはそのような、リクエスト毎のデータを取り扱うために作られました。

この文書は context パッケージの使い方とやりたかったことを
より初心者向けに、バカバカしく、何もかも忘却した2ヶ月後の私自身でもわかるように context の使い方を説明した記事です。

準備編: HTTP サーバとハンドラを書いてみよう。

Go を使うと速くて軽い http サーバが簡単に書けるので、ナウでヤングなマイクロサービスを
やりたいときとかに気軽にサーバプログラムを書いたりします。

一番簡単な HTTP サーバと、HTTPサーバの動作検証方法

というわけで、 "簡単なサーバ" を書いて実行してみましょう。まずは main.go を用意します。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World.")
    })
    http.ListenAndServe(":3000", nil)
}

で、コンパイルして実行します。

$ go run main.go

実行できたらアクセスしてみましょう。

$ curl http://localhost:3000/
Hello, World.

ほらね。簡単に http サーバが書けたでしょう?

この記事では、このプログラムをベースとして様々な改造をしていきます。
その際、毎回 curl で動作検証をするのは面倒です。
ですので、go run main.go で HTTP レスポンス自体を検証できるように、
httptest を使ってリクエスト・レスポンスの検証をしやすくしておきます。

main.go
package main

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

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

func main() {
    server := httptest.NewServer(http.HandlerFunc(handleRoot))
    defer server.Close()

    res, _ := http.Get(server.URL)
    body, _ := ioutil.ReadAll(res.Body)
    fmt.Printf("Response: %s\n", body)
}

こうしておけば、以下のコマンドですぐに実行できますし...

$ go run main.go
Response: Hello, World.

手元に Go 言語の開発環境がなくても、Go Playground から 実行可能 です。

外部 API 呼び出しなどの非同期処理がある場合

さて、Go 言語の特徴の一つとして非同期処理を比較的簡単に書けるというものがあります。
ですので、例えばハンドラ内で外部APIを呼び出したりする場合には、 goroutine を使った非同期かがよく行われます。

具体的なイメージとしては、例えば、下記みたいに ntp 使って時刻を表示するサーバとか

main.go
import "github.com/beevik/ntp"

func handleRoot(w http.ResponseWriter, r *http.Request) {
    time, _ := ntp.Time("ntp.nict.jp")
    fmt.Fprintln(w, time)
}

後は、なんか重めな処理を goroutine 使ってバックエンドに流したりするすサーバとかもあるわけです。

main.go
func request(w io.Writer, text string, count int) {
    for i := 0; i < count; i++ {
        time.Sleep(500 * time.Millisecond)
        fmt.Fprintln(w, text)
    }
}

func handleRoot(w http.ResponseWriter, r *http.Request) {
    go request(w, "foo", 4)
    go request(w, "bar", 4)
    request(w, "baz", 5)
    go request(w, "qux", 4)
}

他にも、MySQL などのデータベースの読み書きや、それをラップした DAO, ORM などの処理。
ファイルの読み書きなどディスクIOが発生する何かなどは気軽に goroutine 化して行くことが数多くあるでしょう。

直接 goroutine 化しないとしても、ライブラリ内などでは積極的にされているかもしれません。
このようなとき、上記のような素直すぎるプログラムを書いたとき、真っ先に困るのが タイムアウト処理の実装 でしょう。

何をどう困るのかを実感するために、サンプルコードとして、ランダムで 0 - 2000 ミリ秒で String を返す request メソッドと、それを呼び出すハンドラを用意します。

main.go
// request は 0-2000ミリ秒ランダムで待機した上で、`kind` を返す
func request(kind string) string {
    time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
    return fmt.Sprintf("%s, ", kind)
}

// handleRoot は HTTP リクエストのハンドラで、foo, bar, baz, qux をランダムに返す
func handleRoot(w http.ResponseWriter, r *http.Request) {
    c := make(chan string)
    go func() { c <- request("foo") }()
    go func() { c <- request("bar") }()
    go func() { c <- request("baz") }()
    go func() { c <- request("qux") }()

    for i := 0; i < 4; i++ {
        res := <-c
        fmt.Fprint(w, res)
    }
}

func main() {
    server := httptest.NewServer(http.HandlerFunc(handleRoot))
    defer server.Close()

    rand.Seed(time.Now().UnixNano())

  // 一応、所要時間も記録しておく
    start := time.Now()
    res, _ := http.Get(server.URL)
    elapsed := time.Since(start)
    body, _ := ioutil.ReadAll(res.Body)
    fmt.Printf("Response: %s (%s)\n", body, elapsed)
  // main が即終了しないようにしておく
  time.Sleep(2000 * time.Millisecond)
}

console $ go run main.go
Response: bar, baz, foo, qux, (1.860987136s)

タイムアウト処理を実装しようとして、失敗してみる

シンプルにタイムアウトを実装するなら、c 以外にもう一つタイムアウト用のチャンネルを用意し、
どちらかが返ってきたら handleRoot を打ち切るように実装すれば ok なはずです。
より、処理を簡単にするため、下のコードではさらに all チャンネルと 4 つのリクエストを待つ goroutine を用意していきます。

  1. タイムアウトして、 "timeout" を返す
  2. 4 つの request を全て処理しきった結果を返す

のどちらかの挙動となるはずです。

main.go
func handleRoot(w http.ResponseWriter, r *http.Request) {
    c := make(chan string)
    go func() { c <- request("foo") }()
    go func() { c <- request("bar") }()
    go func() { c <- request("baz") }()
    go func() { c <- request("qux") }()

  // 全てのレスポンスを待機するチャンネルを作る
    all := make(chan string)
    go func() {
        var res string
        for i := 0; i < 4; i++ {
            res += <-c
        }
        all <- res
    }()

  // タイムアウトを1500ミリ秒後に設定する
    timeout := time.After(1500 * time.Millisecond)

    select {
    case response := <-all:
        fmt.Fprint(w, response)
    case <-timeout:
        fmt.Fprint(w, "timeout")
    }
    return
}

// request, main は略

さて、これでタイムアウトができた。
・・・と、思うでしょう?

例えば、タイムアウト時間を 100 ミリ秒とかに設定すると、期待通りに動かないことがすぐにわかるはずです。

func handleRoot(w http.ResponseWriter, r *http.Request) {
    c := make(chan string)
    go func() { c <- request("foo") }()
    go func() { c <- request("bar") }()
    go func() { c <- request("baz") }()
    go func() { c <- request("qux") }()

    all := make(chan string)
    go func() {
        var res string
        for i := 0; i < 4; i++ {
            res += <-c
            fmt.Printf("all: %s\n", res)
        }
        all <- res
    }()

    timeout := time.After(100 * time.Millisecond)

    select {
    case response := <-all:
        fmt.Fprint(w, response)
    case <-timeout:
        fmt.Fprint(w, "timeout")
    }
    return
}

// request, main は略
$ go run main.go
Response: timeout (103.853504ms)
all: bar,
all: bar, foo,
all: bar, foo, qux,

見ての通り、タイムアウトしたにも関わらず、all チャンネルを使う goroutine や、
request メソッドを呼び出す goroutine は生きていたままになっていることがわかるかと思います。

この問題は、 リクエスト全体を通して、処理が完了しているかを示すフラグやチャンネル があればなんとかなりそうです。

リクエスト毎の変数的な何か。または context が何を解決するか。

上記に出てきたような リクエスト全体を通して、処理が完了しているかを示すフラグやチャンネル
リクエスト毎に区切られた認証情報や処理の経過時間みたいな変数、リクエストを中止させる共通のインターフェースなどを用意したのが
context パッケージです。

  • golang でも気軽に CurrentUserId() 的メソッドで現在アクセス中のユーザIDが欲しい
  • とりあえずエラーを発生させて全ての処理を中止させたい

みたいな要求に気軽に応えられるようになります。

使い方はシンプルで、context を作って、下部のメソッドに渡して行くだけです! 簡単!
(ただし、下部のメソッドは goroutine を作る際に、ctx.Done() で処理が終了していないかチェックかけてね!)

func handleRoot(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(context.Background(), 2000*time.Millisecond)
    defer cancel()

    c := make(chan string)
    go func() { c <- request("foo") }()
    go func() { c <- request("bar") }()
    go func() { c <- request("baz") }()
    go func() { c <- request("qux") }()

    all := make(chan string)
    go func() {
        var res string
        for i := 0; i < 4; i++ {
            select {
            case r := <-c:
                res += r
                fmt.Printf("all: %s\n", res)
            case <-ctx.Done(): // リクエスト全体が完了しているなら、この goroutine を中止する。
                return
            }
        }
        all <- res
    }()

    select {
    case response := <-all:
        fmt.Fprint(w, response)
    case <-ctx.Done():
        fmt.Fprint(w, "timeout")
    }
    return
}

// request, main は略

Context パッケージはこう使うんだよ

さて、ここまでで context が生まれた背景を説明していきました。

context を使うことで、リクエストを処理する全体で、タイムアウトやキャンセルなどの処理や、リクエスト全体をまたがる変数を取り扱うことができるようになります。

ここからは具体的な使い方を見ていきましょう。

Context

Go Concurrency Patterns: Context - The Go Blog にも記載がありますが、Context のコアな構造は次の通りです。

// Context はキャンセルフラグやリクエスト毎の変数、リクエストのデッドラインなどを
// API 境界をまたいでアクセスするために使います。各メソッドは複数の goroutine から
// 同時にアクセス可能です。
type Context interface {
  // Context がキャンセルされたりタイムアウトしたりした時に close されるチャンネルを返す
  Done() <-chan struct{}

  // なんでこのコンテキストが中止されたのかを示す error オブジェクト。Done チャンネルが
  // close した後にセットされる
  Err() error

  // Deadline はこのコンテキストがキャンセルされる予定の time.Time を返す
  Deadline() (deadline time.Time, ok bool)

  // このコンテキストに関連づけられた変数
  Value(key interface{}) interface{}
}

Done 関数はこのリクエスト全体が完了またはキャンセルされた時にシグナルが渡されるチャンネルを返します。
このチャンネルが閉じるか、シグナルが来た場合は、リクエスト全体が終了したため、各 goroutine は直ちに終了する必要があります。
また、 Err 関数は、このリクエスト然たがキャンセルまたはエラーで終了した場合、そのエラーを返します。

子 goroutine をキャンセルさせる

Context にはキャンセルを実行するメソッドは定義されていませんでした。
これは、通常の場合、キャンセルシグナルを受け取る goroutine とキャンセルを実施する goroutine は異なるためです。

例えば、ある goroutine A があり、 A が新しく goroutine B を呼び出す場合で考えますと

  • A は B をキャンセルできます
  • B は A をキャンセルできません
  • A は A 自身をキャンセルできません。(return で抜けることはできるでしょう)
  • B は B 自身をキャンセルできません。(return で抜けることはできるでしょう)

と整理できます。
この場合、A は B にキャンセル可能なコンテキストを渡す必要があり、 context.WithCancel で作成可能です。

例を見せましょう。

まず、コンテキストを受け取り、キャンセル可能な無限ループを作ります。

// infLoop は無限ループを行います。渡された context が終了した際にはこの関数を抜けます。
func infLoop(ctx context.Context) {
    fmt.Println("start infLoop")
    for {
        select {
        case <-ctx.Done():
            fmt.Println("exit infLoop")
            return
        }
    }
}

main では context.Background で新しいコンテキストを作った上で、
この親コンテキストをもとにキャンセル可能な子コンテキストを作り、 infLoop に渡します。

func main() {
    rand.Seed(time.Now().UnixNano())

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    go infLoop(ctx)

後は、好きなタイミングで cancel() を呼び出せば、 infLoop は終了します。

    go infLoop(ctx)

  time.Sllep(1000 * time.Millisecond)
  cancel()
}

実行可能なプログラムの全体像は以下の通りです。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

// infLoop は無限ループを行います。渡された context が終了した際にはこの関数を抜けます。
func infLoop(ctx context.Context) {
    fmt.Println("start infLoop")
    for {
        select {
        case <-ctx.Done():
            fmt.Println("exit infLoop")
            return
        }
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    go infLoop(ctx)

  time.Sleep(1000 * time.Millisecond)
  fmt.Println("do cancel")
  cancel()

  // main が終了しないように sleep を挟んでおく
    time.Sleep(1000 * time.Millisecond)
}

実行結果は以下のようになります。

$ go run main.go
start infLoop
do cancel
exit infLoop

子 goroutine をタイムアウトさせる

ほぼ、上記のキャンセルと同様ではありますが、タイムアウト処理には context.WithDeadline
context.WithTimeout の二つが使えます。

WithDeadline は指定時刻に、WithTimeout は指定時間経過後にそれぞれキャンセルされます。

cancel() の呼び出しはどこに書くか?

ここまでのコード例では、 time.Sleep を多用したため、 cancel() を手動で呼び出していました。
しかし、大抵の場合、 foo 関数で子 goroutine を作りっぱなしにすることはなく、 foo が終了するタイミングで子 goroutine も後始末することが多いでしょう。
ですので、 defer を使って下記のように書くことが多いと思います。

func foo(ctx context.Context) {
  ctx, cancel := context.WithCancel(ctx)
  defer cancel()

  go func(ctx) {
    // ...
  }(ctx)
}

コンテキストで値を渡す

context.WithValue を使い、値を渡します。
重要なのは、ここで渡す値は、例えばリクエストの処理の間、ずっと受け渡したい値であり、gorouitne や関数のオプションに渡すべきではないということです。
(この関数を使うと便利に値が渡せてしまいますので、濫用に注意しましょう)

context.WithValue では、keyval のそれぞれに interface が使えます。

コンテキストの木構造

Context は親子関係を持っています。 WithCancelWithTimeout, WithValue などを呼び出すことで、どんどん子 Context ができていきます。
親の Context がキャンセルされた場合、子供や子孫のコンテキストに Done が送信されます。

では、最上位の根 Context はどうやって作れば良いのでしょうか??

context.Background がそれになります。

このコンテキストはキャンセルもできませんし、タイムアウト設定などもされていません。

まとめと Context を使う際のルールについて

context パッケージを使うと、リクエストの処理中の値を取り扱いや、リクエスト自体がキャンセルされた際の、子 goroutine の適切なキャンセルなどをとてもスッキリと記述できます。
・・・が、それにはいくつかルールが必要です。

Go Concurrency Patterns: Context - The Go Blog の最後に、Google 社内でのルールについて記されています。
まず、 context を用いる全ての関数で、最初の引数として context を受け取れるようにしています。
この時、 必ず第1引数 にしています(社内規約です)。
また、 context を用いた関数が goroutine を呼び出すなら、適切なキャンセル処理をする必要があります。
このルールに従うことで、謎の goroutin がサーバ上で残るなど変な不具合がぐっと減らせるはずでしょう。

サンプルコードは https://github.com/ayasuda/sandbox/tree/master/go_context においてあります。

参照

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

go doc の使い方・コメントを書いて、ちゃんと読む

この記事は 自前のホームページ とのマルチポストです

Go 言語ではソースコード中にコメントを書くことで自動的に API ドキュメントを生成する方法が最初からサポートされています。

Godoc: documenting Go code でも、

The Go project takes documentation seriously.

と書かれているほど、しっかりと練られた文書化ツールが揃っています。

とはいえ、良い書き方や読み方がわからないと、書く気が起きませんよね?
そんなわけで、本文書では文書生成用のドキュメントの書き方と、それぞれのドキュメントの読み方について記していきます。

godoc の動きの確認

まず、サンプルコードを用意しましょう。

$ mkdir -p $GOPATH/src/path/to/your/library
$ cd $GOPATH/src/path/to/youre/library
$ vim sample.go
sample.go
package sample

import "fmt"

func Foo(s string) string {
    return "Foo " + s + " ooF"
}

func PrintFoo(s string) {
    fmt.Println(Foo(s))
}

この段階で、すでに godoc コマンドからドキュメントを参照可能です

まずはブラウザから見てみましょう。-http オプションで、ドキュメントをみるための web サーバが起動できます。

$ godoc -http=:8080

コマンドを実行したら http://localhost:8080 にアクセスしてみましょう。まるで公式サイトのような Web ページが見られると思います。

ヘッダの「Packages」リンクから、標準ライブラリおよび、GOPATH に持ってきている全てのパッケージのドキュメント が確認できます。
もちろん、たった今作った sample.Foo のドキュメントもあります!

なお、この時の URL は http://localhost:600/pkg/path/to/your/library/ になります。わかりやすいですね!

簡単なコメントを書く

Godoc で読むためのコメントはとてもシンプルで、godoc がなくても読みたくなるようなコメントが書けるように意識して書くのが良いでしょう。Go Blog でも以下のように言及されています。

Godoc comments are just good comments, the sort you would want to read even if godoc didn't exist.

コメントは type, variable, constant, function, package に書くことができます。また、コメントには一つルールがあり、その要素名で始める必要があります。

sample.go
// sample パッケージはコメントの書き方と読み方の例を提供します。
package sample

import "fmt"

// Bar はとある変数です
var Bar = "some variable"

// Baz はとある定数です
const Baz = "some constant"

// Qux はとある構造体です
type Qux struct {
    // A には適当な文字列をどうぞ
    A string
    // A にも適当な文字列をどうぞ
    B string
}

// Foo はとりあえず前後に変な文字をくっけます
func Foo(s string) string {
    return "Foo " + s + " ooF"
}

// PrintFoo はとりあえず前後に変な文字をくっけます
func PrintFoo(s string) {
    fmt.Println(Foo(s))
}

こいつをブラウザで表示すると下記のようにいい感じに表示されます。

ブラウザ以外からドキュメントを読む

godoc のヘルプを見ればすぐにわかりますが、godoc package [name] でそのパッケージおよび各種項目のドキュメントがコマンドラインから読めます。

$ godoc fmt Println
func Println(a ...interface{}) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline
    is appended. It returns the number of bytes written and any write error
    encountered.

自作のライブラリ含めたサードパーティのライブラリのドキュメントを読む場合は、パッケージをフルパスで書きましょう。先に作ったサンプルの場合なら次のようになります。

godoc path/to/your/library PrintFoo
func PrintFoo(s string)
    PrintFoo はとりあえず前後に変な文字をくっけます

Q: パッケージ名の sample はどこに行ったのでしょうか?

Answer: 僕がライブラリの作り方ミスって、sample が消失しただけだよ! golang のライブラリの作り方はこの文書の範疇じゃないから仕方ないね!
(具体的には、パッケージ名とディレクトリ名は通常一致させるべき)

もっと詳しくドキュメントを書く

ここまでで日常使いには十分ですが、さらに詳しく、いろいろ書きたい人向けの機能にもふれておきましょう。

パッケージドキュメントを盛り盛りにする

専門知識が必要なパッケージを作りたいときなどに、プログラム本体のコメントとしてドキュメントを書くとちょっと長くなるケースがあると思います。
そういう時はパッケージドキュメント専用のファイルを作ることができます。

具体的には、 doc.go というファイルを用意し、そこにパッケージのドキュメントを記述します。

gob パッケージ では、この機能が使われており、ドキュメントは src/encoding/gob/doc.go に書かれています。

-notes オプションで追加項目をリストアップする

基本的にコード中や指定箇所以外のコメントは無視されますが、godoc のノート機能を使うと、より多くのメタ情報を知るせます。

デフォルトでは BUG(who) が定義されているので、例えば、下記のようなコメントを書くと、godoc で見たときにひとまとまりに表示されます。

// Foo はとりあえず前後に変な文字をくっけます
//
// BUG(ayasuda) ごめん、なんかバグある気がする
func Foo(s string) string {
    return "Foo " + s + " ooF"
}

ブラウザで見ると下記のようになります。

繰り返しになりますが、デフォルトでは BUG が定義されているので、コメント中で BUG(who) を記述可能です。-notes オプションでこの定義は自由に増やせます。

簡単なフォーマット

コメントを書く際に、以下のルールに従うとことで HTML 化した際のフォーマットを変更することができます。

  • 連続した行は一つの段落と見なされます
    • 段落を区切りたい場合は空白行を間に入れる必要があります
  • 英大文字で始まり、直前が句読点ではない単一行の段落は見出しになります
  • 字下げすると整形済みテキストになります
  • URL はリンクになります

そんなわけで、例えば、下記のようなコメントを書くと

/*
ここはパッケージコメントの最初になるから見出しではないよー

Hで始まり単一行かつ句読点なしかつ前が見出しではないのでこれは見出し

段落段落
段落段落
段落段落

次の段落
次の段落

    整形済みテキスt

次のやつはリンクになるはず。
https://golang.org/
*/
package sample

以下のようにHTML化されます。

これ以上にいろいろ装飾したい場合は、 最悪、ソースが ここ にあるので・・・

Example をつけるには

公式ドキュメントのいくつかの関数では、 Example が付いてます。
これは Testable Examples を利用して作られています。

上記の sample パッケージに対して Testable Example を作るなら、まずは /path/to/your/library/example_test.go を用意します。
パッケージ名は sample_test としてください。

中には次のルールでサンプルがかけます。

func ExampleFoo()     // Foo 関数のサンプルコードになります
func ExampleBar_Qux() // Bar 構造体の Qux 関数のサンプルコードになります
func Example()        // パッケージ全体のサンプルコードになります
func ExampleFoo_foo   // Foo 関数のサンプルコード(fooの場合)

また、テストコード中に Output: から始まるコメントを記載することで、標準出力に書き込まれた値をチェックすることができます。

そんなわけで、こんなコードを書きます。

/path/to/your/library/example_test.go
package sample_test

import (
    "fmt"
    "path/to/your/library"
)

func ExampleFoo() {
    fmt.Println(sample.Foo("foo"))
    // Output: Foo foo ooF
}

テストはいつも通り、go test コマンドから実行可能です。

$ go test
PASS
ok  path/to/your/library  0.004s

後は、ドキュメントを生成すると、Example が記載されているはずです。

参照

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

Golangでメソッド呼び出しによる部分適用

出来ないかなーと思いつつ遊んでいたら出来たのでメモ。

// 関数に名前付き型を定義する
type Add func(int, int) int

// 上で定義したAdd型に部分適用するメソッドを定義
func (f Add) apply(applyValue int) func(int) int {
    return func(value int) int {
        return f(value, applyValue)
    }
}

// Add型と同じ型を持つ関数を定義
func add(a, b int) int {
    return a + b
}

func main() {
    // 関数addがAdd型として扱えるように変数に格納する
    var f Add = add

    // Add型であるfからはapplyメソッドが呼び出せる
    f2 := f.apply(2)

    // 部分適用した関数f2は引数が一つで呼び出せるようになる
    fmt.Print(f2(1)) // 3
}

このような方法を取らなくても、以下のような部分適用する関数を定義すればいいだけなので無性にメソッド呼び出ししたくなった時に利用するといいでしょう。

func add(a, b int) int {
    return a + b
}

func apply(f func(int, int) int, applyValue int) f(int) int {
    return func(value int) int {
        return f(value, applyValue)
    }
}

func main() {
    add2 := apply(add, 2)
    fmt.Print(add2(1)) // 3
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む