生涯未熟

プログラミングをちょこちょこと。

mattnさんのリファクタリングを読み解く

現在絶賛開発中のkirimoriですが、なんとGolang界隈で有名なmattnさんにリファクタリングをして頂くという、とても嬉しい事態がありました✨

kirimoriについてはこちら↓

syossan.hateblo.jp

リファクタリング前提でかなり雑に書いていたのですが、めちゃくちゃ良い感じにコードを直して頂けたので自分の勉強のために読み解いてみます?

リファクタリング

kirimoriは以下の機能を有しています。

で、構成的には kirimori.go に全てのコマンドの処理をベタ書きにしてある感じになっております。

リファクタリング前のコード

リファクタリング

細かく読み解く前に、mattnさんによってリファクタリングされた完成形がこちらになります。

リファクタリング後のコード

かなり構成が変わってますね・・・如何にリファクタリング前のコードが酷かったのか窺い知れます?

では、読み解いていきましょう。

ポインタレシーバを使う

ASTのVisitor構造体に対してVisitメソッドを定義する際に以下のように値レシーバを用いていました。

func (v AddVundleVisitor) Visit(node ast.Node) (w ast.Visitor) {
    // 省略
}

ただし、値レシーバを用いた場合にはnilのポインタ変数でメソッドを呼び出すとpanicが発生しますが、ポインタレシーバの場合は発生しません。(構造体のフィールドをメソッド内で参照している場合はpanicが発生します)

なので、以下のようにポインタレシーバを使うようにリファクタリングされておりました。

func (v *AddVundleVisitor) Visit(node ast.Node) (w ast.Visitor) {
    // 省略
}

ここら辺の話は、この記事の説明が物凄く分かりやすかったです?

skatsuta.github.io

追記:mattnさんから訂正が!?

コマンド毎の処理をパッケージに分割する

リファクタリング前にはコマンド毎の処理は以下のように書いていました。

func makeApp() *cli.App {
    app := cli.NewApp()

    app.Name = "kirimori"
    app.Usage = "Add Vim Plugin Tool"
    app.Version = "1.0"

        app.Commands = []cli.Command{
        {
            Name:    "init",
            Aliases: []string{"i"},
            Usage:   "create setting file",
            Action: func(c *cli.Context) error {
                            // 40行ほどの処理
            },
        },
        {
            Name:    "add",
            Aliases: []string{"a"},
            Usage:   "add plugin",
            Action: func(c *cli.Context) error {
                            // 80行ほどの処理
            },
        },
        {
            Name:    "remove",
            Aliases: []string{"r"},
            Usage:   "remove plugin",
            Action: func(c *cli.Context) error {
                            // 70行ほどの処理
            },
        },
        {
            Name:    "list",
            Aliases: []string{"l"},
            Usage:   "list plugin",
            Action: func(c *cli.Context) error {
                            // 40行ほどの処理
            },
        },
    }

    return app
}

この処理を書きながら「さすがに可読性が悪いよなぁ・・・」と思ってましたが、この一連のコマンド処理をpackageに分割されてました。

func makeApp() *cli.App {
    app := cli.NewApp()

    app.Name = "kirimori"
    app.Usage = "Add Vim Plugin Tool"
    app.Version = "1.0"

    app.Commands = []cli.Command{
        {
            Name:    "init",
            Aliases: []string{"i"},
            Usage:   "create setting file",
            Action:  cmdInit,
        },
        {
            Name:    "add",
            Aliases: []string{"a"},
            Usage:   "add plugin",
            Action:  cmdAdd,
        },
        {
            Name:    "remove",
            Aliases: []string{"r"},
            Usage:   "remove plugin",
            Action:  cmdRemove,
        },
        {
            Name:    "list",
            Aliases: []string{"l"},
            Usage:   "list plugin",
            Action:  cmdList,
        },
    }

    return app
}

こっちの方が断然見やすいですね!

printlnからfmt.Printlnへ修正

これ全く知らなかったんですが、ビルトイン関数のprintやprintlnは書き込み先が標準エラー出力でfmtパッケージのPrintやPrintlnは標準出力なのですね・・・?

参考記事?

qiita.com

path/filepathの使用

設定ファイルのパスをこんな風に処理してました。

setting_file_path string = home_path + "/.kirimori.toml"

こうするよりも path/filepath のJoinを使った方がスマートでした?

settingFilePath string = filepath.Join(homePath, ".kirimori.toml")

