Go

Go言語 – エンコードとデコード

こんにちは、イサムです!

本記事では、Go言語のエンコードとデコードについてまとめていきます。
動的に型や値を扱いたい場合に非常に便利なので、ご紹介したいと思います。

対象読者
  • Go言語の様々なエンコードとデコードについて知りたい!
  • Go言語で動的に型や値を簡単に扱いたい!
この記事でやること
  • Go言語のエンコードとデコードについてのまとめ

はじめに

まずエンコードとデコードの前に、データには2種類の扱い方があることについてみていきます。
1つは、「何らかの構造を持つことを前提とした」データの扱い方です。
例えば、配列、スライス、マップ、構造体などのデータ構造が該当します。これらのデータ構造は、「配列n番目」「xxxのkeyの値」といった形で簡単に参照、操作できるように最適化されており、ここではこれらを「インメモリ表現」と呼ぶこととします。
もう1つは「バイトデータとしての」データの扱い方です。
例えば、「ファイルへの書き込み」「ネットワークからの受信」などといったデータそのものに何が書いてあるのかを気にする必要がない、気にするべきではないデータが該当します。ここではこれらを「バイト列表現」と呼ぶこととします。

エンコード・デコード

次に、「エンコード」「デコード」は何を意味するのかをみていきます。
どちらとも「データを変換するもの」ですが、データを変換する対象が異なります。

エンコード

エンコードは、「インメモリ表現からバイト列表現への変換」のことを指します。
例えば、以下のような処理になります。

  • 構造体からJSONデータを生成
  • 平文から暗号文を生成

JSONデータと暗号文は、まったく別の文脈であることに違和感を感じる方もいるかもしれませんが、いずれもデータ転送の際に使用される表現であり、「中に何が書いてあるのか」ということは重要視していないという共通点があります。そのためこの2つを「バイト列表現への変換」として同一視するのは、そこまでおかしなことではないと考えています。
また、エンコードと同じ意味として、シリアライゼーションやマーシャリングが使用されることもあります。

デコード

デコードは、「バイト列表現からインメモリ表現への変換」のことを指します。
例えば、以下のような処理になります。

  • JSONデータから構造体を生成
  • 暗号文から平文を生成

エンコードと同じ意味として、デシリアライゼーションやアンマーシャリングが使用されることもあります。

Go言語でのエンコード・デコード

エンコード・デコードの定義について確認できたので、Go言語でどのように行うかを説明していきます。
「Go構造体をどのようなバイト列表現に変換するのか」というところについては様々な種類がありますが、本記事ではJSONを扱っていきます。
(JSONの他にXML, CSVなどがあります。)

構造体からJSONへのエンコード

構造体をJSONへエンコードする際、以下のような方法があります。

  1. 標準パッケージencoding/jsonのMarshal関数
  2. 標準パッケージencoding/jsonのEncoder型のEncodeメソッド

Marshal関数によるエンコード

func Marshal(v any) ([]byte, error)
import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string
  Age int
}

func main() {
  user := User{Name: "Osamu", Age: 20}

  // 戻り値にはエンコード結果が[]byteの形で格納される
  jsonData, err := json.Marshal(user)
  if err != nil {
    fmt.Println(err)
    return
  }

  fmt.Println("=== Go言語の構文表現 ===")
  fmt.Printf("%#v\n", jsonData)

  fmt.Println("=== []byte 型 → string 型 ===")
  fmt.Printf("%s\n", jsonData)
}
=== Go言語の構文表現 ===
[]byte{0x7b, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x22, 0x4f, 0x73, 0x61, 0x6d, 0x75, 0x22, 0x2c, 0x22, 0x41, 0x67, 0x65, 0x22, 0x3a, 0x32, 0x30, 0x7d}
=== []byte 型 → string 型 ===
{"Name":"Osamu","Age":20}
構造体のフィールドエンコードJSONキー
Name“Name”
Age“Age”

Encoder型のEncodeメソッドによるエンコード

Encodeメソッドをただ使用するだけでは、エンコードはできないため、下記の手順が必要になります。

  1. NewEncoder関数でエンコード結果を出力するエンコーダーを生成
  2. Encodeメソッドでエンコード
type Encoder struct {
	// contains filtered or unexported fields
}

