Edited at

Goの"erning/gorun"パッケージの紹介。

More than 1 year has passed since last update.


"erning/gorun"パッケージって?

Github: https://github.com/erning/gorun

用途は非常にシンプルで、「GoのソースコードをPythonやRubyみたいに実行ファイルとして動かす。」と言う感じです。まさにアイディアの勝利という感じですね、Zhang Erning氏、素晴らしいと思います。


hello.go

#!/usr/bin/env gorun //goにgorunのシェバングを書けば完成。

package main

func main() {
println("Hello world!")
}


$ chmod +x hello.go

$ ./hello.go #「go run hello.go」じゃなくて実行しているだけ!!
Hello world!

このcloudflare社の記事で一躍有名になりましたね。

Using Go as a scripting language in Linux

さてみなさん、紹介なんてのはもう終わってしまうレベルにシンプルなパッケージだったわけですが、それだけじゃつまらないですよね。

「このパッケージって内部でどう言う動きをして実現しているの?」「ただ本体のgo run にフックしているだけなのか?」など動作が気になりませんか?そうでも無い?😢

いやそうは言わずに見てみましょうや😆ってこと残りはソースコードリーディングしていきます。


内部の動き

ソースコード:https://github.com/erning/gorun/blob/master/gorun.go

main関数から順に見て行きます。gorunはシェバングを1行目に突っ込んでるわけですから、実行ファイルとして実行したとしても内包されている動作としてはgorun hello.goとなんら変わりません。

ソースを見ても、ただ第一引数をRun関数に代入しているだけです。

func main() {

args := os.Args[1:]
...
err := Run(args)
...
}

続いてRun関数ではまず引数のソースファイルから実行用のバイナリを置くためのディレクトリもしくはテンポラリファイルをRunFile関数を使って作成しています。

func Run(args []string) error {

sourcefile := args[0]
rundir, runfile, err := RunFile(sourcefile)
...
}

func RunFile(sourcefile string) (rundir, runfile string, err error) {
rundir, err = RunDir()
if err != nil {
return "", "", err
}
sourcefile, err = filepath.Abs(sourcefile)
if err != nil {
return "", "", err
}
sourcefile, err = filepath.EvalSymlinks(sourcefile)
if err != nil {
return "", "", err
}
runfile = strings.Replace(sourcefile, "%", "%%", -1)
runfile = strings.Replace(runfile, string(filepath.Separator), "%", -1)
runfile = filepath.Join(rundir, runfile)
runfile += ".gorun"
return rundir, runfile, nil
}

その後ファイルがあるかどうか見たり、時間で差分見て変化無いか見たりなんやかんやしてエラー分岐した後に問題なければCompile関数に突っ込んで行きます。

func Run(args []string) error {

...
err := Compile(sourcefile, runfile)
...
}

ここが動きの本丸となっています。

見ればわかる通り、マジで「pidでフックしてgo buildでコンパイルして実行」ですね。

func Compile(sourcefile, runfile string) (err error) {

pid := strconv.Itoa(os.Getpid())

content, err := ioutil.ReadFile(sourcefile)
if len(content) > 2 && content[0] == '#' && content[1] == '!' {
content[0] = '/'
content[1] = '/'
sourcefile = runfile + "." + pid + ".go"
ioutil.WriteFile(sourcefile, content, 0600)
defer os.Remove(sourcefile)
}

gotool := filepath.Join(runtime.GOROOT(), "bin", "go")
if _, err := os.Stat(gotool); err != nil {
if gotool, err = exec.LookPath("go"); err != nil {
return errors.New("can't find go tool")
}
}

out := runfile + "." + pid
err = Exec([]string{gotool, "build", "-o", out, sourcefile})
if err != nil {
return err
}
return os.Rename(out, runfile)

func Exec(args []string) error {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
base := filepath.Base(args[0])
if err != nil {
return errors.New("failed to run " + base + ": " + err.Error())
}
return nil
}

そんなこんな、このパッケージ自体は「Go言語でgo runの動きをシェル上でオンデマンドに再現している」という動きになっているのでした。


どういったところで使っていけば良いか。

人それぞれだとは思いますが・・・・。


  • シェルスクリプトの代わりGoで書く時に、ビルド忘れを気にせず使いたい時。


  • 自作playground環境とか作る際に。


  • Goでメタプログラミングの生成ソースとして(多分これが本命)。


そんな所でしょうか。便利なので使って行きましょう。