goでWebサービス No.7(ファイルの取り扱い)

公開:2020/10/29
更新:2020/10/29
13 min読了の目安(約12500字TECH技術記事

今回は、ファイルの取り扱いです。まず初めに簡単にGoでのファイルの取り扱いとGoの標準パッケージがサポートしているファイル形式を確認し、昨今のウェブでよく使われるJSON形式についてみていきます。
今回はDBと繋いだり大掛かりなことはしないのでローカル環境でやっていきます。

今回のデータはgithubにあげています。必要なファイルはクローンしてお使いください。

注意

コマンドラインを使った操作が出て来ることがあります。cd, ls, mkdir, touchといった基本的なコマンドを知っていることを前提に進めさせていただきます。
環境の違いで操作や実行結果に差異が出てくる可能性があります。私の実行環境は以下になります。

MacBook Pro (Early 2015)
macOS Catalina ver.10.15.6
go version go1.14.7 darwin/amd64
エディタはVScode

ファイル操作

基本的なファイル操作

基本的なファイル操作はa tour of goで取り扱われているのでここでは簡単な例を上げておきます。
まずディレクトリ構成は以下のようにします。まずはexample.mdを使います。

$ tree
.
├── main.go
└── source
    ├── example.json
    └── example.md

Goでのファイル操作は標準パッケージのosを使用します。これはOSの機能へのアクセスを提供します。つまりこのパッケージを使用すれば、ディレクトリ操作やファイル操作ができるようになります。

Package os provides a platform-independent interface to operating system functionality. The design is Unix-like, although the error handling is Go-like; failing calls return values of type error rather than error numbers. Often, more information is available within the error. For example, if a call that takes a file name fails, such as Open or Stat, the error will include the failing file name when printed and will be of type *PathError, which may be unpacked for more information.
引用:go公式

osパッケージを使用してファイルを読み込んで標準出力に出力してみましょう。ここではexample.mdを読み込みます。

main.go
// code:web7-1
package main

import (
	"encoding/json"
	"fmt"
	"os"
)
func main() {
	f, fOpenErr := os.Open("source/example.md")
	if fOpenErr != nil {
		fmt.Println("file open error:", fOpenErr)
		return
	}
	defer f.Close()
	buf := make([]byte, 32)
	for {
		n, err := f.Read(buf)
		if n == 0 {
			break
		}
		if err != nil {
			break
		}
		fmt.Printf("byte: %v \n", buf[:n])
		fmt.Printf("string: %v \n", string(buf[:n]))
	}
}
/* 実行結果
$ go run main.go
byte: [35 32 87 104 97 116 32 105 115 32 71 111 10 71 111 32 105 115 32 97 109 97 122 105 110 103 32 99 111 109 112 117] 
string: # What is Go
Go is amazing compu 
byte: [116 101 114 32 112 114 111 103 114 97 109 105 110 103 32 108 97 110 103 117 97 103 101 46 10 35 227 128 128 87 104 97] 
string: ter programing language.
# Wha 
byte: [116 32 100 111 32 105 116 32 100 111 63 10 87 105 116 104 32 71 111 44 32 121 111 117 32 99 97 110 32 100 111 32] 
string: t do it do?
With Go, you can do  
byte: [101 118 101 114 121 116 104 105 110 103 32 102 114 111 109 32 116 97 107 105 110 103 32 115 105 109 112 108 101 32 110 111] 
string: everything from taking simple no 
byte: [116 101 115 32 116 111 32 109 111 118 105 110 103 32 116 104 114 111 117 103 104 32 116 105 109 101 46 10 78 111 119 32] 
string: tes to moving through time.
Now  
byte: [105 116 39 115 32 117 112 32 116 111 32 121 111 117 32 116 111 32 99 111 109 101 32 117 112 32 119 105 116 104 32 116] 
string: it's up to you to come up with t 
byte: [104 101 32 114 101 115 116 33 10 35 32 72 111 119 32 100 111 32 119 101 32 103 101 116 32 115 116 97 114 116 101 100] 
string: he rest!
# How do we get started 
byte: [63 10 73 32 103 111 116 32 105 116 33 32 65 99 99 101 115 115 32 104 101 114 101 33 10 104 116 116 112 115 58 47] 
string: ?
I got it! Access here!
https:/ 
byte: [47 103 111 108 97 110 103 46 111 114 103 47] 
string: /golang.org/ 
*/

細かい説明はa tour of goの方に任せます。

ファイル操作をもう少し抽象的に

code:web7-1の場合、定量のバイト列なのでループで指定した量ずつ取り出さなければいけません、いっぺんに取り出したい場合はデータストリームを扱う機能を提供するio/ioutilを使うと良いです。

Package io provides basic interfaces to I/O primitives. Its primary job is to wrap existing implementations of such primitives, such as those in package os, into shared public interfaces that abstract the functionality, plus some other related primitives.
引用: Go公式

Package ioutil implements some I/O utility functions.
引用: Go公式

ioutilを使ったコード

main.go
// code:web7-2
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	f, fOpenErr := os.Open("source/example.md")
	if fOpenErr != nil {
		fmt.Println("file open error:", fOpenErr)
		return
	}
	defer f.Close()
	b, rErr := ioutil.ReadAll(f)
	if rErr != nil {
		fmt.Println("file read error:", rErr)
		return
	}
    fmt.Println(string(b))
}

