【Go言語】簡単なLinuxコマンド実装で学ぶioパッケージ

この記事は eureka Advent Calendar 2018 4日目の記事です。

こんにちは!eurekaのAPIチームでエンジニアをやっているrikiiです。

最近ついにAPIチームでモブプロを始めました。前は設計や実装について一人で悩んでたりした部分が、すぐ議論できたりホワイトボードに図で書いて理解を深めたりして、問題が素早く解決できてすごくいい感じで進んでいます。

はじめに

初級者向けです。
Go言語にはOSレベルの低レイヤーを扱うにあたって非常に便利な io.Writerio.Reader というインターフェースがioパッケージの内部に定義されています。

今回はこのインターフェースを用いてLinuxコマンドを実装してみることで、利便性を紹介しようと思います。

io.Writer, io.Readerとは?

以下のようにどちらもインターフェース型で定義されています。

type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}

どちらもbyte型を引数にして、int型(size)とerror型を返しています。このインターフェースを満たしていればどんな型でもio.Writer, io.Readerとして扱うことができます。

copyコマンドを実装してみる

お馴染みのLinuxのcopyコマンドをGoで実装してみたいと思います。

cp test.txt copy.txt -> go run xxx.go test.txt copy.txt でコピー可能です。

package main
import (
"io"
"os"
)
func main() {
originName := os.Args[1]
copyName := os.Args[2]
   f, err := os.Open(originName)
defer f.Close()
   if err != nil {
panic(err)
}
   cf, err := os.Create(copyName)
if err != nil {
panic(err)
}
byteFile, _ := ioutil.ReadAll(f)
cf.Write(byteFile)
}

まずはファイルの名前の引数を os.Args で受け取ります。

次に os.Open でコピー元のファイル名を渡し os.File型で受け取り、コピー先のファイル名を os.Createに渡し os.File型で受け取ります。

ioutil.ReadAll() という関数が出てきました。

ioutil.ReadAll() は、引数に取った File 型のファイルのすべてのデータを []byte で返してくれます。

最後に、 cf, err := os.Create(copyName) で作成した File に対してWriteメソッドを呼ぶことで実現できます

これで、渡されたファイルが画像であろうが文字列であろうがコピーが完了します。

また io.Copy という関数もあり、こちらを使えば以下のように1行でできます。

io.Copy(cf, f) // io.Writerとio.Readerが引数

割と簡単に実装できました。

もちろん、 os.File 型の Write メソッドが、内部では最終的には writeシステムコールが実行したりと複雑な処理を実行しています。

ここまでは何が便利なのかよくわからないと思いますので次にいきます。

zipコマンドを実装してみる

次はzipコマンド を実装してみます。

zip test.zip test.png -> go run xxx.go test.zip test.png

package main
import (
"archive/zip"
"io"
"os"
)
func main() {
zipName := os.Args[1]
fileName := os.Args[2]
   f, err := os.Create(zipName)
if err != nil {
panic(err)
}
defer f.Close()
   cpFile, err := os.Open(fileName)
if err != nil {
panic(err)
}
defer cpFile.Close()
   zipWriter := zip.NewWriter(f)
writer, err := zipWriter.Create(fileName)
if err != nil {
panic(err)
}
defer zipWriter.Close()
io.Copy(writer, cpFile)
}

先程のCopyコマンドとほぼ同じような実装になっていることがわかるかと思います。

違いがあるとしたら、今回は io.Copy に渡している Writeros.File 型ではなく、 zip.Writer 型になっていることぐらいです。

io.Copy の実装を少しみてみます。

func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}

引数が io.Writerio.Reader になっています。

つまり、下記のメソッドを持つ構造体であればなんでも使用可能です。

type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}

copy コマンドの場合は os.File 型。

zip コマンドの場合は zip.Writer 型で異なる型ですが、

io.Writerio.Reader のInterfaceを満たしているためどちらも使用できるというメリットが生まれました。

このように WriterReader の型が異なっていても、 io.Writerio.Reader を使えばとてもシンプルな実装で様々な読み書きが実現が可能となります。

zip番外編

今回の趣旨とは関係ないですが、zipコマンド実装してて、少し本家っぽい感じにしたいなーと思い、 recursive オプションだけ対応してみました。

エラー処理などかなり雑なところ多いですが、ご容赦ください。。。

オプション引数を処理する場合、flagパッケージを使うと簡単に作れて便利です。

package main
import (
"archive/zip"
"flag"
"io"
"io/ioutil"
"os"
)
const ExitOK = 0
func main() {
os.Exit(Run(os.Args))
}
func Run(args []string) int {
fileName := args[1]
copyFileName := args[2]
var (
r = flag.Bool("bool", false, "recurse into directories")
)
   flag.Parse()
   cpFileInfo, err := os.Stat(copyFileName)
if err != nil {
panic(err)
}
   f, err := os.Create(fileName)
if err != nil {
panic(err)
}
zipWriter := zip.NewWriter(f)
defer zipWriter.Close()
   recursiveOptFunc := func(name string) int {
files, err := ioutil.ReadDir(copyFileName)
if err != nil {
panic(err)
}
for _, f := range files {
writer, err := zipWriter.Create(f.Name())
if err != nil {
panic(err)
}
f, err := os.Open(copyFileName + "/" + f.Name())
defer f.Close()
if err != nil {
panic(err)
}
io.Copy(writer, f)
}
return ExitOK
}
   if *r {
return recursiveOptFunc(copyFileName)
}
   // ディレクトリの場合、オプションが無くても実行
if cpFileInfo.IsDir() {
return recursiveOptFunc(copyFileName)
}
cpFile, err := os.Open(copyFileName)
defer cpFile.Close()
if err != nil {
panic(err)
}
writer, err := zipWriter.Create(copyFileName)
if err != nil {
panic(err)
}
io.Copy(writer, cpFile)
return ExitOK
}

まとめ

copyコマンドとzipコマンドを実装することによって io.Reader, io.Writerについてみてきました。

他にもネットワーク系であれば、 net.Conn インターフェースを使って同様のことが可能です。( net.Conn インターフェースが Read, Write メソッドを持っている)

公式のFAQをみると、Goは、Googleで行っていた作業のための既存の言語と環境の不満から生まれたことがわかります。

Why did you create a new language?
Go was born out of frustration with existing languages and environments for the work we were doing at Google. Programming had become too difficult and the choice of languages was partly to blame. One had to choose either efficient compilation, efficient execution, or ease of programming; all three were not available in the same mainstream language. Programmers who could were choosing ease over safety and efficiency by moving to dynamically typed languages such as Python and JavaScript rather than C++ or, to a lesser extent, Java.

Googleは様々なプラットフォームのソフトウェアを扱っているため、既存の言語ではあまりに複雑になりすぎ、その問題解決として、Goという言語を作ったそうです。

なので、OSに近い低レイヤーの処理が簡潔に書けるように設計されており、CLIの実装等には非常に向いていると思います。

簡単な内容ではありましたが参考にしていただけると幸いです。