Windows対応

完全にWindowsをガン無視で作っていましたが、リファクタリングWindows対応まで付けて頂きました・・・!?

runtimeパッケージを使って処理を分ければ良いという知見を学びました!

f runtime.GOOS == "windows" {
    vimrcName = "_vimrc"
} else {
    vimrcName = ".vimrc"
}

golintを使う

これまたlint系も後で良いやーって感じでやってたので、golintでの指摘点を大量に修正して頂いてました?

また、twitterのフォロワーさんにgometalinterというgoでのlintの集合体を扱うツールを教えてもらいました!

便利なので今後使っていきたいと思います!✨

github.com

プラグインマネージャー毎にパッケージに分割する

プラグインマネージャー毎の処理を scanAddLineForVundle みたいに For[プラグイン名] って感じでプラグイン毎に名称を分けてたんですが、こちらもパッケージにそれぞれ分けて頂けました?

これでまたkirimori.go本体がスッキリ!

引数をos.Fileからio.Readerに修正

NewScannerに引き渡すために各関数の引数をos.Fileにしていたのですが、io.Readerに修正されておりました。

これはテストの際にわざわざファイルを用意してOpenして、ってやるよりかio.Readerを生成して喰わせる方が楽だからなのかな?と思っております。(この辺自信が無いのでどなたかツッコミがあればツッコミ入れて下さい?)

変数名を短く

全体的に変数名を短く修正されており、以下の記事にあるようにGolangの文化なのかなと。

blog.sigbus.info

癖で長く変数名書いてるので、短くても伝わるような設計心掛けたいです✨

まとめ

というわけで、初心者Go使いには物凄く学びが多いPRでした!

わざわざ時間を割いてリファクタリングしていただいたmattnさんに多謝?

golangでScanが上手く動かなかったので原因調べてみた

どうも、今日も元気にGolang生活しております。

今回はbufioパッケージにあるScanの話をば。

問題の発生

とあるライブラリを使ってコードを書いていた時に、Scanを実行しても動かないという現象に遭遇しました。

コードは以下のような感じ

func main() {
    f, err := os.Open("hoge.txt")
    if err != nil {
        println("Err")
    }

    libraryFunc(f)

    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        println(scanner.Text())
    }
}

で、 hoge.txt の中身が出力されるのが予想される挙動だったんですが、出力されず・・・

どうしてだろうと、libraryFuncの中を読んでいくとこちらでもfを受け取ってScanを走らせていたことが分かり、「もしやScanって2回動かすとダメなのか?」と以下のようなコードを書いて調査してみました。

func main() {
    f, err := os.Open("hoge.txt")
    if err != nil {
        println("Err")
    }
    // 1回目
    err = file_scan(f)
    if err != nil {
        println("Err")
    }

    // 2回目
    err = file_scan(f)
    if err != nil {
        println("Err")
    }
}

func file_scan(file *os.File) error {
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    err := scanner.Err()
    return err
}

一旦、Scanを2回実行してみる形でコードを組んでみました。

で、これを実行してみてもやはり動かない。

原因は?

色々と調べてみてもあんまり理解を得なくて、結局Twitterで凄いエンジニアの方たちに調べてもらった結果、「ファイルのオフセットが回りきってる」という結論に。

というわけで、これまたコード書いて調べてみました。

func main() {
    f, err := os.Open("hoge.txt")
    if err != nil {
        println("Err")
    }
    ret, err := f.Seek(0, 1) //ファイルオフセットの現在位置を取得
    if err != nil {
       println("Err")
    }
    println(ret)
    // 1回目
    err = file_scan(f)
    if err != nil {
        println("Err")
    }
    ret, err := f.Seek(0, 1) //ファイルオフセットの現在位置を取得
    if err != nil {
       println("Err")
    }
    println(ret)
    // 2回目
    err = file_scan(f)
    if err != nil {
        println("Err")
    }
}

func file_scan(file *os.File) error {
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    err := scanner.Err()
    return err
}

実行してみると、「0 10」という結果が。

10行のファイルだったので、2回目実行前には既にファイルオフセットが最大の10になっていることが分かりますね。

対処法

対処としては、再度Scanを実行する前にファイルをオープンし直せば大丈夫でしょう。(何か他に良い方法を知っている方がおられましたらご教示下さい・・・)

[追記] もしくは2回目のScan実行前に f.Seek(0, 0) でファイルオフセットを0に戻してもいいかもしれません。