データ形式が決まっているものの操作

これまでにみたものは単純な文字列として扱えるものでした(マークダウンファイルを読み込んでいましたが記法は反映されておらずあくまで文字列として読み込まれていました)。csvやjsonのように記述の規則が構造を持たせるようなデータの場合はどうでしょう?Goではそのようなデータのエンコード・デコードを行ってくれる標準パッケージencodingを提供しています。

Package encoding defines interfaces shared by other packages that convert data to and from byte-level and textual representations. Packages that check for these interfaces include encoding/gob, encoding/json, and encoding/xml. As a result, implementing an interface once can make a type useful in multiple encodings. Standard types that implement these interfaces include time.Time and net.IP. The interfaces come in pairs that produce and consume encoded data.
引用: Go公式

対応している形式は以下のようになります。

  • ascii85
    Package ascii85 implements the ascii85 data encoding as used in the btoa tool and Adobe's PostScript and PDF document formats.
  • asn1
    Package asn1 implements parsing of DER-encoded ASN.1 data structures, as defined in ITU-T Rec X.690.
  • base32
    Package base32 implements base32 encoding as specified by RFC 4648.
  • base64
    Package base64 implements base64 encoding as specified by RFC 4648.
  • binary
    Package binary implements simple translation between numbers and byte sequences and encoding and decoding of varints.
  • csv
    Package csv reads and writes comma-separated values (CSV) files.
  • gob
    Package gob manages streams of gobs - binary values exchanged between an Encoder (transmitter) and a Decoder (receiver).
  • hex
    Package hex implements hexadecimal encoding and decoding.
  • json
    Package json implements encoding and decoding of JSON as defined in RFC 7159.
  • pem
    Package pem implements the PEM data encoding, which originated in Privacy Enhanced Mail.
  • xml
    Package xml implements a simple XML 1.0 parser that understands XML name spaces.

ここではウェブてよく扱うjsonを使ってこれらの基本的な操作を確認していきたいと思います。

データ形式が決まった記述

ここからはjsonを扱ってみようと思います。普通はファイルから読み取ったものを扱うところを想像すると思いますが、手軽に挙動を確認するためにまずは文字列で定義して実行してみましょう。

構造を定義して読み込む

まずは単純に読み込んで標準出力に出力します。ここでは読み込んだデータを格納する変数の構造はこちらで定義します。

main.go
// code:web7-3
package main

import (
	"encoding/json"
	"fmt"
)

type pl struct {
	Language  string `json:"language"`
	Birthyear int    `json:"birthyear"`
	Character string `json:"character"`
}

