How to use encoding package

Go has no single encoding package; use specific subpackages like encoding/json or encoding/base64 for your data format.

The encoding package is a directory, not a library

You type import "encoding" and the compiler rejects the program with undefined: encoding. You expected a Swiss Army knife for data conversion. Go gives you a toolbox where every tool has its own handle.

The encoding path in the standard library is a directory that groups related packages, not a package you can import. Go organizes its standard library hierarchically to keep the codebase tidy, but the import system requires you to specify the exact package path. You import the leaf, not the folder.

The standard library contains encoding/json, encoding/xml, encoding/gob, encoding/base64, encoding/binary, encoding/csv, and encoding/asn1. Each one lives in its own subdirectory and provides its own API. You pick the tool that matches your format. Importing encoding/json pulls in only the JSON implementation. Importing encoding/xml pulls in only the XML implementation. This keeps your binary size small and your dependencies explicit.

Go's encoding packages are tools, not a single library. Import what you need.

Import the leaf, not the root

When you see encoding/json in documentation, the slash indicates the package path. The package name is json. The import path is encoding/json. You use the package name to call functions.

package main

import (
	"encoding/json"
	"fmt"
)

type Config struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

func main() {
	c := Config{Host: "localhost", Port: 8080}
	// Marshal converts the struct to a JSON byte slice.
	data, err := json.Marshal(c)
	if err != nil {
		// Marshal fails only if the value contains unexported fields or unmarshalable types.
		panic(err)
	}
	fmt.Println(string(data))
}

Here's the pattern: define a struct, add struct tags to control the output keys, call Marshal to get bytes, and handle the error. The json package uses reflection to inspect the struct fields at runtime. It reads the tags to determine how to map Go fields to JSON keys.

The output of this program is {"host":"localhost","port":8080}. The keys match the tags, not the Go field names. If you remove the tags, the encoder uses the field names, producing {"Host":"localhost","Port":8080}. JSON keys are case-sensitive. If the consumer expects lowercase keys, your tags must match.

Tags control the mapping. If the tag is wrong, the data is wrong.

Struct tags control the mapping

Struct tags are metadata attached to fields. They are not part of the type system, so the compiler does not enforce their syntax. A typo in a tag won't cause a compile error. It will just be ignored or misinterpreted by the encoder.

The json package supports several options in tags. The most common is omitempty. This tells the encoder to skip the field if it holds its zero value. For strings, the zero value is an empty string. For numbers, it is zero. For pointers, it is nil.

type User struct {
	Name    string `json:"name"`
	Age     int    `json:"age,omitempty"`
	Email   string `json:"email,omitempty"`
	Private string `json:"-"`
}

The Age field disappears from the JSON output if it is zero. The Email field disappears if it is empty. The Private field is ignored entirely because the tag is -. This is useful for fields you want to keep in the struct but never serialize, like a password hash or a database connection.

You can also use the string option to encode numbers as JSON strings. This preserves precision for large integers that might lose accuracy when parsed as floating-point numbers in JavaScript.

type ID struct {
	Value int64 `json:"value,string"`
}

This produces {"value":"1234567890123456789"} instead of {"value":1234567890123456789}. The consumer receives a string and can parse it with arbitrary precision.

Unexported fields are always ignored by the encoder. If a field starts with a lowercase letter, json.Marshal skips it. The compiler won't warn you. The field just won't appear in the output. This is a common source of confusion when debugging missing data.

Public names start with a capital letter. Private names start lowercase. The encoder respects this boundary.

Unmarshal requires a pointer

Decoding works in reverse. You pass a byte slice and a pointer to a variable. The decoder updates the variable in place.

package main

import (
	"encoding/json"
	"fmt"
)

type Config struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

func main() {
	input := []byte(`{"host":"example.com","port":9090}`)
	var c Config
	// Unmarshal reads JSON into the struct pointed to by &c.
	err := json.Unmarshal(input, &c)
	if err != nil {
		// Unmarshal fails if the JSON is malformed or types don't match.
		panic(err)
	}
	fmt.Printf("%+v\n", c)
}

Here's the critical rule: Unmarshal takes a pointer. If you pass a value, the function updates a copy and your original variable stays unchanged. The compiler won't catch this mistake, but the runtime behavior will be silent and wrong.

The compiler rejects the program with cannot use c (variable of type Config) as *Config value in argument if you forget the &. This is a compile-time safety net. Always pass a pointer to Unmarshal.

If the JSON contains a key that doesn't match any field, the decoder ignores it. If the JSON contains a value that doesn't match the field type, the decoder returns an error. For example, passing a string where an integer is expected produces json: cannot unmarshal string into Go struct field Config.port of type int.

Unmarshal requires a pointer. Forgetting the pointer is the most common bug.