コードとしてはこんな感じ。

func main() {
    f, err := os.Open("hoge.txt")
    if err != nil {
        println("Err")
    }
    // 1回目
    err = file_scan(f)
    if err != nil {
        println("Err")
    }

    f, err = os.Open("hoge.txt")
    if err != nil {
        println("Err")
    }
    // 2回目
    err = file_scan(f)
    if err != nil {
        println("Err")
    }
}

func file_scan(file *os.File) error {
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    err := scanner.Err()
    return err
}

これで想定した挙動になりました!

思わぬところでハマりましたが、原因調べてハラオチできたので満足です。

教えてくださった方ありがとうございました!

webpackerを試してみた

Rails5.1からwebpackが標準装備されるということで、試しにRails5.0.1 + webpackerでやってみました。

インストール

Rails5.0.1でプロジェクトを作成し、Gemfileに gem 'webpacker', github:'rails/webpacker' としてmasterから取ってくる。(rails webpacker:install:react を使いたかったため)

bundle install なりでインスコ出来たら bundle exec rails webpacker:install で必要ファイルを生成する。

生成できたらbundle exec rails webpacker:install:reactとすることでReactのファイルが生成されたりします。

bundle exec rails webpacker:install:reactを実行した際に、Yarn executable was not detected in the system.と怒られる場合があります。

僕の場合、yarnのバージョンが古くてyarnpkgのエイリアスが入っていなかったので怒られていました。

webpacker/yarn.tt at master · rails/webpacker · GitHub

↑を見るとyarnpkgとして実行しているのが分かると思います。

なので、yarnのバージョンを上げてから実行しましょう。

これでサンプルのReactファイルが生成され、yarnも実行されるはずです。

また、webpackerのREADME.mdを雑に日本語にしましたので、気になる方は読んでみてください。

github.com

yarnpkgに関してのこのツイートが無ければ永久にエラーで止まったままでした・・・感謝?

kirimoriというコマンドラインツールを作ってみた

久々にGolangコマンドラインツール作りました。

その名もkirimori(切り盛り)。

github.com

何が出来るの?

Vimプラグインを追加・削除・一覧表示が出来ます。

例えば、

$ kirimori add Shougo/neocomplete

と実行することで、指定した .vimrc にVundle/NeoBundle/dein.vimのいずれか指定した形式で行を追加します。

Vundleの場合は以下の行が追記されます。

Bundle 'Shougo/neocomplete'

で、後は BundleInstall を実行するだけ。

要らなくなったら、

$ kirimori remove Shougo/neocomplete

で、行を削除。

一覧が見たい場合は、

$ kirimori list
Shougo/neocomplete
Shougo/unite.vim
Shougo/neosnippet.vim

という形で表示されます。

かなり大雑把にコード書いたので、PR・ISSUEなど大募集しております。

2017年の抱負

今年の三が日は食っちゃ寝しながらscalaを勉強してたしょっさんです。

去年も抱負を考えたんですが、今年も何か抱負を立てようかなと。

PMとしての経験値を溜める

去年は年末にちょびっとPMとしてお仕事しました。

自分以外のエンジニアを携えてPMをやるのは、ほぼ初めてだったので至らない所が多々あり「経験不足だなぁ」と我ながら感じました。

なので、今年も機会があればPMをやって経験値を溜めたいなと。

こればっかりは会社の都合もあるので、機会があるなら手を挙げていきたい。

キチンとしたOSSを開発する

今迄はちょっとしたツールの開発くらいでしたが、今年はキチンとした役立つOSSを開発したいなーと。

もっと俗な言い方をすればStarが欲しいです!

ISUCON再トライ

去年のISUCONでほぼ役に立たなかったので、今年こそはちゃんと役に立てるようになって再トライを!

ISUCON開催までにGolang力溜めておきます!

痩せる

めちゃくちゃ不摂生していたためにかなりのデブになってしまいました。

健康のためにもそろそろ標準体重に戻していきます。

英語勉強する

ゆるふわに英語読んだり書いたりしてましたが(Google翻訳使いつつ)、Google様の力を借りなくてもドキュメントはサラサラっと読めるくらいには勉強します。

こんな感じでザッと挙げましたが、これらを中心に頑張っていきます。

あと、色んなエンジニアさんと会ってみたいので興味がある方はお気軽にこちらtwitterアカウントにリプライを飛ばして頂けるとうれしいです。