func NewEncoder(w io.Writer) *Encoder
func (enc *Encoder) Encode(v any) error
import (
  "fmt"
  "bytes"
  "encoding/json"
)

type User struct {
  Name string
  Age int
}

func main() {
  user := User{Name: "Osamu", Age: 20}

  body := bytes.Buffer{}

  // バッファにjsonエンコード結果を出す
	err := json.NewEncoder(&body).Encode(user)
	if err != nil {
		fmt.Println(err)
	}

  fmt.Println("=== Go言語の構文表現 ===")
  fmt.Printf("%#v\n", &body)

  fmt.Println("=== []byte 型 → string 型 ===")
  fmt.Printf("%s\n", &body)
}
=== Go言語の構文表現 ===
&bytes.Buffer{buf:[]uint8{0x7b, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x22, 0x4f, 0x73, 0x61, 0x6d, 0x75, 0x22, 0x2c, 0x22, 0x41, 0x67, 0x65, 0x22, 0x3a, 0x32, 0x30, 0x7d, 0xa}, off:0, lastRead:0}
=== []byte 型 → string 型 ===
{"Name":"Osamu","Age":20}
構造体のフィールドエンコードJSONキー
Name“Name”
Age“Age”

Marshal関数によるエンコードとの違いは、Marshal関数の結果が[]byteになるのに対し、Encodeメソッドでは、io.Writerの形で出力先を自由に指定できることです。
例えば、エンコード結果をos.Fileやhttp.ResponseWriterに書き込みたい場合、Marshal関数では、エンコード結果をそれぞれのWriteメソッドに渡すという2ステップが必要ですが、Encodeメソッドの場合、1ステップで実現することができます。

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

// ファイル出力(os.File型)
fileName := "hoge.txt"
file, err := os.Create(fileName)
user := User{Name: "Osamu", Age: 20}

// Marshal関数の場合
jsonData, _ := json.Marshal(user)
file.Write(jsonData)

// Encodeメソッドの場合
json.NewEncoder(file).Encode(user)

JSONから構造体へのデコード

JSONを構造体へのデコードする際、以下のような方法があります。

  1. 標準パッケージencoding/jsonのUnmarshal関数
  2. 標準パッケージencoding/jsonのDecoder型のDecodeメソッド

Unmarshal関数によるエンコード

func Unmarshal(data []byte, v any) error
import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string
  Age int
}

func main() {
  var user User
  jsonData := `{"Name": "Osamu", "Age": 20}`

  // デコード結果が構造体にマッピングされる
  if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
    fmt.Println(err)
    return
  }

  fmt.Println("=== Go言語の構文表現 ===")
  fmt.Printf("%#v\n", user)
}
=== Go言語の構文表現 ===
main.User{Name:"Osamu", Age:20}
構造体のフィールドデコードJSONキー
Name“Name”
Age“Age”

Decoder型のDecodeメソッドによるデコード

Decodeメソッドをただ使用するだけでは、デコードはできないため、下記の手順が必要になります。

  1. NewDecoder関数でデコード結果を出力するデコーダーを生成
  2. Decodeメソッドでデコード
type Decoder struct {
	// contains filtered or unexported fields
}

func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v any) error
import (
  "fmt"
  "strings"
  "encoding/json"
)

type User struct {
  Name string
  Age int
}

func main() {
  var user User
  jsonData := `{"Name": "Osamu", "Age": 20}`

  // デコード結果が構造体にマッピングされる
  if err := json.NewDecoder(strings.NewReader(jsonData)).Decode(&user); err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== Go言語の構文表現 ===")
  fmt.Printf("%#v\n", user)
}
=== Go言語の構文表現 ===
main.User{Name:"Osamu", Age:20}
構造体のフィールドデコードJSONキー
Name“Name”
Age“Age”

Unmarshal関数によるデコードとの違いは、デコード対象のデータが[]byteの形で指定しているのに対し、Decodeメソッドでは、io.Readerの形で指定できることです。

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

// ファイルデータ取得(os.File型)
filePath := "hoge.txt"

fileA := ioutil.ReadFile(filePath)
fileB := os.Open(filePath)

var user User

// Unmarshal関数の場合
json.Unmarshal(fileA, &user)

// Decodeメソッドの場合
json.NewDecoder(fileB).Decode(&user)

構造体のフィールドとJSONキーの規則