Realistic example: Streaming JSON to HTTP

In production, you rarely marshal JSON to a byte slice and print it. You send it over the network. The json package provides an Encoder that writes directly to an io.Writer. This avoids allocating a full buffer in memory and streams the data efficiently.

package main

import (
	"encoding/json"
	"net/http"
)

type Response struct {
	Status string `json:"status"`
	Count  int    `json:"count"`
}

// HandleStats returns a JSON response with the current count.
func HandleStats(w http.ResponseWriter, r *http.Request) {
	// Set content type so the client knows how to parse the body.
	w.Header().Set("Content-Type", "application/json")

	resp := Response{Status: "ok", Count: 42}

	// Marshal writes directly to the response writer to avoid allocating a buffer.
	err := json.NewEncoder(w).Encode(resp)
	if err != nil {
		// Encoder errors are rare but indicate the writer closed or the struct is invalid.
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
}

Here's the production pattern: create an encoder bound to the response writer, call Encode, and handle errors. The Encode method adds a newline at the end of the output, which is useful for log processing but usually harmless for HTTP clients.

The if err != nil check is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Don't hide encoding errors. If the encoder fails, the client receives a broken response.

The encoder streams data directly to the http.ResponseWriter. This avoids allocating a full byte slice in memory before writing. It also handles partial writes more gracefully than Marshal followed by Write.

Accept interfaces, return structs. The handler returns a struct, and the encoder handles the serialization.

Pitfalls and compiler errors

Encoding and decoding have subtle traps. The compiler catches type mismatches, but runtime behavior can be surprising.

If you try to marshal a function or a channel, the encoder panics with json: unsupported type: func(). Go types like functions, channels, and complex numbers cannot be serialized to JSON. You must convert them to supported types first.

If you unmarshal into a map, the keys are always strings and the values are interface{}. The decoder uses float64 for all numbers. This can lose precision for large integers. If you need exact decimal precision, use json.Number or a custom unmarshaler.

type Precise struct {
	Amount json.Number `json:"amount"`
}

This stores the number as a string internally. You can parse it with strconv.ParseFloat or a big-number library when you need the value.

Pointers in structs affect how omitempty works. A pointer field is omitted if it is nil. A value field is omitted if it is the zero value. If you want to distinguish between "missing" and "zero", use a pointer. A nil pointer omits the field. A pointer to zero includes the field with value 0.

Don't pass a *string to a function expecting a string. Strings are already cheap to pass by value. Pointers to strings add indirection without benefit. The same applies to encoding: pass structs by value to Marshal, pass pointers to Unmarshal.

The worst encoding bug is the one that silently drops data. Check your tags. Verify your types. Test with malformed input.

Binary encoding and other formats

JSON and XML are text-based formats. They are human-readable and interoperable. Go also provides packages for binary formats.

encoding/base64 converts binary data to ASCII text. Use it when you need to embed binary data in a text protocol, like putting an image in a JSON payload or encoding a token for a URL. The package provides standard, URL-safe, and raw encodings.

package main

import (
	"encoding/base64"
	"fmt"
)

func main() {
	data := []byte("binary data")
	// EncodeToString returns a URL-safe base64 string.
	encoded := base64.URLEncoding.EncodeToString(data)
	fmt.Println(encoded)
}

Here's the base64 pattern: pick an encoding scheme, call EncodeToString, and handle the result. Base64 increases the size of the data by about 33%. Use it only when necessary.

encoding/binary handles raw binary data with specific endianness. Use it when you are working with network protocols or file formats that define byte order. The package provides BigEndian and LittleEndian converters.

encoding/gob is a Go-specific binary format. It is faster and more compact than JSON, but only works between Go programs. Use it for internal communication where performance matters and cross-language compatibility is not required.

encoding/csv processes comma-separated values. It provides a Reader and Writer that handle quoting and escaping. Use it for spreadsheet data or data exports.

encoding/asn1 handles Abstract Syntax Notation One, used in cryptography for certificates and keys. You rarely use this directly unless you are working with X.509 certificates.

Gob is fast. JSON is universal. Pick based on who reads the data.

Decision matrix

Use encoding/json when you need interoperability with web services, JavaScript, or other languages.

Use encoding/xml when you are integrating with legacy SOAP services or configuration formats that require XML.

Use encoding/gob when you are serializing data between Go processes and want better performance than JSON without caring about cross-language compatibility.

Use encoding/base64 when you need to represent binary data as ASCII text, such as embedding images in a data URI or encoding tokens for URLs.

Use encoding/binary when you are working with raw binary protocols and need to convert integers or floats to byte slices with specific endianness.

Use encoding/csv when you are processing tabular data from spreadsheets or data exports.

Use encoding/asn1 when you are parsing or generating cryptographic structures like X.509 certificates.

Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.

Where to go next