Go言語:ファイル入出力

環境

・macOS Big Sur ver. 12.6 (21G115)
・MacBook Air M1, 2020 メモリ 8GB
・go1.18.3 darwin/arm64


目標

main.goを実行したら指定したテキストファイルを初期化(白紙化)し,ある関数(play)を実行するとそのテキストファイルに文字列を書き込み,webassembly.goでそのテキストファイルを読み込む.

ファイル構成

├── ast
├── docs
│   ├── build.wasm
│   ├── index.html
│   ├── melody.txt
│   └── wasm_exec.js
├── evaluator
├── lexer
├── object
├── parser
├── repl
├── server
├── token
├── wasm
│   └── webassembly.go
├── go.mod
├── go.sum
└── main.go


ファイル入出力

Go 言語は「ファイルディスクプリタ」というデータを介してファイルにアクセスします。ファイルディスクプリタはファイルと一対一に対応していて、ファイルからデータを入力する場合は、ファイルディスクプリタを経由してデータが渡されます。逆に、ファイルへデータを出力するときも、ファイルディスクプリタを経由して行われます。

ファイルディスクプリタはパッケージ os に定義されている構造体 File に格納されてユーザーに渡されます。ただし、Go 言語の File には低レベルな処理 (メソッド) しか用意されていません。Go 言語の場合、パッケージ bufio に高レベルな処理を行うための構造体 Reader や Writer が用意されているので、File からそれらの型を生成して入出力処理を行います。

http://www.nct9.ne.jp/m_hiroi/golang/abcgo11.html


ファイルをオープンするには関数 os.Open と os.Create を使う.

func Open(name string) (file *File, err error)
func Create(name string) (file *File, err error)

Open は引数 name で指定されたファイルをリードモードでオープンする.Create はライトモードでオープンする.正常にファイルをオープンできた場合,返り値は File 構造体へのポインタ *File と nil となる.エラーが発生した場合は err に nil 以外の値がセットされる.リードモードの場合,ファイルが存在しないとエラーになる.ライトモードの場合,ファイルが存在すれば、そのファイルのサイズを 0 に切り詰めてからオープンする.

初期化の部分についてはCreateを使えば自動で解決するかもしれない.
ただし,文字列を書き込む実際のファイルはbuiltins.goなので,書き込みできる関数がCreateだけだと実行した関数で毎回書き込むごとに初期化されてしまってこれまでのものが蓄積されないので困る.

オープンしたファイルは必ずクローズする.それを行うメソッドがos.Close

func (f *File) Close() error

正常にファイルをクローズした場合,返り値は nil になる.

 Closeの手順を踏まないでファイルにデータを書き込むものもある.それがioutil.WriteFileだ.第一引数にファイルのパス,第二引数に書き込む文字をバイト化したもの,第三引数はファイルのパーミッションである.

WriteFileの使用例

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    data := "hello world"

    err := ioutil.WriteFile("hello.txt", []byte(data), 0664)
    if err != nil {
        fmt.Println(err)
    }
}

go1.16からioutilは非推奨になったのでos.WriteFileを使う方が良いかもしれない.

package main

import (
	"os"
)

func main() {
	hoge := "hogeeeee"
	os.WriteFile("sample3.txt", []byte(hoge), 0664)
}

ただし,この方法でも,実行するたびにテキストファイルに書いてあった元の内容は切り捨てられてしまう.

Appendモードのファイルはos.OpenFileを使ってフラグ指定をしながらファイルを開くことで行うことができる.

f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    panic(err)
}

defer f.Close()


main.go

package main

import(
	"fmt"
	"os"
	"os/user"
	"monkey/repl"
)

func main(){
	
	output, _ := os.Create("docs/melody.txt")
	output.Close()
	

	user,err := user.Current()
	if err != nil{
		panic(err)
	}
    
	fmt.Printf("Hello %s. This is The Monkey Programming language.\n", user.Username)
	fmt.Printf("Feel free to type in commands\n")
	repl.Start(os.Stdin, os.Stdout)
}

上記のようにした.

	output, _ := os.Create("docs/melody.txt")
	output.Close()

を追加しただけで,これによりmelody.txtはgo run main.goをするたびに白紙にすることができる.


