【ChatGPTに聞いてみた】Go言語のmapをテーブル形式で出力する

mapの中身を綺麗にデバッグしたいので、mapをテーブル形式でコンソール出力したいことがあった。

ChatGPTに聞きつつ、軽い修正したら完成しそうなのでやってみた。

今回使用するModelはGPT-4である。

あなたはGo言語のプロフェッショナルです。

map型の値をテーブル形式で出力するプログラムを書いてください。

[条件]
- 標準ライブラリのみを使用すること
- コード以外は返答しないこと
package main

import (
    "fmt"
    "sort"
    "strings"
)

func main() {
    data := map[string]int{
        "apple":  5,
        "banana": 3,
        "orange": 8,
        "pear":   2,
    }

    printTable(data)
}

func printTable(data map[string]int) {
    // キーを取得し、ソートする
    keys := make([]string, 0, len(data))
    for k := range data {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    // 最長のキー長を計算する
    maxKeyLen := 0
    for _, key := range keys {
        keyLen := len(key)
        if keyLen > maxKeyLen {
            maxKeyLen = keyLen
        }
    }

    // テーブルを出力する
    fmt.Println(strings.Repeat("-", maxKeyLen+9))
    fmt.Printf("| %-*s | value |\n", maxKeyLen, "key")
    fmt.Println(strings.Repeat("-", maxKeyLen+9))
    for _, key := range keys {
        fmt.Printf("| %-*s | %5d |\n", maxKeyLen, key, data[key])
    }
    fmt.Println(strings.Repeat("-", maxKeyLen+9))
}

試してみると。

% go run main.go
---------------
| key    | value |
---------------
| apple  |     5 |
| banana |     3 |
| orange |     8 |
| pear   |     2 |
---------------

うん、うん良い感じ。

値をmapの入れ子構造にできるよう改良してみよう。

入れ子構造のmapを許容できるよう改修してください。
import (
    "fmt"
    "sort"
    "strings"
)

func main() {
    data := map[string]interface{}{
        "apple":  5,
        "banana": 3,
        "fruits": map[string]interface{}{
            "orange": 8,
            "pear":   2,
        },
    }

    printTable(data, 0)
}

func printTable(data map[string]interface{}, level int) {
    // キーを取得し、ソートする
    keys := make([]string, 0, len(data))
    for k := range data {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    // 最長のキー長を計算する
    maxKeyLen := 0
    for _, key := range keys {
        keyLen := len(key)
        if keyLen > maxKeyLen {
            maxKeyLen = keyLen
        }
    }

    // テーブルを出力する
    fmt.Println(strings.Repeat("-", maxKeyLen+9+(level*2)))
    fmt.Printf("| %-*s | %*svalue |\n", maxKeyLen, "key", level*2, "")
    fmt.Println(strings.Repeat("-", maxKeyLen+9+(level*2)))
    for _, key := range keys {
        switch v := data[key].(type) {
        case map[string]interface{}:
            fmt.Printf("| %-*s | %*s=> map |\n", maxKeyLen, key, level*2, "")
            printTable(v, level+1)
        default:
            fmt.Printf("| %-*s | %*s%5v |\n", maxKeyLen, key, level*2, "", data[key])
        }
    }
    fmt.Println(strings.Repeat("-", maxKeyLen+9+(level*2)))
}

printTableのシグネチャが変わって、引数が増えた。

出力はこちら。

% go run main.go
---------------
| key    | value |
---------------
| apple  |     5 |
| banana |     3 |
| fruits | => map |
-----------------
| key    |   value |
-----------------
| orange |       8 |
| pear   |       2 |
-----------------
---------------

なんかそれっぽくなった。

ソースコードで意味がわからなかった箇所を質問してみる。

ありがとうございます。
一部のコードの意味がわからなかったので、教えてください。質問事項は下記です。

- printTable関数のlevelという引数はどういう意味を持った値が入りますか?
- maxKeyLen+9+(level*2) で9を加算するのは何故ですか?
- maxKeyLen+9+(level*2) で(level*2)を乗算するのは何故ですか?
質問事項について説明します。

level 引数は、入れ子構造の深さを示す値です。これにより、現在のマップがどの深さの入れ子にあるかを表現します。level の値が大きくなるほど、入れ子構造が深くなります。この値は、テーブル形式で出力する際に、マップの深さに応じて適切なインデントを行うために使用されます。

maxKeyLen+9 の 9 は、テーブル形式で出力する際の固定幅部分を加算しています。具体的には、"|", " ", " |", " value |" といったテキストの幅を考慮しています。ここで 9 を加算することで、テーブルの区切り線が正しい位置で表示されるようになります。

maxKeyLen+9+(level*2) の (level*2) は、入れ子構造の深さに応じて追加のインデントを行うための値です。level の値が大きくなるほど、入れ子構造が深くなるため、その深さに応じてインデント幅を追加する必要があります。この例では、入れ子構造ごとにインデント幅を 2 ずつ増やしています。この値は、区切り線やテーブルの各行のインデントに使用されます。

なるほど?

levelは自動で検知できないのかな?

`level`引数を使わず、動的に深さを取得できるように改修してください。
level 引数を使用せずに、動的に入れ子構造の深さを取得できるように改修したコードは以下のとおりです。

package main

import (
    "fmt"
    "sort"
    "strings"
)

func main() {
    data := map[string]interface{}{
        "apple":  5,
        "banana": 3,
        "fruits": map[string]interface{}{
            "orange": 8,
            "pear":   2,
            "nested": map[string]interface{}{
                "kiwi":  4,
                "grape": 6,
            },
        },
    }

    printTable(data)
}

func printTable(data map[string]interface{}) {
    printNestedTable(data, getMapDepth(data))
}

func printNestedTable(data map[string]interface{}, level int) {
    // キーを取得し、ソートする
    keys := make([]string, 0, len(data))
    for k := range data {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    // 最長のキー長を計算する
    maxKeyLen := 0
    for _, key := range keys {
        keyLen := len(key)
        if keyLen > maxKeyLen {
            maxKeyLen = keyLen
        }
    }

    // テーブルを出力する
    fmt.Println(strings.Repeat("-", maxKeyLen+9+(level*2)))
    fmt.Printf("| %-*s | %*svalue |\n", maxKeyLen, "key", level*2, "")
    fmt.Println(strings.Repeat("-", maxKeyLen+9+(level*2)))
    for _, key := range keys {
        switch v := data[key].(type) {
        case map[string]interface{}:
            fmt.Printf("| %-*s | %*s=> map |\n", maxKeyLen, key, level*2, "")
            printNestedTable(v, level-1)
        default:
            fmt.Printf("| %-*s | %*s%5v |\n", maxKeyLen, key, level*2, "", data[key])
        }
    }
    fmt.Println(strings.Repeat("-", maxKeyLen+9+(level*2)))
}

func getMapDepth(data map[string]interface{}) int {
    maxDepth := 0
    for _, value := range data {
        if nestedMap, ok := value.(map[string]interface{}); ok {
            depth := getMapDepth(nestedMap) + 1
            if depth > maxDepth {
                maxDepth = depth
            }
        }
    }
    return maxDepth
}

この改修では、getMapDepth 関数を新たに追加し、この関数で入れ子構造の最大深さを取得しています。getMapDepth 関数は再帰的に入れ子構造の深さを調べます。printTable 関数で getMapDepth 関数を使って最大深さを取得し、printNestedTable 関数に渡すことで、深さに応じたインデントを行っています。

出力してみる。

% go run main.go
-------------------
| key    |     value |
-------------------
| apple  |         5 |
| banana |         3 |
| fruits |     => map |
-----------------
| key    |   value |
-----------------
| nested |   => map |
--------------
| key   | value |
--------------
| grape |     6 |
| kiwi  |     4 |
--------------
| orange |       8 |
| pear   |       2 |
-----------------
-------------------

なんか-の長さおかしない?

あと一番深いネストのmapの出力順番あっている?

というかテーブルが見にくいので、JSONYAMLにしたい。

さらにフラグ引数で選択できるとなお良い。

出力形式をJSONもしくはYAMLにできるように改修してください。
出力形式はフラグ引数で受け取れるようにしてください。
フラグ引数で出力形式を指定できるように改修し、JSON と YAML の両方に対応したコードは以下のとおりです。

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "sort"

    "gopkg.in/yaml.v2"
)

type OutputFormat int

const (
    JSON OutputFormat = iota
    YAML
)

func main() {
    data := map[string]interface{}{
        "apple":  5,
        "banana": 3,
        "fruits": map[string]interface{}{
            "orange": 8,
            "pear":   2,
        },
    }

    var format string
    flag.StringVar(&format, "format", "json", "Output format: json or yaml")
    flag.Parse()

    var outputFormat OutputFormat
    switch format {
    case "json":
        outputFormat = JSON
    case "yaml":
        outputFormat = YAML
    default:
        log.Fatalf("Invalid output format: %s", format)
    }

    printData(data, outputFormat)
}

func printData(data map[string]interface{}, format OutputFormat) {
    var output []byte
    var err error

    switch format {
    case JSON:
        output, err = json.MarshalIndent(data, "", "  ")
    case YAML:
        output, err = yaml.Marshal(data)
    }

    if err != nil {
        log.Fatalf("Failed to marshal data: %v", err)
    }

    fmt.Println(string(output))
}

func sortMap(data map[string]interface{}) map[string]interface{} {
    sortedData := make(map[string]interface{}, len(data))
    keys := make([]string, 0, len(data))

    for k := range data {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    for _, k := range keys {
        v := data[k]
        if nestedMap, ok := v.(map[string]interface{}); ok {
            sortedData[k] = sortMap(nestedMap)
        } else {
            sortedData[k] = v
        }
    }

    return sortedData
}

この改修では、フラグ引数 -format を追加し、出力形式を指定できるようにしました。また、printData 関数を新たに作成し、指定された形式に応じて JSON または YAML の形式で出力するようにしています。

注意: このコードでは YAML のサポートのために、gopkg.in/yaml.v2 パッケージを使用しています。使用する前に、次のコマンドでパッケージをインストールしてください。

go get gopkg.in/yaml.v2

インストール後、上記のコードを実行して JSON または YAML 形式で出力できます。例えば、次のように実行できます。

go run main.go -format=json

または

go run main.go -format=yaml

出力してみる。

% go run ver3.go -format=yaml
apple: 5
banana: 3
fruits:
  orange: 8
  pear: 2
% go run ver3.go -format=json
{
  "apple": 5,
  "banana": 3,
  "fruits": {
    "orange": 8,
    "pear": 2
  }
}

良い感じですねー。

GoにはYAMLを扱う標準ライブラリが無いが、無理言って作ってもらう。

"gopkg.in/yaml.v2"を使わずに実装できますか?
標準ライブラリのみを使用して、JSON と独自の簡易 YAML 形式に対応したコードは以下のとおりです。YAML の標準仕様には完全には対応していませんが、簡単なケースであれば動作します。

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "sort"
    "strings"
)

type OutputFormat int

const (
    JSON OutputFormat = iota
    YAML
)

func main() {
    data := map[string]interface{}{
        "apple":  5,
        "banana": 3,
        "fruits": map[string]interface{}{
            "orange": 8,
            "pear":   2,
        },
    }

    var format string
    flag.StringVar(&format, "format", "json", "Output format: json or yaml")
    flag.Parse()

    var outputFormat OutputFormat
    switch format {
    case "json":
        outputFormat = JSON
    case "yaml":
        outputFormat = YAML
    default:
        log.Fatalf("Invalid output format: %s", format)
    }

    printData(data, outputFormat)
}

func printData(data map[string]interface{}, format OutputFormat) {
    switch format {
    case JSON:
        output, err := json.MarshalIndent(data, "", "  ")
        if err != nil {
            log.Fatalf("Failed to marshal data: %v", err)
        }
        fmt.Println(string(output))
    case YAML:
        printYAML(data, 0)
    }
}

func printYAML(data map[string]interface{}, indent int) {
    ind := strings.Repeat(" ", indent)
    sortedKeys := sortKeys(data)

    for _, key := range sortedKeys {
        value := data[key]
        switch v := value.(type) {
        case map[string]interface{}:
            fmt.Printf("%s%s:\n", ind, key)
            printYAML(v, indent+2)
        default:
            fmt.Printf("%s%s: %v\n", ind, key, value)
        }
    }
}

func sortKeys(data map[string]interface{}) []string {
    keys := make([]string, 0, len(data))
    for k := range data {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    return keys
}

このコードでは、printYAML 関数を新たに作成し、独自の簡易 YAML 形式を出力しています。printYAML 関数は再帰的に入れ子構造を処理し、適切なインデントで YAML 形式のテキストを出力します。

注意: この簡易 YAML 出力は、正式な YAML 標準には準拠していません。複雑なケースに対応するには、専用の YAML パッケージ(例: "gopkg.in/yaml.v2")を使用することをお勧めします。

Valueがmap[string]any以外の構造的な型の場合上手く動作しなさそうだけど、まあOK。