2020/02/21


Go 言語は struct のレシーバがポインタの場合は実体であってもポインタの場合であっても呼び出せるので、もし struct が参照カウントに従い動作する様な場合は実体でコピーされてしまっては困る場合があります。例えば以下の様なインタフェースを考えます。

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

type foo struct {
    n int64
    q chan struct{}
}

func (f *foo) Add() {
    if atomic.AddInt64(&f.n, 1) == 1 {
        f.q = make(chan struct{})
    }
}

func (f *foo) Done() {
    if atomic.AddInt64(&f.n, -1) == 0 {
        f.q <- struct{}{}
    }
}

func (f *foo) Watch() {
    <-f.q
}

func main() {
    var f foo

    f.Add()
    f.Add()
    f.Add()
    go func() {
        fmt.Println("いーち!")
        time.Sleep(time.Second)
        f.Done()
        fmt.Println("にー!")
        time.Sleep(time.Second)
        f.Done()
        fmt.Println("さーん!")
        time.Sleep(time.Second)
        f.Done()
    }()

    f.Watch()
    fmt.Println("ダーッ!")
}

このコードは main の中だけで動く場合には機嫌良く動きます。次にこの処理を分散してみたい考えてみます。関数 doSomething1 と doSomething2 に foo を引数で渡します。

func doSomething1(f foo) {
    time.Sleep(2 * time.Second)
    fmt.Println("さーん!")
    time.Sleep(time.Second)
    f.Done()
}

func doSomething2(f foo) {
    fmt.Println("いーち!")
    time.Sleep(time.Second)
    f.Done()
    fmt.Println("にー!")
    time.Sleep(time.Second)
    f.Done()
}

func main() {
    var f foo

    f.Add()
    f.Add()
    f.Add()
    go doSomething1(f)
    go doSomething2(f)

    f.Watch()
    fmt.Println("ダーッ!")
}

この処理は一見うまく行きそうに見えます。しかし実行するとデッドロックが起きます。

いーち!
にー!
さーん!
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.(*foo).Watch(...)
    C:/Users/mattn/go/src/github.com/mattn/misc/inoki_app/main.go:27
main.main()
    C:/Users/mattn/go/src/github.com/mattn/misc/inoki_app/main.go:55 +0xfd

「しっかり atomic.AddInt64 を使っているのにおかしい」と思うかもしれません。しかし実際は doSomething1 や doSomething2 の引数として foo の実体を渡した際にはコピーが発生してしまいます。参照カウンタである foo.n は両方の関数に 3 が渡り、foo.n が 0 になる事はありません。もちろんこれは引数をポインタにする事で回避できます。

func doSomething1(f *foo) {
    time.Sleep(2 * time.Second)
    fmt.Println("さーん!")
    time.Sleep(time.Second)
    f.Done()
}

func doSomething2(f *foo) {
    fmt.Println("いーち!")
    time.Sleep(time.Second)
    f.Done()
    fmt.Println("にー!")
    time.Sleep(time.Second)
    f.Done()
}

func main() {
    var f foo

    f.Add()
    f.Add()
    f.Add()
    go doSomething1(&f)
    go doSomething2(&f)

    f.Watch()
    fmt.Println("ダーッ!")
}

こういった struct をライブラリとして提供したい場合、使い手側に「ポインタで使って欲しい」と示す事ができないと、いくらでもバグが発生してしまいます。そこで使うテクニックが noCopy です。Go 言語を知っていてここまで読んだ方であれば、これが何かに似ていると気付いたはずです。そう sync.WaitGroup です。sync.WaitGroup も実体で引数に渡すとデッドロックが発生します。sync.WaitGroup の場合は以下のテクニックを使っています。

type WaitGroup struct {
    noCopy noCopy

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    // 64-bit atomic operations require 64-bit alignment, but 32-bit
    // compilers do not ensure it. So we allocate 12 bytes and then use
    // the aligned 8 bytes in them as state, and the other 4 as storage
    // for the sema.
    state1 [3]uint32
}

type noCopy struct{}
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

go vet は Go 言語でのお作法の良くない書き方を検出してくれるツールですが、この Lock() と Unlock() を持ったインタフェースを実体でコピーしようとすると go vet の copylocks というチェック機能により警告がでる仕組みになっています。

# github.com/mattn/misc/inoki_app
.\main.go:5:21: doSomething passes lock by value: sync.WaitGroup contains sync.noCopy
.\main.go:11:14: call of doSomething copies lock value: sync.WaitGroup contains sync.noCopy