builtins.go(playの箇所だけ)

package evaluator

import (
	"monkey/object"
	"fmt"
	"os"
)

var builtins = map[string]*object.Builtin{
	"play": &object.Builtin{
		Fn: func(args ...object.Object) object.Object{
			if len(args) != 2{
				return newError("wrong number of arguments. got=%d, want=2", len(args))
			}
			switch args[1].(type){
			case *object.Float:
				switch arg1 := args[0].(type){
				case *object.Integer:
					//ここでファイルの書き込み
					f, err := os.OpenFile("docs/melody.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
					if err != nil {
    					panic(err)
					}
					str := "play(" + args[0].Inspect() + ", " + args[1].Inspect() + ")\n"
					data := []byte(str)
					count, err := f.Write(data)
					if err != nil {
    					fmt.Println(err)
	    				fmt.Println("fail to write file")
					}
					defer f.Close()
					fmt.Printf("write %d bytes\n", count)
					return &object.Integer{Value: arg1.Value}
				default:
					return newError("1st argument to `play` must be int, got %s", args[0].Type())
				}
			default:
				return newError("2nd argument to `play` must be float64, got %s", args[1].Type())
			}
		},
	},
}

この状態でmonkeyのインタプリタを起動すると以下のようなコマンドでつg気の結果を得られる.(loopとwhile文は独自に拡張してあるもの)
あくまでもplay()が実行される場所はmain.goの場所なので,ファイルのパス指定を,"../wasm/melody.txt"のようにする必要はない.


MacBook-Air halo % go run main.go
Hello sonoyamayuto. This is The Monkey Programming language.
Feel free to type in commands
>> play(50, 0.2);
write 19 bytes
50
>> play(60, 0.3);
write 19 bytes
60
>> let i = 0; 
>> while(i< 3){play(51+i, 0.2); let i = i + 1;}
write 19 bytes
write 19 bytes
write 19 bytes
>> loop(4){play(45, 0.4);}
write 19 bytes
write 19 bytes
write 19 bytes
write 19 bytes
res: 45
45

melody.txt

play(50, 0.200000)
play(60, 0.300000)
play(51, 0.200000)
play(52, 0.200000)
play(53, 0.200000)
play(45, 0.400000)
play(45, 0.400000)
play(45, 0.400000)
play(45, 0.400000)

この後,main.goを終了させ,もう一度起動するときちんとmelody.txtの中身は白紙になる.


ブラウザ上でローカルファイルの読み込み

ここで必要になってくるのはローカルにあるmelody.txtを読み込むことだ.

JavaScriptでファイルを読み込むメジャーな方法は3つ.

  • 「input type="file"」フォームからユーザーがファイルをアップロードしようとするところをインターセプトする方法

  • staticファイルサーブコンテンツに読みたいファイルを含めて置き、それをfetchで取得する方法

  • ブラウザ拡張やFileSystemAccess-API(Chrome系 Only)を利用する方法

Chromeを拡張してやろうとしたけど,無理だった,次の記事にまとめる.

JavaScriptのfetchなどの非同期挙動を使って実現を考えることにする.

フェッチ API は、リクエストやレスポンスといったプロトコルを操作する要素にアクセスするための JavaScript インターフェイスです。グローバルの fetch() メソッドも提供しており、簡単で論理的な方法で、非同期にネットワーク越しでリソースを取得することができます。

従来、このような機能は XMLHttpRequest を使用して実現されてきました。フェッチはそれのより良い代替となるもので、サービスワーカーのような他の技術から簡単に利用することができます。フェッチは CORS や HTTP への拡張のような HTTP に関連する概念をまとめて定義する場所でもあります。

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch

server.go

静的ファイルサーブは既にこのserver.goで行なっている.

http.Handle("/", http.FileServer(http.Dir("../docs/")))

docsにmelody.txtを置いてある現段階で既に配置自体は行えた.
一応,他のやり方で静的ファイルサーブが行えたことを確認しやすいと思ったサイトを載せておく.


webassembly.go

package main

import (
	"math"
	"syscall/js"
	"log"
)

var (
	window   = js.Global()
	document = window.Get("document")

	AudioContext   = js.Global().Get("AudioContext")
	OscillatorNode = js.Global().Get("OscillatorNode")
	GainNode       = js.Global().Get("GainNode")
	fetch    = js.Global().Get("fetch")

	note2freq = []float64{}
)

func init() {
	for i := 0; i < 128; i++ {
		note2freq = append(note2freq, 440*math.Pow(2, float64(i-69)/12))
	}
}

func load(fn string) string {
	ch := make(chan string, 1)
	success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		log.Println("success")
		var received js.Func
		received = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
			defer received.Release()
			log.Println("received")
			ch <- args[0].String()
			return nil
		})
		args[0].Call("text").Call("then", received)
		return nil
	})
	defer success.Release()
	failed := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		log.Println("failed")
		ch <- ""
		return nil
	})
	defer failed.Release()
	go fetch.Invoke(fn).Call("then", success).Call("catch", failed)
	return <-ch
}

