【改善編】簡単なツール作成を通してGolangを学ぶ

# お題
[前回](https://qiita.com/sky0621/items/27c610e08391c7cc83ea)、Python3で書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のGolang版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)」からの改善版。

# 試行Index
- 第１回：[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)
- 第２回：[【改善編】簡単なツール作成を通してRubyを学ぶ](https://qiita.com/sky0621/items/5868b19934187571d0cd)
- 第３回：[【改善編】簡単なツール作成を通してPython3を学ぶ](https://qiita.com/sky0621/items/27c610e08391c7cc83ea)
- 第４回：[【改善編】簡単なツール作成を通してGolangを学ぶ](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)
- 第５回：[【改善編】簡単なツール作成を通してJavaを学ぶ](https://qiita.com/sky0621/items/f19f5191754f61a4d4c3)
- 第６回：[【改善編】簡単なツール作成を通してScalaを学ぶ](https://qiita.com/sky0621/items/a68ae8b64d4856d1a195)
- 第７回：[簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/86d9159bc1930618162c)
- 第８回：[【改善編】簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/a70cc7a9453795e0c9e9)

# 実装・動作確認端末
### # 言語バージョン
```
$ go version
go version go1.11.4 linux/amd64
```

### # IDE - Goland
```
GoLand 2019.2
Build #GO-192.5728.103, built on July 23, 2019
```

# 実践

## 要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば第１回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件

### ソース全量
https://github.com/sky0621/book_go/tree/v0.2.0

## 解説

### 全ソースファイル

|Golangソース|説明|
|---|---|
|main.go|アプリ起動エントリーポイント|
|store_info.go|キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。<br>現状は「ファイル名」だけ保持|
|commands.go|キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。<br>コマンドの増減に関する影響は、このソースに閉じる。|
|command.go|各コマンドに共通のインタフェース|
|save.go|キーバリュー情報の保存を担う。|
|get.go|指定キーに対するバリューの取得を担う。|
|list.go|全キーバリュー情報の取得を担う。|
|remove.go|指定キーに対するバリューの削除を担う。|
|clear.go|全キーバリュー情報の削除を担う。|
|help.go|ヘルプ情報の表示を担う。|
|end.go|アプリの終了を担う。|

### [main.go]アプリ起動エントリーポイント

```go:[main.go]
package main

import (
	"bufio"
	"log"
	"os"
	"strings"
)

func main() {
	commands := NewCommands(NewStoreInfo("store.json"))

	println("Start!")
	for {
		s := bufio.NewScanner(os.Stdin)
		for s.Scan() {
			cmds := strings.Split(s.Text(), " ")
			commands.Exec(cmds)
		}
		if s.Err() != nil {
			log.Fatal(s.Err())
		}
	}
}
```

### [store_info.go]ストア情報の管理

```go:[store_info.go]
package main

// アルファベット大文字で始まると定数扱い
const DefaultStoreName = "store.json"

func NewStoreInfo(storeName string) StoreInfo {
	if storeName == "" {
		return &storeInfo{storeName: DefaultStoreName}
	}
	return &storeInfo{storeName: storeName}
}

type StoreInfo interface {
	GetName() string
}

type storeInfo struct {
	storeName string
}

func (s *storeInfo) GetName() string {
	return s.storeName
}
```

### [commands.go]各コマンドの管理

```go:[commands.go]
package main

import (
	"fmt"
	"os"
)

func NewCommands(storeInfo StoreInfo) *Commands {
	_, err := os.Stat(storeInfo.GetName())
	if err != nil {
		NewClearCommand(storeInfo).Exec(nil)
	}
	return &Commands{
		commands: map[string]Command{
			"end":    NewEndCommand(),
			"help":   NewHelpCommand(),
			"clear":  NewClearCommand(storeInfo),
			"save":   NewSaveCommand(storeInfo),
			"get":    NewGetCommand(storeInfo),
			"remove": NewRemoveCommand(storeInfo),
			"list":   NewListCommand(storeInfo),
		},
	}
}

type Commands struct {
	commands map[string]Command
}

func (c *Commands) Exec(cmds []string) {
	if len(cmds) < 1 {
		fmt.Println("no target")
		return
	}

	cmd := c.commands[cmds[0]]
	if cmd == nil {
		fmt.Println("no target")
		return
	}

	if len(cmds) == 1 {
		cmd.Exec(nil)
		return
	}
	cmd.Exec(cmds[1:])
}
```

### [command.go]各コマンドの親クラス

```go:[command.go]
package main

type Command interface {
	Exec(args []string)
}
```

### 各コマンドクラス

各コマンドについては、ストア情報がオンメモリのハッシュからJSONに変わった点以外はやることは一緒。（なので説明省く。）

#### ■保存

```go:[save.go]
package main

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

func NewSaveCommand(storeInfo StoreInfo) Command {
	return &saveCommand{storeInfo: storeInfo}
}

type saveCommand struct {
	storeInfo StoreInfo
}

func (c *saveCommand) Exec(args []string) {
	if len(args) != 2 {
		fmt.Printf("not valid: %#v\n", args)
		return
	}

	b, err := ioutil.ReadFile(c.storeInfo.GetName())
	if err != nil {
		fmt.Printf("error1: %s\n", err.Error())
		return
	}

	var d map[string]string
	err = json.Unmarshal(b, &d)
	if err != nil {
		fmt.Printf("error2: %s\n", err.Error())
		return
	}
	d[args[0]] = args[1]

	res, err := json.Marshal(d)
	if err != nil {
		fmt.Printf("error3: %s\n", err.Error())
		return
	}

	err = ioutil.WriteFile(c.storeInfo.GetName(), res, os.ModePerm)
	if err != nil {
		fmt.Printf("error4: %s\n", err.Error())
		return
	}
}
```

#### ■１件取得

```go:[get.go]
package main

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

func NewGetCommand(storeInfo StoreInfo) Command {
	return &getCommand{storeInfo: storeInfo}
}

type getCommand struct {
	storeInfo StoreInfo
}

func (c *getCommand) Exec(args []string) {
	if len(args) != 1 {
		fmt.Printf("not valid: %#v\n", args)
		return
	}

	b, err := ioutil.ReadFile(c.storeInfo.GetName())
	if err != nil {
		fmt.Printf("error1: %s\n", err.Error())
		return
	}

	var d map[string]string
	err = json.Unmarshal(b, &d)
	if err != nil {
		fmt.Printf("error2: %s\n", err.Error())
		return
	}
	fmt.Println(d[args[0]])
}
```

#### ■全件取得

```go:[list.go]
package main

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

func NewListCommand(storeInfo StoreInfo) Command {
	return &listCommand{storeInfo: storeInfo}
}

type listCommand struct {
	storeInfo StoreInfo
}

func (c *listCommand) Exec(args []string) {
	b, err := ioutil.ReadFile(c.storeInfo.GetName())
	if err != nil {
		fmt.Printf("error1: %s\n", err.Error())
		return
	}

	var d map[string]string
	err = json.Unmarshal(b, &d)
	if err != nil {
		fmt.Printf("error2: %s\n", err.Error())
		return
	}

	println(`"key","value"`)
	for k, v := range d {
		println("\"" + k + "\",\"" + v + "\"")
	}

}
```

#### ■１件削除

```go:[remove.go]
package main

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

func NewRemoveCommand(storeInfo StoreInfo) Command {
	return &removeCommand{storeInfo: storeInfo}
}

type removeCommand struct {
	storeInfo StoreInfo
}

func (c *removeCommand) Exec(args []string) {
	if len(args) != 1 {
		fmt.Printf("not valid: %#v\n", args)
		return
	}

	b, err := ioutil.ReadFile(c.storeInfo.GetName())
	if err != nil {
		fmt.Printf("error1: %s\n", err.Error())
		return
	}

	var d map[string]string
	err = json.Unmarshal(b, &d)
	if err != nil {
		fmt.Printf("error2: %s\n", err.Error())
		return
	}
	delete(d, args[0])

	res, err := json.Marshal(d)
	if err != nil {
		fmt.Printf("error3: %s\n", err.Error())
		return
	}

	err = ioutil.WriteFile(c.storeInfo.GetName(), res, os.ModePerm)
	if err != nil {
		fmt.Printf("error4: %s\n", err.Error())
		return
	}
}
```

#### ■全件削除

```go:[clear.go]
package main

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

func NewClearCommand(storeInfo StoreInfo) Command {
	return &clearCommand{storeInfo: storeInfo}
}

type clearCommand struct {
	storeInfo StoreInfo
}

func (c *clearCommand) Exec(args []string) {
	_, err := os.Create(c.storeInfo.GetName())
	if err != nil {
		fmt.Printf("error1: %s\n", err.Error())
		return
	}

	d := make(map[string]string, 1)
	b, err := json.Marshal(&d)
	if err != nil {
		fmt.Printf("error2: %s\n", err.Error())
		return
	}

	err = ioutil.WriteFile(c.storeInfo.GetName(), b, os.ModePerm)
	if err != nil {
		fmt.Printf("error3: %s\n", err.Error())
		return
	}
}
```

#### ■ヘルプ

```go:[help.go]
package main

func NewHelpCommand() Command {
	return &helpCommand{}
}

type helpCommand struct {
}

func (c *helpCommand) Exec(args []string) {
	msg := `

[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。

list   ... 保存済みの内容を一覧表示します。
save   ... keyとvalueを渡して保存します。
get    ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
endCommand   ... ヘルプ情報（当内容と同じ）を表示します。

`
	println(msg)
}
```

#### ■アプリ終了

```go:[end.go]
package main

import (
	"fmt"
	"os"
)

func NewEndCommand() Command {
	return &endCommand{}
}

type endCommand struct {
}

func (c *endCommand) Exec(args []string) {
	fmt.Println("End!")
	os.Exit(-1)
}
```

# まとめ
同じことを複数言語で実装するの、だんだんしんどくなってきたな・・・。