実際に組み込んでみましょう。

package inoki

import (
    "sync/atomic"
)

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

type Toukon struct {
    noCopy noCopy

    n int64
    q chan struct{}
}

func (f *Toukon) Add() {
    if atomic.AddInt64(&f.n, 1) == 1 {
        f.q = make(chan struct{})
    }
}

func (f *Toukon) Done() {
    if atomic.AddInt64(&f.n, -1) == 0 {
        f.q <- struct{}{}
    }
}

func (f *Toukon) Watch() {
    <-f.q
}

言語仕様上、禁止する事はできないのでコンパイルは出来てしまいますが、go vet を使う IDE 等ではちゃんと警告がでる様になっています。

ダー!

便利なテクニックなので使ってみてみるといいと思います。

改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 Kindle版 / ¥2,278 (2019年08月01日)
 
発送可能時間:

Posted at by



2020/02/10


vim-jp の Slack で「zsh の PATH 環境変数に相対パスを含んでいる場合、補完ができないけど意図的か」という話題が出たので調べてみた。

補完できない様にしているのはこの変更

39104: do not hash relative paths in findcmd() · zsh-users/zsh@b312abc
https://github.com/zsh-users/zsh/commit/b312abc93b3b8eae8feb4a9884b22f519a137c7f

結構古い変更。この変更が行われた理由を追ってみた所、メーリングリストでこの会話が見つかった。

Running 'type' causes false positive hashed command completion

Zsh Mailing List Archive Messages sorted by: Reverse Date , Date , Thread , Author Running 'type' ca...

http://www.zsh.org/mla/workers/2016/msg01583.html
$ zsh -f
% cd $(mtemp -d)
% touch sudofoo; chmod +x $_
% ./sudo<TAB>
<becomes>
% ./sudofoo <^C>
% type -w ./sudo
./sudo: none
% ./sudo<TAB>
./sudo    sudofoo*

That's wrong because ./sudo does not exist.  However, it's hashed:

% print $commands[./sudo]
/usr/bin/./sudo

To confuse matters further, even though "./sudo" is hashed, a subsequent
'type -w ./sudo' will print "none", because the hash node lacks the
HASHED bit in its .flags and the PATH_DIRS option is unset by default.

/usr/bin 対しては ./sudo が存在する為、sudofoo に対する ./sudo からの補完候補に sudo が出てきてしまう、これは混乱を生んでしまう」という物だった。これを回避する為に上記の変更で相対パスはハッシュしない様にしている。ちなみに bash や fish だと相対パスの中のコマンドも補完された。

相対パス上で sudofoo の一部 sudo が補完されてしまったとして困るのは、その相対パス内に危ないファイルを追加してしまった場合だろうと推測するが、そもそも相対パスを PATH に追加したい要件が僕には見つからなかった。おそらく自動的に node_modules/.bin 内のコマンドを扱える様にしたいといった物だと思う。もし zsh でやりたい人は direnv を使って動的に PATH を追加するのが良いと思う。

Posted at by



2020/01/20


昨年から Oracle Cloud の無料枠を使っています。2 vCPU な VM を2台も無料で使わせて頂けるという Oracle Cloud さんの大盤振る舞いに感謝しつつ、Oracle Cloud Function でしりとりをしてみました。

Oracle Cloud Function は Fn Project というサーバレスプラットフォームをベースにしており、同プロジェクトの fn というツールを使う事で、他の Fn Project を使うクラウドと同様に操作を行う事ができます。
Fn Project

Open Source. Container-native. Serverless platform.

https://fnproject.io/

fn コマンドは以下の手順でインストールする事ができます。

curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh

MacOS の場合は homebrew からインストールが可能。

brew update && brew install fn

Windows の場合は僕が送っている pull-request を使わないとエラーになります。ソースを git clone して go build して下さい。

Fix atomicwrite for Windows by mattn - Pull Request #605 - fnproject/cli
https://github.com/fnproject/cli/pull/605

fn コマンドを使える様になるまでは sugimount さんが Qiita に書いている記事を参考にしました。

サーバレスな Oracle Functions (Fn) をやってみた - Qiita
https://qiita.com/sugimount/items/018e08f575ecefb1546c