type Note struct {
	Number   int
	Duration float64
}

type MusicBox struct {
	ctx     js.Value
	osc     js.Value
	gain    js.Value
	current float64
}

func NewMusicBox() *MusicBox {
	ctx := AudioContext.New()
	osc := OscillatorNode.New(ctx)
	gain := GainNode.New(ctx)
	osc.Call("connect", gain)
	osc.Get("frequency").Set("value", 0)
	gain.Call("connect", ctx.Get("destination"))
	gain.Get("gain").Set("value", 0)
	osc.Call("start")
	return &MusicBox{
		ctx:     ctx,
		osc:     osc,
		gain:    gain,
		current: ctx.Get("currentTime").Float(),
	}
}

func (mb *MusicBox) Play(note Note) {
	mb.osc.Get("frequency").Call("setValueAtTime", note2freq[note.Number], mb.current)
	mb.gain.Get("gain").Call("setValueAtTime", 0.3, mb.current)
	mb.gain.Get("gain").Call("setValueAtTime", 0.0, mb.current+note.Duration)
	mb.current += note.Duration
}

const sample = ``

func main() {
	code := document.Call("createElement", "textarea")
	code.Set("id", "code")
	code.Set("value", sample)
	code.Get("style").Set("width", "50%")
	code.Get("style").Set("height", "20em")
	document.Get("body").Call("appendChild", code)
	btn := document.Call("createElement", "button")
	btn.Set("textContent", "music start!")
	btn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} {
		//ファイルの読み込み
		go func() {
			log.Println(load("melody.txt"))
			code.Set("value", load("melody.txt"))
		}()

		mb := NewMusicBox()
		window.Set("play", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
			mb.Play(Note{args[0].Int(), args[1].Float()})
			return nil
		}))
		code := document.Call("getElementById", "code")
		src := code.Get("value")
		window.Call("eval", src)
		return nil
	}))
	document.Get("body").Call("appendChild", btn)
	select {}
}

追加した部分を以下に記す.


import "log"
 
var fetch    = js.Global().Get("fetch")

func load(fn string) string {
	ch := make(chan string, 1)
	success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		log.Println("success")
		var received js.Func
		received = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
			defer received.Release()
			log.Println("received")
			ch <- args[0].String()
			return nil
		})
		args[0].Call("text").Call("then", received)
		return nil
	})
	defer success.Release()
	failed := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		log.Println("failed")
		ch <- ""
		return nil
	})
	defer failed.Release()
	go fetch.Invoke(fn).Call("then", success).Call("catch", failed)
	return <-ch
}


//main関数, btn.Call内
		//ファイルの読み込み
		go func() {
			log.Println(load("melody.txt"))
			code.Set("value", load("melody.txt"))
		}()


現時点でブラウザでボタンを押すと次のようになる.

ボタンを押すと音もなる.melody.txtの内容の変更もボタンを押すと反映してくれる(キャッシュなどの影響かわからないが,たまにされない時もある).

とりあえずは目標達成とする.

ちなみに,

この記事で書いたテストケースはファイルの読み込みに対応させていないので,今後このままテストを行うと失敗してしまう.
動くのを確認したらplayのテストケースはコメントアウトしておくのがいいかもしれない.

この記事が気に入ったらサポートをしてみませんか?