Help us understand the problem. What is going on with this article?

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

お題

前回、Python3で書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のGolang版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ」からの改善版。

試行Index

実装・動作確認端末

# 言語バージョン

$ 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ファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば第1回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件

ソース全量

https://github.com/sky0621/book_go/tree/v0.2.0

解説

全ソースファイル

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

[main.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]ストア情報の管理

[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]各コマンドの管理

[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]各コマンドの親クラス

[command.go]
package main

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

各コマンドクラス

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

■保存

[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
    }
}

■1件取得

[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]])
}

■全件取得

[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 + "\"")
    }

}

■1件削除

[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
    }
}

■全件削除

[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
    }
}

■ヘルプ

[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)
}

■アプリ終了

[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)
}

まとめ

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away