注意点としては、Oracle Cloud はデフォルトで VCN が作られていますが、Function を使う場合は別途 VCN を作らないと DHCP オプションが一致しないというエラーが出てしまいます。新しく作って下さい。あと sugimount さんが書かれているリポジトリ名と異なる物が実際には用意されるので、詳細は Oracle Cloud のダッシュボードからファンクションを選択し、アプリケーション詳細の「開始」タブを見るとほぼやるべき事が書いてあります。

Oracle Cloud

アプリケーションを作る所まで出来たら、しりとりサーバを作ります。短いのでコード全体を載せます。

package main

import (
    "bufio"
    "context"
    "encoding/json"
    "errors"
    "io"
    "math/rand"
    "strings"

    _ "func/statik"

    fdk "github.com/fnproject/fdk-go"
    "github.com/rakyll/statik/fs"
)

var upper = strings.NewReplacer(
    "ぁ""あ",
    "ぃ""い",
    "ぅ""う",
    "ぇ""え",
    "ぉ""お",
    "ゃ""や",
    "ゅ""ゆ",
    "ょ""よ",
)

func kana2hira(s stringstring {
    return strings.Map(func(r runerune {
        if 0x30A1 <= r && r <= 0x30F6 {
            return r - 0x0060
        }
        return r
    }, s)
}

func hira2kana(s stringstring {
    return strings.Map(func(r runerune {
        if 0x3041 <= r && r <= 0x3096 {
            return r + 0x0060
        }
        return r
    }, s)
}

func search(text string) (stringerror) {
    rs := []rune(text)
    r := rs[len(rs)-1]

    statikFS, err := fs.New()
    if err != nil {
        return "", err
    }
    f, err := statikFS.Open("/dict.txt")
    if err != nil {
        return "", err
    }
    defer f.Close()
    buf := bufio.NewReader(f)

    words := []string{}
    for {
        b, _, err := buf.ReadLine()
        if err != nil {
            break
        }
        line := string(b)
        if ([]rune(line))[0] == r {
            words = append(words, line)
        }
    }
    if len(words) == 0 {
        return "", errors.New("empty dictionary")
    }
    return words[rand.Int()%len(words)], nil
}

func shiritori(text string) (stringerror) {
    text = strings.Replace(text, "ー""", -1)
    if rand.Int()%2 == 0 {
        text = hira2kana(text)
    } else {
        text = kana2hira(text)
    }
    return search(text)
}

func handleText(text string) (stringerror) {
    rs := []rune(strings.TrimSpace(text))
    if len(rs) == 0 {
        return "", errors.New("なんやねん")
    }
    if rs[len(rs)-1] == 'ん' || rs[len(rs)-1] == 'ン' {
        return "", errors.New("出直して来い")
    }
    s, err := shiritori(text)
    if err != nil {
        return "", err
    }
    if s == "" {
        return "", errors.New("わかりません")
    }
    rs = []rune(s)
    if rs[len(rs)-1] == 'ん' || rs[len(rs)-1] == 'ン' {
        s += "\nあっ..."
    }
    return s, nil
}

func main() {
    fdk.Handle(fdk.HandlerFunc(myHandler))
}

type Siritori struct {
    Word string `json:"word"`
}

func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
    var s Siritori
    json.NewDecoder(in).Decode(&s)
    var err error
    s.Word, err = handleText(s.Word)
    if err != nil {
        s.Word = err.Error()
    }
    json.NewEncoder(out).Encode(&s)
}

辞書ファイルは statik を使ってバイナリに埋め込みました。ソースコードは GitHub に置いておきます。

mattn/oracle-cloud-function-siritori
https://github.com/mattn/oracle-cloud-function-siritori/

デプロイは以下の手順で行います。

$ fn  --verbose deploy --app [your-app]

デプロイが完了すると標準入力で JSON を受け取り、標準出力で JSON を出力するコマンドが動く様になっています。

Oracle Cloud

VCN を作らないといけない事に気付くまで結構時間を使ってしまったけど、動く事が分かってからは結構サクサク操作できる様になりました。これだけ遊んでもまだ無料範囲内らしいので、もう少し遊んでみたいと思います。

Oracle Cloud Pocket Solutions Guide (English Edition) Oracle Cloud Pocket Solutions Guide (English Edition)
Kim, Charles, Ward, Jerry, Balasubramanian, Sudhir, Vengurlekar, Nitin
Kindle版 / ¥221 (2016年11月23日)
 
発送可能時間:

Posted at by