var strB = `
[
	{"language": "Go", "birthyear": 2009, "character": "gopher"},
	{"language": "JavaScript", "birthyear": 1997, "character": null},
	{"language": "Java", "birthyear": 1995, "character":"Duke"},
	{"language": "D", "birthyear": 2007, "character":"D-man"},
	{"language": "PHP", "birthyear": 1995, "character":"elePHPant"},
	{"language": "LISP", "birthyear": 1958, "character":"lien"},
	{"language": "Python", "birthyear": 1990, "character":"python"}
]
`

func main() {
	var pls []pl
	if err := json.Unmarshal([]byte(strB), &pls); err != nil {
		fmt.Println(err)
	}
	fmt.Println(pls)
}
/* 実行結果
[{Go 2009 gopher} {JavaScript 1997 } {Java 1995 Duke} {D 2007 D-man} {PHP 1995 elePHPant} {LISP 1958 lien} {Python 1990 python}]
*/

jsonを扱う場合はencoding/jsonを使用します。strBがjson形式のデータを文字列として代入された変数です。それをmain関数内で定義した変数plsにデコードして格納します。
変数plsはpl型で定義されており、jsonデータの持つ構造と対応するようになっています。
pl型はこちらで定義したものになります。一番右に書いているjson:"language"などはjsonデータのどの要素と対応するかを表しています。コメントアウト等ではなく書く必要があるものなので注意してください。

type pl struct {
	Language  string `json:"language"`
	Birthyear int    `json:"birthyear"`
	Character string `json:"character"`
}

json.UnmarshalがjsonからGoの構造体にデコードするための関数です。jsonデータをbyte型で受け取りポインタ変数に渡します。
このようにすることでGoで構造体として扱えます。

構造を定義しないで読み込む

jsonデータの構造がわかっていないときはどうでしょう?実はjson.Unmarshalの第二引数はinterface{}なので型を選びません。ではその時はどのような結果が出力されるのでしょうか?デコードしたデータを格納する変数をinterface{}に変えてやってみましょう。

main.go
// code:web7-4
package main

import (
	"encoding/json"
	"fmt"

)

var strB = `
[
	{"language": "Go", "birthyear": 2009, "character": "gopher"},
	{"language": "JavaScript", "birthyear": 1997, "character": null},
	{"language": "Java", "birthyear": 1995, "character":"Duke"},
	{"language": "D", "birthyear": 2007, "character":"D-man"},
	{"language": "PHP", "birthyear": 1995, "character":"elePHPant"},
	{"language": "LISP", "birthyear": 1958, "character":"lien"},
	{"language": "Python", "birthyear": 1990, "character":"python"}
]
`

func main() {
	var s interface{}
	json.Unmarshal([]byte(strB), &s)
	fmt.Println(s)
}
/* 実行結果
[map[birthyear:2009 character:gopher language:Go] map[birthyear:1997 character:<nil> language:JavaScript] map[birthyear:1995 character:Duke language:Java] map[birthyear:2007 character:D-man language:D] map[birthyear:1995 character:elePHPant language:PHP] map[birthyear:1958 character:lien language:LISP] map[birthyear:1990 character:python language:Python]]
*/

ご覧のようにmapとして出力されます。もちろんこのままだとsの型は定まったものではないのでループを回してここの要素にアクセスすることもできません。基本的にはweb7-3のように構造を定義した方が良いと思います。

データ形式の決まったファイル

読み込み

次にjsonファイルから読み取ってみましょう。example.jsonに以下を記述します。これは先ほどのstrB変数に記述したものと同じです。

example.json
[
    {"language": "Go", "birthyear": 2009, "character": "gopher"},
    {"language": "JavaScript", "birthyear": 1997, "character": null},
    {"language": "Java", "birthyear": 1995, "character":"Duke"},
    {"language": "D", "birthyear": 2007, "character":"D-man"},
    {"language": "PHP", "birthyear": 1995, "character":"elePHPant"},
    {"language": "LISP", "birthyear": 1958, "character":"lien"},
    {"language": "Python", "birthyear": 1990, "character":"python"}
]

main.goは以下のようになるかと思います。