エンコード・デコードは、構造体のフィールドとJSONキーの間で対応付けが行われます。
これまでは、同名のフィールド・JSONキーが対応づいていましたが、これを柔軟に変更したい場合もあるかと思います。
ここからは、構造体フィールド・JSONキーのマッピングルールについてみていきます。

大文字・小文字の区別はない

ここまで扱ってきたJSONは、キーに大文字が含まれているものを使用してきました。しかし、JSONキーを大文字で使用するのは違和感に感じる方もいると思います。できればキーは小文字にしたいと思います。
そこで「大文字・小文字が一致していないフィールドとJSONキーを対応付ける」ことが可能かどうか見ていきます。

import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string
  AGE int
  Blood string
  CoUNTRY string
}

func main() {

  // エンコード(構造体 → JSON)
  user := User{Name: "Osamu", AGE: 20, Blood: "A", CoUNTRY: "Japan"}
  encode(user)

  // デコード(JSON → 構造体)
  // Name → Name
  // age → AGE (全小文字 → 全大文字)
  // bLOOD → Blood (先頭だけ不一致)
  // cOuntry → CoUNTRY (全不一致)
  jsonData := `{"Name": "Osamu", "age": 20, "bLOOD": "A", "cOuntry": "Japan"}`
  decode(jsonData)

}

func encode(u User) {
  jsonData, err := json.Marshal(u)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== エンコード(構造体 → JSON) ===")
  fmt.Printf("%s\n", jsonData)
}

func decode(jsonData string) {
  var user User
  if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== デコード(JSON → 構造体) ===")
  fmt.Printf("%+v\n", user)
}
=== エンコード(構造体 → JSON) ===
{"Name":"Osamu","AGE":20,"Blood":"A","CoUNTRY":"Japan"}
=== デコード(JSON → 構造体) ===
{Name:Osamu AGE:20 Blood:A CoUNTRY:Japan}
構造体のフィールドエンコードJSONキー
Name“Name”
AGE“AGE”
Blood“Blood”
CoUNTRY“CoUNTRY”
構造体のフィールドデコードJSONキー
Name“Name”
AGE“age”
Blood“bLOOD”
CoUNTRY“cOuntry”

上記の結果から、エンコードの場合は、フィールド名がJSONデータのキーになるので関係はありませんが、デコードの場合、構造体とJSONキーの対応付けは「大文字小文字の区別はない」ということがわかりました。
これらをまとめますと以下のようになります。

  • エンコードでは、構造体のフィールドがJSONデータのキーになる
  • デコードでは、構造体のフィールドとJSONデータのキーが大文字小文字区別一致していなくてもマッピングできる

非公開フィールドはエンコード・デコード対象外

これまでは、公開フィールドのみを扱ってきましたが、非公開フィールドはどのように扱われるのか見ていきたいと思います。

import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string
  age int // 非公開フィールド
  Blood string
  Country string
}

func main() {

  // エンコード(構造体 → JSON)
  user := User{Name: "Osamu", age: 20, Blood: "A", Country: "Japan"}
  encode(user)

  // デコード(JSON → 構造体)
  jsonData := `{"Name": "Osamu", "age": 20, "Blood": "A", "Country": "Japan"}`
  decode(jsonData)

}

func encode(u User) {
  jsonData, err := json.Marshal(u)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== エンコード(構造体 → JSON) ===")
  fmt.Printf("%s\n", jsonData)
}

func decode(jsonData string) {
  var user User
  if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== デコード(JSON → 構造体) ===")
  fmt.Printf("%+v\n", user)
}
=== エンコード(構造体 → JSON) ===
{"Name":"Osamu","Blood":"A","Country":"Japan"}
=== デコード(JSON → 構造体) ===
{Name:Osamu age:0 Blood:A Country:Japan}
構造体のフィールドエンコードJSONキー
Name“Name”
age
(キーすら生成されない)
Blood“Blood”
Country“Country”
構造体のフィールドデコードJSONキー
Name“Name”
age
(値が反映されず、型のゼロ値)
“age”
Blood“Blood”
Country“Country”

構造体からJSONデータへのエンコード、JSONデータから構造体へのデコードも非公開フィールドに対しては処理されないことがわかりました。
これらをまとめますと以下のようになります。

  • 構造体の非公開フィールドは、JSONデータでも外部公開できない
  • JSONデータから構造体の非公開フィールドへマッピングすることができない(フィールドのゼロ値が格納される)

タグでフィールド名と異なるマッピングができる

これまでは、大文字小文字が異なるが構造体のフィールドとJSONキーが一致していればエンコード・デコードができましたが、全く異なる場合にどうすればよいかといいますと、構造体のフィールドにタグをつけることでエンコード・デコードができるようになります。

import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string `json:"first"`
  Age int `json:"second"`
  Blood string `json:"third"`
  Country string `json:"fourth"`
}

func main() {

  // エンコード(構造体 → JSON)
  user := User{Name: "Osamu", Age: 20, Blood: "A", Country: "Japan"}
  encode(user)

  // デコード(JSON → 構造体)
  // JSONキーと構造体のフィールド名一致
  jsonData1 := `{"Name": "Osamu", "Age": 20, "Blood": "A", "Country": "Japan"}`
  decode(jsonData1)

  // JSONキーと構造体のフィールド名のタグ一致
  jsonData2 := `{"first": "Osamu", "second": 20, "third": "A", "fourth": "Japan"}`
  decode(jsonData2)

  // JSONキーと構造体のフィールド名のタグ一致(大文字小文字バラバラ)
  jsonData3 := `{"First": "Osamu", "sEcond": 20, "thIrd": "A", "fouRth": "Japan"}`
  decode(jsonData3)

}

func encode(u User) {
  jsonData, err := json.Marshal(u)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== エンコード(構造体 → JSON) ===")
  fmt.Printf("%s\n", jsonData)
}

func decode(jsonData string) {
  var user User
  if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== デコード(JSON → 構造体) ===")
  fmt.Printf("%+v\n", user)
}
=== エンコード(構造体 → JSON) ===
{"first":"Osamu","second":20,"third":"A","fourth":"Japan"}
=== デコード(JSON → 構造体) ===
{Name: Age:0 Blood: Country:}
=== デコード(JSON → 構造体) ===
{Name:Osamu Age:20 Blood:A Country:Japan}
=== デコード(JSON → 構造体) ===
{Name:Osamu Age:20 Blood:A Country:Japan}
構造体のフィールドエンコードJSONキー
Name (json: “first”)“first”
Age (json: “second”)“second”
Blood (json: “third”)“third”
Country (json: “fourth”)“fourth”
構造体のフィールドデコードJSONキー
Name
(値が反映されず、型のゼロ値)
“Name”
Age
(値が反映されず、型のゼロ値)
“Age”
Blood
(値が反映されず、型のゼロ値)
“blood”
Country
(値が反映されず、型のゼロ値)
“country”
構造体のフィールドデコードJSONキー
Name“first”
Age“second”
Blood“third”
Country“fourth”
構造体のフィールドデコードJSONキー
Name“First”
Age“sEcond”
Blood“thIrd”
Country“fouRth”

構造体のフィールドにタグを付与すると構造体からJSONデータへはタグ名でエンコードされ、JSONデータから構造体へはタグ名でデコードされることがわかりました。
これらをまとめますと以下のようになります。

  • 構造体のフィールドにタグを付与するとタグによってエンコード・デコードされる
  • タグ名も大文字小文字問わず文字が一致していればマッピングされる

エンコード・デコードで特定のフィールドを常に無視する

構造体のフィールドにjsonタグの値に「-」を指定することで、設定したフィールドはエンコード・デコードの対象外になります。

import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string `json:"first"`
  Age int `json:"-"`
  Blood string `json:"third"`
  Country string `json:"fourth"`
}

func main() {

  // エンコード(構造体 → JSON)
  user := User{Name: "Osamu", Age: 20, Blood: "A", Country: "Japan"}
  encode(user)

  // デコード(JSON → 構造体)
  // JSONキーと構造体のフィールド名一致
  jsonData := `{"first": "Osamu", "Age": 20, "third": "A", "fourth": "Japan"}`
  decode(jsonData)
}

func encode(u User) {
  jsonData, err := json.Marshal(u)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== エンコード(構造体 → JSON) ===")
  fmt.Printf("%s\n", jsonData)
}