main.go
// code:web7-5
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
)

type pl struct {
	Language  string `json:"language"`
	Birthyear int    `json:"birthyear"`
	Character string `json:"character"`
}

func main() {
	// ファイルの読み込み
	f, fOpenErr := os.Open("source/example.json")
	if fOpenErr != nil {
		fmt.Println("file open error:", fOpenErr)
		return
	}
	defer f.Close()
	b, rErr := ioutil.ReadAll(f)
	if rErr != nil {
		fmt.Println("file read error:", rErr)
		return
	}
	// jsonの処理
	var pls []pl
	if err := json.Unmarshal(b, &pls); err != nil {
		fmt.Println(err)
	}
	// 表示
	for _, p := range pls {
		fmt.Println("language:", p.Language, ", birthyear:", p.Birthyear, ", character:", p.Character)
	}
}
/* 実行結果
language: Go , birthyear: 2009 , character: gopher
language: JavaScript , birthyear: 1997 , character: 
language: Java , birthyear: 1995 , character: Duke
language: D , birthyear: 2007 , character: D-man
language: PHP , birthyear: 1995 , character: elePHPant
language: LISP , birthyear: 1958 , character: lien
language: Python , birthyear: 1990 , character: python
*/

これまでに紹介したファイルの読み込みとjsonのデコード処理の組み合わせなので説明は不要と思います。

書き込み

次にjsonファイルへの書き込みを行います。以下のようにupperExample.jsonを作成し、先ほど読み込んだデータのアルファベットを全て大文字にして書き込んでみたいと思います。

$ tree
.
├── main.go
└── source
    ├── example.json
    ├── example.md
    └── upperExample.json

code:web7-5の一番したのループの変更と追加の記述をしましょう。

main.go(一部)
// code:web7-6
	// データの書き換え
	for i, p := range pls {
		fmt.Println("language:", p.Language, ", birthyear:", p.Birthyear, ", character:", p.Character)
		pls[i].Language = strings.ToUpper(pls[i].Language)
		pls[i].Character = strings.ToUpper(pls[i].Character)
	}
	// jsonにエンコード
	b, err := json.Marshal(pls)
	if err != nil {
		fmt.Println("encode error:", err)
	}
	// ファイルに書き込み
	if err := ioutil.WriteFile("source/upperExample.json", b, 0664); err != nil {
		fmt.Println("write file error:", err)
	}

これも特に説明は必要ないかと思います。ファイルへの書き込みはos.Write()もありますがioutil.WriteFile()が便利なのでこちらを使っています。実行し書き込まれたファイルが以下になります。

upperExample.json
[{"language":"GO","birthyear":2009,"character":"GOPHER"},{"language":"JAVASCRIPT","birthyear":1997,"character":""},{"language":"JAVA","birthyear":1995,"character":"DUKE"},{"language":"D","birthyear":2007,"character":"D-MAN"},{"language":"PHP","birthyear":1995,"character":"ELEPHPANT"},{"language":"LISP","birthyear":1958,"character":"LIEN"},{"language":"PYTHON","birthyear":1990,"character":"PYTHON"}]

最後に

今回は今後もウェブで一番使われるであろうjsonについてみていきましたが基本的な使い方は他のファイル形式でも同じだと思うので必要に応じて試してみてください。
もしかしたらサードパーティ製のパッケージにはいろいろな便利機能のついたものもあるかもしれません。探して試してみるのもいいかもしれませんね。

追記

Twitterでファイルパスの指定についてのツイートを見てあ「あっ!」と思ったので追記します。
ここで書いたコードではパスを文字列リテラルで書いていましたがOSによる差分を吸収するためにpath/filepathライブラリを使用した方がいいと思います(例えばmacOSではsource/upperExample.jsonですがwindowsではsource\upperExample.jsonまたはsource¥upperExample.jsonとなります)。

readPath := filepath.Join("source", "example.json")
f, fOpenErr := os.Open(readPath)
if fOpenErr != nil {
	fmt.Println("file open error:", fOpenErr)
	return
}