func decode(jsonData string) {
  var user User
  if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== デコード(JSON → 構造体) ===")
  fmt.Printf("%+v\n", user)
}
=== エンコード(構造体 → JSON) ===
{"first":"Osamu","third":"A","fourth":"Japan"}
=== デコード(JSON → 構造体) ===
{Name:Osamu Age:0 Blood:A Country:Japan}
構造体のフィールドエンコードJSONキー
Name (json: “first”)“first”
Age (json: “-“)
(キーすら生成されない)
Blood (json: “third”)“third”
Country (json: “fourth”)“fourth”
構造体のフィールドデコードJSONキー
Name (json: “first”)“first”
Age (json: “-“)
(値が反映されず、型のゼロ値)
“Age”
Blood (json: “third”)“third”
Country (json: “fourth”)“fourth”

構造体のタグに「-」を設定することで、エンコード時は、対象のフィールドが省略され、デコード時は、値が反映されずゼロ値が設定されることがわかりました。

エンコードで特定のフィールドがゼロ値の時無視する

構造体のフィールドにjsonタグの値に「omitempty」を指定することで、設定したフィールドがゼロ値だった場合、エンコード時、そのフィールドは対象外になります。

import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string `json:"first"`
  Age int `json:"second,omitempty"`
  Blood string `json:"third"`
  Country string `json:"fourth"`
}

func main() {

  // エンコード(構造体 → JSON)
  user := User{Name: "Osamu", Age: 0, Blood: "A", Country: "Japan"}
  encode(user)
}

func encode(u User) {
  jsonData, err := json.Marshal(u)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("=== エンコード(構造体 → JSON) ===")
  fmt.Printf("%s\n", jsonData)
}
=== エンコード(構造体 → JSON) ===
{"first":"Osamu","third":"A","fourth":"Japan"}
構造体のフィールドエンコードJSONキー
Name (json: “first”)“first”
Age (json: “second,omitempty”)
(キーすら生成されない)
Blood (json: “third”)“third”
Country (json: “fourth”)“fourth”

構造体のタグに「omitempty」を設定することで、値がゼロ値の場合、エンコード時、対象のフィールドが省略されることがわかりました。

デコード時に「ゼロ値」と「値なし」を区別する

「ゼロ値と同じ値が入っている状況」と「値自体がない状況」を区別する場合、フィールドのデータ型をポインタ型にすることで区別することできます。

import (
  "fmt"
  "encoding/json"
)

type User struct {
  Name string `json:"first"`
  Age *int `json:"second"`
  Blood string `json:"third"`
  Country string `json:"fourth"`
}

func main() {

  // デコード(JSON → 構造体)
  // ゼロ値と同じ値が入っている
  fmt.Println("=== デコード(JSON → 構造体) ゼロ値 ===")
  jsonData1 := `{"first": "Osamu", "second": 0, "third": "A", "fourth": "Japan"}`
  decode(jsonData1)

  // 値がない
  fmt.Println("=== デコード(JSON → 構造体) 値がない ===")
  jsonData2 := `{"first": "Osamu", "third": "A", "fourth": "Japan"}`
  decode(jsonData2)
}

func decode(jsonData string) {
  var user User
  if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
    fmt.Println(err)
    return
  }
  fmt.Printf("%+v\n", user)
}
=== デコード(JSON → 構造体) ゼロ値 ===
{Name:Osamu Age:0xc000018170 Blood:A Country:Japan}
=== デコード(JSON → 構造体) 値がない ===
{Name:Osamu Age:<nil> Blood:A Country:Japan}
構造体のフィールドデコードJSONキー
Name (json: “first”)“first”
Age (json: “second”)“second”
Blood (json: “third”)“third”
Country (json: “fourth”)“fourth”

構造体のフィールドのデータ型にポインタ型にすることで、JSONキーの有無によってデコード時の値が変わることがわかりました。

  • JSONキーがある場合、キーに格納されている値のメモリアドレス
  • JSONキーがない場合、nil

まとめ

この記事では、Go言語のエンコード・デコードについてまとめました。

少しGo言語をいじってみたいと思ったら、ブラウザ上で実行できる公式チュートリアルでコードを実際に書いてみると雰囲気がつかめるかもしれません。
今後、さらにGo言語のことについて記事にしていきたいと思います。

もし「記事の内容が間違えている!」等ありましたら、Twitterまたはお問い合わせフォームからご連絡いただければと思います。

最後までご覧いただきありがとうございました。

参考サイト

A Tour of Go

encoding/json