Go Language Essentials: Building Blocks Of Effective Code

Overview

In this chapter, we will explore the essential components of the Go programming language. You will learn how to use the keywords, operators, and delimiters that form the foundation of Go, along with the idiomatic ways to structure your code. We will also delve into data structures, including how Go handles default values (zero values) and the characteristics of structs. This chapter covers Go’s treatment of strings, UTF-8 encoding, and the different types of string literals. Finally, we will examine how to declare variables and constants, and introduce the use of the iota keyword for efficiently handling constant definitions. This chapter sets the stage for mastering Go by equipping you with the knowledge of its core language features.

Keywords

There are only 25 keywords in Go. These keywords may not be used as identifiers.

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

Predeclared Identifiers

Go also has the following predeclared identifiers.

Constants

true  false  iota  nil

Types

bool      byte      rune        string       error
int       int8      int16       int32        int64
uint      uint8     uint16      uint32       uint64   uintptr
float32   float64   complex64   complex128

Functions

make      len      cap       new    append   copy
close     delete   complex   real   imag     panic
recover   print    println

Operators And Delimiters

The following character sequences represent operators (including assignment operators) and punctuation:

+    &     +=    &=     &&    ==    !=    (    )
-    |     -=    |=     ||    <     <=    [    ]
*    ^     *=    ^=     <-    >     >=    {    }
/    <<    /=    <<=    ++    =     :=    ,    ;
%    >>    %=    >>=    --    !     ...   .    :
     &^          &^=

Declaring Variables

In Go, there are several ways to declare a variable, and in some cases, more than one way to declare the exact same variable and value.

First, let’s declare a variable called i of data type int without initialzation. This means we will declare a space to put a value, but not give it an initial value.

var i int

You now have a variable declared as i of data type int.

You can also initialize the value by using the equal (=) operator:

var i int = 1

Both of the above forms are declaration are called long variable declarations in Go.

You can also use short variable declaration:

i := 1

In this case, you have a variable called i, and a data type of int. When you don’t specify a data type, Go will infer the data type.

With there being three ways to declare variables, the Go community has adopted the following idioms:

Only use long form when you are not initializing the variable:

var i int

Use short form when declaring and initializing:

i := 1

If you did not desire Go to infer your data type, but you still want to use short variable declaration, you can wrap your value in your desired type.

i := int64(1)

The long form is seldom used and not considered idiomatic in Go when you are also initializing the value:

var i int = 1

Zero Values

All builtin types have a zero value. Any allocated variable is usable even if it never has a value assigned. We can see the zero values for the following types with the following code:


package main

import "fmt"

func main() {
	var a int
	var b string
	var c float64
	var d bool

	fmt.Printf("var a %T = %d\n", a, a)
	fmt.Printf("var b %T = %q\n", b, b)
	fmt.Printf("var c %T = %f\n", c, c)
	fmt.Printf("var d %T = %t\n\n", d, d)
}

/* output
var a int = 0
var b string = ""
var c float64 = 0.000000
var d bool = false
*/

In Go, because all values have a zero value, you can’t have undefined values like some other languages.

For instance, a boolean in some languages could be undefined, true, or false, thus allowing for three states to the variable. In Go, you can’t have more than two states for a boolean value.

We used the %T verb in the fmt.Printf statement. This tells the function to print the data type for the variable.

Nil

Another type in Go is the nil type. This can represent many concepts.

One of the core concepts for nil is that it is the default value for many common types:

  • maps
  • slices
  • functions
  • channels
  • interfaces
  • errors

We’ll cover nil as it pertains to each type in later chapters.

Zero Values Cheat Sheet

The following types will default to the corresponding zero values. There are some types we have not covered yet, but we will cover them in detail in future chapters.


var s string    // defaults to ""
var r rune      // defaults to 0
var bt byte     // defaults to 0
var i int       // defaults to 0
var ui uint     // defaults to 0
var f float32   // defaults to 0
var c complex64 // defaults to 0
var b bool      // defaults to false
var arr [2]int  // defaults to [0 0]
var obj struct {
	b   bool
	arr [2]int
}                     // defaults to {false [0 0]}
var si []int          // defaults to empty slice of int
var ch chan string    // defaults to nil
var mp map[int]string // defaults to nil
var fn func()         // defaults to nil
var ptr *string       // defaults to nil

Output:


string              :
rune                : 0
byte                : 0
int                 : 0
uint                : 0
float32             : 0
complex64           : (0+0i)
bool                : false
array [2]int        : [0 0]
struct              : {false [0 0]}
slice []int         : []
channel chan string : <nil>
map map[int]string  : map[]
function func()     : <nil>
*string             : <nil>

Naming Rules

The naming of variables is quite flexible, but there are some rules you need to keep in mind:

  • Variable names must only be one word (as in no spaces)
  • Variable names must be made up of only letters, numbers, and underscore (_)
  • Variable names cannot begin with a number

Following the rules above, let’s look at both valid and invalid variable names:

Valid Invalid Why Invalid
userName user-name Hyphens are not permitted
name4 4name Cannot begin with a number
user $user Cannot use symbols
userName user name Cannot be more than one word

Something else to keep in mind when naming variables, is that they are case-sensitive, meaning that userName, USERNAME, UserName, and uSERnAME are all completely different variables. You should avoid using similar variable names within a program to ensure that both you and your current and future collaborators can keep your variables straight.

While variables are case sensitive, the case of the first letter of a variable has special meaning in Go. If a variable starts with an uppercase letter, then that variable is accessible outside the package it was declared in (or exported). If a variable starts with a lowercase letter, then it is only available within the package it is declared in.

var Email string
var password string

Email starts with a uppercase letter and can be accessed by other packages. password starts with a lowercase letter, and is only accessible inside the package it is declared in.

Naming Style

It is common in Go to use very terse (or short) variable names. Given the choice between using userName and user for a variable, it would be idiomatic to choose user.

Scope also plays a role in the terseness of the variable name. The rule is that the smaller the scope the variable exists in, the smaller the variable name.

names := []string{"Mary", "John", "Bob", "Anna"}
for i, n := range names {
 fmt.Printf("index: %d = %q\n", i, n)
}

The variable names is used in a larger scope, so it would be common to give it a more meaningful name to help remember what it means in the program. However, the variables i and n are used immediately in the next line of code, and never used again. Because of this, someone reading the code will not be confused about where they are used, or what they mean.

Finally, some notes about style. The style is to used MixedCaps or mixedCaps rather than underscores for multiword names.

Conventional Style Unconventional Style Why Unconventional
userName user_name Underscores are not conventional
i index prefer i over index as it is shorter
serveHTTP serveHttp acronyms should be capitalized
userID UserId acronyms should be capitalized

The most important thing about style is to be consistent, and that the team you work on agrees to the style.

Dave Cheney did an entire talk on idiomatic variable names used in go: What’s in a name

Semantic Naming

Beyond variable naming, Go has conventions for naming types, interfaces, and functions that make code read more naturally.

Structs should be plain nouns:

type API struct { ... }
type User struct { ... }
type Replica struct { ... }

Interfaces should be active nouns (often ending in -er):

type Reader interface { ... }
type Writer interface { ... }
type JobProcessor interface { ... }

Functions and methods should be verbs:

func Read() { ... }
func Process() { ... }
func (r *Replica) Sync() { ... }

Why Semantic Naming Matters

Following these conventions makes doc comments natural and fluent:

// Sync the local replica state to the provided upstream.
func (r *Replica) Sync(u Upstream) error { ... }

// Process the next available job from the queue
// and emit results to the sink.
func Process(q JobQueuer, s Sinker) error { ... }

It also makes code read like prose - each statement becomes a sentence:

result := Process(queue, sink)
user := Fetch(userID)
data := reader.Read()

Adapted from Go Naming Tips by Peter Bourgon

Multiple Assignment

Go also allows you to assign several values to several variables within the same line. Each of these values can be of a different data type:


j, k, l := "gopher", 2.05, 15
fmt.Println(j)
fmt.Println(k)
fmt.Println(l)

/*
	output:
	gopher
	2.05
	15
*/

In the example above, the variable j was assigned to the string “gopher”, the variable k was assigned to the float 2.05, and the variable l was assigned to the integer 15.

This approach to assigning multiple variables to multiple values in one line can keep your lines of code down, but make sure you are not compromising readability for fewer lines of code.

Statically Typed

Go is a statically typed language. Statically typed means that each statement in the program is checked at compile time. It also means that the data type is bound to the variable, whereas in Dynamically linked languages, the data type is bound to the value.

For example, in Go, the type is declared when declaring a variable:

var pi float64 = 3.14
var week int = 7

As apposed to a language like PHP, where the data type is associated to the value:

$s = "gopher";        // $s is a string
$s = 123;             // $s is now an integer

Now that we know a little about a statically typed language, this will help us better understand how numbers in Go work.

Numeric Types

Go has two types of numeric types. The first type is an architecture independent type. This means that regardless of the architecture you compile for, the type will have the correct size (bytes). The second type is a implementation specific type, and the byte size of that numeric type can vary based on the architecture the program is built for.

Go has the following architecture-independent numeric types:

uint8       unsigned  8-bit integers (0 to 255)
uint16      unsigned 16-bit integers (0 to 65535)
uint32      unsigned 32-bit integers (0 to 4294967295)
uint64      unsigned 64-bit integers (0 to 18446744073709551615)
int8        signed  8-bit integers (-128 to 127)
int16       signed 16-bit integers (-32768 to 32767)
int32       signed 32-bit integers (-2147483648 to 2147483647)
int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
float32     IEEE-754 32-bit floating-point numbers (+- 1O-45 -> +- 3.4 * 1038 )
float64     IEEE-754 64-bit floating-point numbers (+- 5 * 10-324 -> 1.7 * 10308 )
complex64   complex numbers with float32 real and imaginary parts
complex128  complex numbers with float64 real and imaginary parts
byte        alias for uint8
rune        alias for int32

In addition, Go has the following implementation specific types:

uint     either 32 or 64 bits
int      same size as uint
uintptr  an unsigned integer large enough to store the uninterpreted bits of a pointer value

Implementation specific types will have their size defined by the architecture the program is compiled for.

Picking The Correct Numeric Type

In Go, picking the correct type usually has more to do with performance for the target architecture you are programming for than the size of the data you are working with. However, without needing to know the specific ramifications of performance for your program, you can follow some of these basic guidelines when first starting out.

For integer data, it’s common in Go to use the implementation types like int or uint. This will typically result in the fastest processing speed for your target architecture.

If you know you won’t exceed a specific size range, then picking an architecture-independent type can both increase speed decrease memory usage. To understand integer ranges, we can look at the following examples:

int8 (-128 -> 127)
int16 (-32768 -> 32767)
int32 (− 2,147,483,648 -> 2,147,483,647)
int64 (− 9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)

And for unsigned integers, we have the following ranges:

uint8 (with alias byte, 0 -> 255)
uint16 (0 -> 65,535)
uint32 (0 -> 4,294,967,295)
uint64 (0 -> 18,446,744,073,709,551,615)

For floats:

float32 (+- 1O-45 -> +- 3.4 * 1038 )
(IEEE-754) float64 (+- 5 * 10-324 -> 1.7 * 10308 )

Now that we have looked at some of the possible ranges for numeric data types, we can see how they will be affected if we exceed those ranges in our program.

Number Conversions

Because Go is a typed language, when working with different number types, conversions may need to be made when performing calculations.

If two numbers of are the same type, no conversion is needed:


package main

import "fmt"

func main() {
	Cup := 8
	Pint := Cup * 2
	Quart := Pint * 2
	Gallon := Quart * 4

	fmt.Println("1 cup and 2 quart is", Cup+Quart*2, "ounces")
	fmt.Println("1 cup and 2 gallons is", Cup+Gallon*2, "ounces")
}

Output:


1 cup and 2 quart is 72 ounces
1 cup and 2 gallons is 264 ounces

Converting Numbers

If two numbers are of different types, the developer will need to decide which conversion to use. Failing to do so will result in a compilation error.

Numbers can be converted by wrapping the type around the variable or value:

i := int32(42)

The type of i is now an int32.

Compiler Checked

All conversions happen at compile time. If a conversion can not be made, the compiler will throw an error:


package main

import "fmt"

func main() {
	i := 42       // int
	j := int32(i) // int32 - this is a valid conversion

	fmt.Println(i, j)

	pi := 3.14

	area := int(pi) * i

	fmt.Println(area)

	// illegal conversion:
	week := int("7")
}

Output:


./main.go:18:14: cannot convert "7" (untyped string constant) to type int

Valid Conversions

In the following example, we need to convert to the correct types:


package main

import "fmt"

func main() {
	Cup := 8
	Pint := Cup * 2

	Quart := int32(Pint * 2)
	Gallon := int32(Quart * 4)

	fmt.Println("1 cup and 2 quart is", Cup+Quart*2, "ounces")
	fmt.Println("1 cup and 2 gallons is", Cup+Gallon*2, "ounces")
}

Output:


./main.go:12:41: invalid operation: Cup + Quart * 2 (mismatched types int and int32)
./main.go:13:43: invalid operation: Cup + Gallon * 2 (mismatched types int and int32)

To allow two different numeric types to be used in a calculation, you have to explicitly convert each value as necessary:


package main

import "fmt"

func main() {
	Cup := 8
	Pint := Cup * 2

	Quart := int32(Pint * 2)
	Gallon := int32(Quart * 4)

	fmt.Println("1 cup and 2 quart is", Cup+int(Quart)*2, "ounces")
	fmt.Println("1 cup and 2 gallons is", Cup+int(Gallon)*2, "ounces")
}

Output:


1 cup and 2 quart is 72 ounces
1 cup and 2 gallons is 264 ounces

Overflow Vs. Wraparound

Go has the potential to both overflow a number as well as wraparound a number. An overflow happens when you try to store a value larger than the data type was designed to store. When one happens vs. the other is dependent on if the value can be calculated at compile time or at runtime.

At compile time, if the compiler can determine a value will be too large to hold in the data type specified, it will throw an overflow error. This means that the value you calculated to store is too large for the data type you specified.

If we take the following example:


var maxUint8 uint8 = 255 // Max Uint8 size
fmt.Println(maxUint8)

It will compile and run with the following result:

255

If we add 1 to the value at runtime, it will wraparound to 0:


fmt.Println(maxUint8 + 1)

Output:

0

If we change the program to add 1 to the variable when we assign it, it will not compile:


var maxUint8 uint8 = 255 + 1
fmt.Println(maxUint8)

Because the compiler can determine it will overflow the value it will now throw an error:

constant 256 overflows uint8

Understanding the boundaries of your data will help you avoid potential bugs in your program in the future.

Saturation

Go does not saturate variables during mathematical operations such as addition or multiplication. In languages that saturate, if you had a uint8 with a max value of 255, and added 1, the value would still be the max (saturated) value of 255.

In go, however, it will always wrap around. There is no saturation in Go.


var maxUint8 uint8 = 11
maxUint8 = maxUint8 * 25
fmt.Println("new value:", maxUint8)

Output:


new value: 19

Big Numbers

The Go standard library also has support for big numbers via the math/big package.

It has support for:

Int    signed integers
Rat    rational numbers
Float  floating-point numbers

While a deep dive of big numbers it outside the scope of this training, here are some quick examples of how to convert some integer data types:


package main

import (
	"fmt"
	"math/big"
)

func main() {

	bi := big.NewInt(int64(4351454541541445))
	fmt.Println(bi)

	sn, ok := bi.SetString("999888555444777666222", 10)
	if !ok {
		fmt.Println("failed to set string")
	}
	fmt.Println(sn.Uint64())
	fmt.Println(sn)

}

Output:


4351454541541445
3764375464461878958
999888555444777666222

Exercise - Numbers (10m)

You can complete this exercise in the playground, or your local computer.

If you are completing this exercise on your local computer, copy the exercise, create a local file called main.go. Complete the solution, and then see if you have the correct results by running the following command from the same location you saved your main.go file:

go run main.go

Fix the code below by using the proper conversions so the code will compile and execute properly:


package main

import "fmt"

func main() {
	pi := 3.14
	radius := 25

	area := pi * radius * radius

	fmt.Println(area)

	day := 1
	week := uint8(7)

	year := week * day * 52

	fmt.Println(year)

}

Currently, the code does not compile and has the following errors:


./main.go:9:10: invalid operation: pi * radius (mismatched types float64 and int)
./main.go:16:10: invalid operation: week * day (mismatched types uint8 and int)

Solution - Variables

The following is the solution:


package main

import "fmt"

func main() {
	pi := 3.14
	radius := 25

	area := pi * float64(radius) * float64(radius)

	fmt.Println(area)

	day := 1
	week := uint8(7)

	year := int(week) * day * 52

	fmt.Println(year)

}

Running the solution should result in the following output:


1962.5
364

Booleans

The Boolean data type (bool) can be one of two values, either true or false. We use Booleans in programming to make comparisons and to control the flow of the program.

Many operations in math give us answers that evaluate to either True or False:

var ok bool
fmt.Println(ok)

Output:

false

In this example we defined a variable called ok with the data type of bool. When it was printed, we saw the output was false. In Go, all variables have a zero value. In the case of the bool data type, the zero value is false.

To declare a variable and initialize the value, the := operator can be used:

found := true
fmt.Println(found)

Output:

true

The bool data type is useful for making logic decisions in your code.

Strings

A string is a sequence of one or more characters (letters, numbers, symbols) that can be either a constant or a variable. Strings exist within either back quotes ` or double quotes in Go and have different characteristics depending on which quotes you use.

If you use the back quotes, you are creating a raw string literal. If you use the double quotes, you are creating an interpreted string literal.

Raw String Literals

Raw string literals are character sequences between back quotes, often called back ticks. Within the quotes, any character may appear except back quote.

a := `Say "hello" to Go!`

Backslashes have no special meaning inside of raw string literals. Raw string literals may also be used to create multiline strings:

a := `Go is expressive, concise, clean, and efficient.
Its concurrency mechanisms make it easy to write programs
that get the most out of multicore and networked machines,
while its novel type system enables flexible and modular
program construction. Go compiles quickly to machine code
yet has the convenience of garbage collection and the power
of run-time reflection. It's a fast, statically typed,
compiled language that feels like a dynamically typed,
interpreted language.`

Interpreted String Literals

Interpreted string literals are character sequences between double quotes, as in "bar". Within the quotes, any character may appear except newline and unescaped double quote.

a := "Say \"hello\" to Go!"

You will almost always use interpreted string literals because they allow for escape characters within them.

Recommended reading: https://blog.golang.org/strings

UTF-8

Go supports UTF-8 characters out of the box, without any special setup, libaries, or packages.

a := "Hello, 世界"

Runes

A rune is a special type in go that represents special characters.

You can define a rune using the single quote (') character:


a := 'A'
fmt.Println(a)

If you run the program, it prints out the value of 65.

Rune Type

The rune type in go is an alias for the int32 type. This allows for storing up to 4 bytes of data, which allows for much larger values that can map into the UTF-8 space of characters.

Why a special type and not just use an int32? The short answer is that the new type allows the programming language to distinguish between character values and integer values.

The below example shows the value of a simple character and a complex (utf-8) character, as well as the character, integer, and binary value representations.


simple := 'a'
complex := '😻'

fmt.Printf("char\tint\tlen\tbin\n")
fmt.Printf("%[1]s\t%[2]d\t%[3]d\t%[2]b\n", string(simple), simple, len(string(simple)))

char    int     len     bin
a       97      1       1100001
😻      128571  4       11111011000111011

Care Must Be Taken

In many languages, the correct way to iterate over a string would look very much like the following code sample…


a := "Hello, 世界" // 9 characters (including the space and comma)
for i := 0; i < len(a); i++ {
	fmt.Printf("%d: %s\n", i, string(a[i]))
}

But, this will not have the output you would expect:

0: H
1: e
2: l
3: l
4: o
5: ,
6:  
7: ä
8: ¸
9: –
10: ç
11: •
12: Œ

Notice the unexpected characters that were printed out for index 7-12? This is because we were taking part of the rune as an int32, not the entire set of int32’s that make up the rune.

The Right Way

If you intend to walk through each character in a string, the proper way is to use the range keyword in the loop.


a := "Hello, 世界"
for i, c := range a {
	fmt.Printf("%d: %s\n", i, string(c))
}
0: H
1: e
2: l
3: l
4: o
5: ,
6:
7: 世
10: 界

Range ensures that we use the proper index and length of int32’s to capture the proper rune value.

Exercise - Variables (10m)

You can complete this exercise in the playground, or your local computer.

If you are completing this exercise on your local computer, copy the exercise, create a local file called main.go. Complete the solution, and then see if you have the correct results by running the following command from the same location you saved your main.go file:

go run main.go

Complete the TODO sections in the exercise below:


package main

import "fmt"

func main() {
	// TODO: Declare the following variables with zero values:
	// name: valid, type bool
	// name: command, type string

	// TODO: print the value of those variables using the `fmt.Println` function

	// TODO: using short variable declaration, declare a multi-line string variable called "template".  Hint, this will be a `raw` string literal.
	// template should have the value of `autosave=true` on the first line, `backupext=bak` on the second line, and `interval=10s` on the third line.

	// TODO: print the `template` variable

	// TODO: based on the naming styles and rules, correct the following statements
	BookId := 1
	proper_name := "Mr. Rob Pike"
	bodyHtml := `<body>This is the body of the page.  Link to <a href="https://www.gopherguides.com">Gopher Guides</a></body>`

	fmt.Println(BookId)
	fmt.Println(proper_name)
	fmt.Println(bodyHtml)
}

Solution - Variables

The following is the solution:


package main

import "fmt"

func main() {
	var valid bool
	var command string
	fmt.Println(valid)
	fmt.Println(command)

	template := `autosave=true
backupext=bak
interval=10s`

	fmt.Println(template)

	// Acronyms should be capitalized (Id -> ID)
	BookID := 1

	// Underscores are not idiomatic in Go
	properName := "Mr. Rob Pike"

	// Acronyms should be capitalized (Html -> HTML)
	bodyHTML := `<body>This is the body of the page.  Link to <a href="https://www.gopherguides.com">Gopher Guides</a></body>`

	fmt.Println(BookID)
	fmt.Println(properName)
	fmt.Println(bodyHTML)
}

Running the solution should result in the following output:


false

autosave=true
backupext=bak
interval=10s
1
Mr. Rob Pike
<body>This is the body of the page.  Link to <a href="https://www.gopherguides.com">Gopher Guides</a></body>

Constants

Constants are like variables, except they can’t be modified once they have been declared.

Constants can only be a character, string, boolean, or numeric value.

const gopher = "Genny"
fmt.Println(gopher)

Output:

Genny

If you try to modify a constant after it was declared, you will get a compile time error:


const gopher = "Genny"
gopher = "george"
fmt.Println(gopher)

Output:


./main.go:8:9: cannot assign to gopher

Untyped

Constants can be untypted. This can be useful when working with numbers such as integer type data. If the constant is untyped, it is explicitly converted, where typed constants are not.


package main

import "fmt"

const (
	year     = 365        // untyped
	leapYear = int32(366) // typed
)

func main() {
	hours := 24
	minutes := int32(60)
	fmt.Println(hours * year)       // multiplying an int and untyped
	fmt.Println(minutes * year)     // multiplying an int32 and untyped
	fmt.Println(minutes * leapYear) // multiplying both int32 types
}

/*
	output:
	8760
	21900
	21960
*/

Typed

If you declare a constant with a type, it will be that exact type. leapYear was defined as data type int32. This means it can only operate with int32 data types. year was declared with no type, so it is considered untyped. Because of this, you can use it with any integer data type.


package main

import "fmt"

const (
	leapYear = int32(366) // typed
)

func main() {
	hours := 24
	fmt.Println(hours * leapYear) // multiplying int and int32 types}
}

/*
	output:
	invalid operation: hours * leapYear (mismatched types int and int32)
*/

If you try to use a typed constant with anything other than it’s type, Go will throw a compile time error.

Type Inference

Remember that the untyped const or var will be converted to the type it is combined with for any mathematical operation:


const (
	a = 2
	b = 2
	c = int32(2)
)

func main() {
	fmt.Printf("a = %[1]d (%[1]T)\n", a)
	fmt.Printf("b = %[1]d (%[1]T)\n", b)
	fmt.Printf("c = %[1]d (%[1]T)\n", c)

	fmt.Printf("a*b = %[1]d (%[1]T)\n", a*b)
	fmt.Printf("a*c = %[1]d (%[1]T)\n", a*c)

	d := 4
	e := int32(4)

	fmt.Printf("a*d = %[1]d (%[1]T)\n", a*d)
	fmt.Printf("a*e = %[1]d (%[1]T)\n", a*e)
}

Output:


a = 2 (int)
b = 2 (int)
c = 2 (int32)
a*b = 4 (int)
a*c = 4 (int32)
a*d = 8 (int)
a*e = 8 (int32)

Iota

Go’s iota identifier is used in const declarations to simplify definitions of incrementing numbers. Because it can be used in expressions, it provides a generality beyond that of simple enumerations.

The value of iota starts from 0 in each discreet const block.


const (
	Apple int = iota
	Orange
	Banana
)

func main() {
	fmt.Printf("The value of Apple is %v\n", Apple)
	fmt.Printf("The value of Orange is %v\n", Orange)
	fmt.Printf("The value of Banana is %v\n", Banana)
}

Output:


The value of Apple is 0
The value of Orange is 1
The value of Banana is 2

You can use constant shorthand (leaving out everything after the constant name) to declare consecutive numbered constants.

Order Matters With Iota

Changing the order of the defined constants in an iota block will change their values. Therefore it is very important that you understand how this will affect your code before you change the order.


const (
	Apple int = iota
	Banana
	Orange
)

func main() {
	fmt.Printf("The value of Apple is %v\n", Apple)
	fmt.Printf("The value of Orange is %v\n", Orange)
	fmt.Printf("The value of Banana is %v\n", Banana)
}

Output:


The value of Apple is 0
The value of Orange is 2
The value of Banana is 1

Standard Library

Go uses iota in many areas of the standard library.

Here is an excerpt from the time package:

const (
  January Month = 1 + iota
  February
  March
  April
  May
  June
  July
  August
  September
  October
  November
  December
)

Shifting

You can use the shift operator and iota to create bitmasks as well:


package main

import "fmt"

const (
	read   = 1 << iota // 00000001 = 1
	write              // 00000010 = 2
	remove             // 00000100 = 4

	// admin will have all of the permissions
	admin = read | write | remove
)

func main() {
	fmt.Printf("read =  %v\n", read)
	fmt.Printf("write =  %v\n", write)
	fmt.Printf("remove =  %v\n", remove)
	fmt.Printf("admin =  %v\n", admin)
}

Powers

Iota’s can also help define patterns with powers calculations:


package main

import "fmt"

const (
	_  = 1 << (iota * 10) // ignore the first value
	KB                    // decimal:       1024 -> binary 00000000000000000000010000000000
	MB                    // decimal:    1048576 -> binary 00000000000100000000000000000000
	GB                    // decimal: 1073741824 -> binary 01000000000000000000000000000000
)

func main() {
	fmt.Printf("KB =  %v\n", KB)
	fmt.Printf("MB =  %v\n", MB)
	fmt.Printf("GB =  %v\n", GB)
}

Using Iota

While we just went through several examples of iota, in general, it’s best practice to avoid them due to the potential brittle nature that changing the order can create. Even with something as complex as the shifting multiplier example we saw, we could re-write it as just constants and ensure that the code is not brittle:


package main

import "fmt"

const (
	KB = 1024       // binary 00000000000000000000010000000000
	MB = 1048576    // binary 00000000000100000000000000000000
	GB = 1073741824 // binary 01000000000000000000000000000000
)

func main() {
	fmt.Printf("KB =  %v\n", KB)
	fmt.Printf("MB =  %v\n", MB)
	fmt.Printf("GB =  %v\n", GB)
}

To Use Or Not To Use Iota

While the standard library does use Iota (but shockingly lack tests for some basic cases such as month…), in general, it may be more desirable to always use basic constants to avoid future data corruption. For more in depth information about why, read the article How to use Iota in Go

Print

So far we’ve used the fmt package to Print out our values. Since using print is very useful in debugging your code, lets cover several different ways to use it now.

Here are some basic print statements:


package main

import "fmt"

func main() {
	fmt.Println("Print a statement followed by a line return")
	fmt.Printf("Hello %s\n", "gopher")
}

Print a statement followed by a line return
Hello gopher

Println will automatically add a line return, but will not use format verbs

Printf will use format verbs to assemble a string with arguments you pass in Note: You have to use a \n newline symbol if you want to add a line return at the end

Println With Arguments

You can send in as many arguments as you want to the Println function. Println will automatically put spaces between all arguments when it prints them out.


package main

import "fmt"

func main() {
	a := 1
	fmt.Println("This will join all arguments and print them. a =", a)

	fmt.Println("Println will also automatically insert spaced between arguments.", "foo", "bar")

	fmt.Println("So many arguments", "carrot", "onion", "potato", "tomato", "celery", "spinach")
}

This will join all arguments and print them. a = 1
Println will also automatically insert spaced between arguments. foo bar
So many arguments carrot onion potato tomato celery spinach

Verbs

All Printf statements can use verbs. These verbs will create different styles of formatting. Verbs are usually proceeded by a % or \ character.

Use %v to print the value
Use %s to print a string
Use %q to quote a string
Use %d to print a decimal (int, int32, etc)
Use %.2f to fomrmat floating point numbers with a decimal percision of 2
Use %T to print out the data type of the variable
Use \ to escape a character, like a quote:  \"
Use \n to print a new line (line return)
Use \t to insert a tab
Use %+v to print the name and value

Verbs In Action

Here are some examples of using different verbs in print statements.


package main

import "fmt"

func main() {
	a := 1
	fmt.Printf("This is the `format` string. Escape \" and print value of a: %v\n", a)

	type User struct {
		Name string
	}
	u := User{Name: "Mary"}
	fmt.Printf("user: %v\n", u)

	// use the `+` operator to show field names in the struct as well
	fmt.Printf("user: %+v\n", u)
}

This is the `format` string. Escape " and print value of a: 1
user: {Mary}
user: {Name:Mary}

Note: You can use the + operator when formatting structs and it will result in the print statement showing both the field and the value, instead of just the value.

Using Wrong Verbs

Occasionally you may accidentally use the wrong verb which will result in an invalid formatting output.


func main() {
	fmt.Printf("This is an int: %s\n", 42)
	fmt.Printf("This is a string: %d\n", "hello")
}

The desired output is:


This is an int: 42
This is a string: hello

However, because of the invalid formatting directives, this is the actual output:


This is an int: %!s(int=42)
This is a string: %!d(string=hello)

While this won’t hurt the performance of your program, it certainly isn’t desirable.

To avoid this, you should always run go vet on your code


$ go vet main.go
# command-line-arguments
./main.go:6:2: Printf format %s has arg 42 of wrong type int
./main.go:7:2: Printf format %d has arg "hello" of wrong type string

Padding Numbers

A quick trick to pad numbers with fmt:


package main

import "fmt"

func main() {
	fmt.Println("Left pad numbers")
	fmt.Printf("|%06d|%6d|\n\n", 12, 345)

	fmt.Println("Right pad numbers")
	// NOTE: Right padding with trailing zeros does not work...
	fmt.Printf("|%0-6d|%-6d|\n\n", 12, 345)

	fmt.Println("Use a variable of value 6 to define the width")
	width := 6
	fmt.Printf("|%0*d|\n", width, 12)
	fmt.Printf("|%*d|\n\n", width, 12)

	fmt.Println("Use a variable of value 3 to define the width")
	width = 3
	fmt.Printf("|%0*d|\n", width, 12)
	fmt.Printf("|%*d|", width, 12)
}

Left pad numbers
|000012|   345|

Right pad numbers
|12    |345   |

Use a variable of value 6 to define the width
|000012|
|    12|

Use a variable of value 3 to define the width
|012|
| 12|

06 means pad the number up to six places using a leading 0

6 means pad the number up to six places using spaces

Using a - will right justify the number

Using a * allows you to pass a variable for the amount of padding you desire.

Explicit Argument Indexes

The default behavior for print format commands is for each formatting verb to format successive arguments passed into the call.

For instance, we have seen that arguments appear in the order they are passed in:


fmt.Printf("in order: %s %s %s %s\n", "one", "two", "three", "four")

Output:


in order: one two three four

However, you can use the following notation to specify the index of the argument passed in for the format expression provided:


fmt.Printf("explicit: %[4]s %[3]s %[2]s %[1]s\n", "one", "two", "three", "four")

Output:


explicit: four three two one

You can also use the same argument more than once:


fmt.Printf("repeated: %[1]s %[1]s %[2]s %[2]s\n", "one", "two")

Output:


repeated: one one two two

Explicit Argument Example

Normally, you would use the explicit arguments to avoid passing in a variable more than once if you needed to reference it multiple times in the same format directive. This is common when debugging variables to determine both value and type at runtime:


package main

import "fmt"

func main() {
	name := "Rob Pike"
	fmt.Printf("Value of name is %[1]q, type: %[1]T\n", name)

	name = ""
	fmt.Printf("Value of name is %[1]q, type: %[1]T\n", name)
}

Output:


Value of name is "Rob Pike", type: string
Value of name is "", type: string

Using Pretty Print

Sometimes, it’s much easier to just use existing pretty print packages. KR Pretty is one of the most common packages used to print variables debugging.

One of the big differences in behavior between the fmt package and pretty package is how it handles printing out pointer values. Instead of printing the value of the pointer (which is usually meaningless for debugging), it will print the value that is stored at that pointer.


package main

import (
	"fmt"
	"time"

	"github.com/kr/pretty"
)

type User struct {
	ID    int
	Name  string
	Email string
}

func main() {
	// print a struct
	u := User{Name: "Homer Simpson", Email: "homer@example.com"}
	pretty.Println(u)

	// print a slice
	data := []string{"one", "", "32"}
	pretty.Println(data)

	// print time:
	t := time.Now()
	pretty.Println(t)
	fmt.Println(t)

	// embedded pointer in an anonymous struct (we'll cover these in depth later)
	admin := struct {
		permissions map[int]string
		*User
	}{
		map[int]string{1: "admin"},
		&User{
			ID:    2,
			Name:  "Rob Pike",
			Email: "commander@golang.org",
		},
	}

	fmt.Println(admin)
	pretty.Println(admin)

}

Output:


main.User{ID:0, Name:"Homer Simpson", Email:"homer@example.com"}
[]string{"one", "", "32"}
time.Date(2021, time.November, 8, 16, 28, 22, 24381000, time.Local)
2021-11-08 16:28:22.024381 -0600 CST m=+0.000225641
{map[1:admin] 0xc000098360}
struct { permissions map[int]string; *main.User }{
    permissions: {1:"admin"},
    User:        &main.User{ID:2, Name:"Rob Pike", Email:"commander@golang.org"},
}

As you can see, using a package doesn’t always result in the desired affect. The standard library will “pretty print” differently than the KR library. The big difference is that the KR package will try to give you as much internal information as possible.

Install with:

$ go get github.com/kr/pretty
go get: added github.com/kr/pretty v0.3.0

Scanning Data From Strings

In Go, you can also parse data from strings. Parsing a string for multiple different types of variables and data types can be done with the fmt package.

The fmt package has nine ways to scan strings. They are primarily broken into three categories as they pertain to the source of what they are scanning.

To scan from os.Stdin, you can use the Scan, Scanf and Scanln functions.

To scan from a file, you can use the Fscan, Fscanf and Fscanln functions.

And to scan from a string value, you can use the Sscan, Sscanf and Sscanln functions.

As you can see, each function has three different ways to scan. The first (with no prefix such as Scan will treat newlines in the string values as a space. Any of the functions ending in ln will stop scanning at a newline and require that all the items to be followed by a newline or EOF symbol. Finally, the functions ending in f will parse the arguments according to a format string using verbs that we previously discussed in formatting strings.

Sscan

Going through every function variation is beyond the scope of this book, so we will focus primarily on Sscan and Sscanf, as all other variations are a form of this operation.

To scan data from a string, you first have to declare the variables and the types of data you want to scan. The data you are scanning should be space delimited for this function to work to be parsed properly.


package main

import "fmt"

func main() {
	var name string
	var wage float64
	var age int

	n, err := fmt.Sscan("Riley 25.50 35", &name, &wage, &age)

	if err != nil {
		panic(err)
	}

	fmt.Printf("name: %s, wage: $%.2f, age: %d\n", name, wage, age)
	fmt.Printf("%d values successfuly scanned\n", n)
}

Output:


$ go run ./main.go
name: Riley, wage: $25.50, age: 35
3 values successfuly scanned

Sscanf

Sscanf differs from Sscan in that you can give it a format string to match and parse specific values from that string.


package main

import (
	"fmt"
)

func main() {
	var name string
	var wage float64
	var age int

	data := "Riley is 35 and makes $25.50 per hour"
	format := "%s is %d and makes $%f per hour"

	n, err := fmt.Sscanf(data, format, &name, &age, &wage)

	if err != nil {
		panic(err)
	}

	fmt.Printf("name: %s, wage: $%.2f, age: %d\n", name, wage, age)
	fmt.Printf("%d values successfuly scanned\n", n)
}

Output:


$ go run ./main.go
name: Riley, wage: $25.50, age: 35
3 values successfuly scanned

Converting Strings To Numbers

Unlike the fmt package, the strconv package has functions to explicitly try to convert a single value or variable to a number. The most commonly used functions are ParseInt, ParseUint, ParseFloat, and Atoi.

Looking at the documentation for Parseint, we see that you can parse values with different number bases. For our example, we’ll use base 0, which means the parser will use a prefix to determine the base.


$ go doc strconv.Parseint

package strconv // import "strconv"

func ParseInt(s string, base int, bitSize int) (i int64, err error)
    ParseInt interprets a string s in the given base (0, 2 to 36) and bit size
    (0 to 64) and returns the corresponding value i.

    The string may begin with a leading sign: "+" or "-".

    If the base argument is 0, the true base is implied by the string's prefix
    following the sign (if present): 2 for "0b", 8 for "0" or "0o", 16 for "0x",
    and 10 otherwise. Also, for argument base 0 only, underscore characters are
    permitted as defined by the Go syntax for integer literals.

    The bitSize argument specifies the integer type that the result must fit
    into. Bit sizes 0, 8, 16, 32, and 64 correspond to int, int8, int16, int32,
    and int64. If bitSize is below 0 or above 64, an error is returned.

    The errors that ParseInt returns have concrete type *NumError and include
    err.Num = s. If s is empty or contains invalid digits, err.Err = ErrSyntax
    and the returned value is 0; if the value corresponding to s cannot be
    represented by a signed integer of the given size, err.Err = ErrRange and
    the returned value is the maximum magnitude integer of the appropriate
    bitSize and sign.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	i, err := strconv.ParseInt("-42", 0, 64)
	fmt.Println(i)

	if err != nil {
		panic(err)
	}

	x, err := strconv.ParseInt("0x2A", 0, 64)
	fmt.Println(x)

	if err != nil {
		panic(err)
	}

	u, err := strconv.ParseUint("42", 0, 64)
	fmt.Println(u)

	if err != nil {
		panic(err)
	}
	f, err := strconv.ParseFloat("42.234", 64)
	fmt.Println(f)

	if err != nil {
		panic(err)
	}

	a, err := strconv.Atoi("42")
	fmt.Println(a)

	if err != nil {
		panic(err)
	}

}

Output:


$ go run ./main.go
-42
42
42
42.234
42

Atoi stands for Ascii to int

Standard Library

While the Go programming language may be small in size, the standard library is vast and powerful.

It is highly recommended that before starting a project that you review the standard library for packages that may be useful.

For developers new to Go, here are some general packages that are highly recommend to review:

  • fmt - implements formatted I/O with functions analogous to C’s printf and scanf.
  • strconv - Package strconv implements conversions to and from string representations of basic data types.
  • strings - Package strings implements simple functions to manipulate UTF-8 encoded strings.
  • time - Package time provides functionality for measuring and displaying time.

Exercise - Constants (10m)


package main

import "fmt"

// Declare the following untyped constants
// - Cup with a value of 8
// - Pint with a value of 16
// - Quart with a value of 32
// - Gallon with a value of 128

// Declare the following typed constant:
// - Barrel of type int32 with a value of 4032

func main() {
	// Fix the following format directives
	fmt.Printf("there are %v cups in 1 gallon\n", Gallon/Cup)
	fmt.Printf("there are %s pints in 1 gallon\n", Gallon/Pint)
	fmt.Printf("there are %q Quarts in 1 gallon\n", Gallon/Quart)

	// Fix the following statement to properly print the correct value
	fmt.Printf("there are %.1f gallons in a barrel\n", Barrel/Gallon)

	// Fix the following print statements to be left padded with 6 leading zeros
	fmt.Printf("|%d|%d|%d|%d|%d|\n", 25, 523, 9, 1048, 12345)
	fmt.Printf("|%d|%d|%d|%d|%d|\n", 12345, 9, 1048, 523, 24)

	// Fix the following print statements to be right padded with 6 spaces
	fmt.Printf("|%d|%d|%d|%d|%d|\n", 25, 523, 9, 1048, 12345)
	fmt.Printf("|%d|%d|%d|%d|%d|\n", 12345, 9, 1048, 523, 24)

	// correct the order of the arguments in this print statement:
	fmt.Printf("2+2 = %d. The sky is %s. The mascot for Go is a %s", "Gopher", 4, "Blue")
}

You can complete this exercise in the playground, or your local computer.

If you are completing this exercise on your local computer, copy the exercise, create a local file called main.go. Complete the solution, and then see if you have the correct results by running the following command from the same location you saved your main.go file:

go run main.go

Solution - Variables


package main

import "fmt"

// Declare the following untyped constants
// - Cup with a value of 8
// - Pint with a value of 16
// - Quart with a value of 32
// - Gallon with a value of 128
const (
	Cup    = 8
	Pint   = 16
	Quart  = 32
	Gallon = 128
)

// Declare the following typed constant:
// - Barrel of type int32 with a value of 4032
const Barrel = int32(4032)

func main() {
	// Fix the following format directives
	fmt.Printf("there are %d cups in 1 gallon\n", Gallon/Cup)
	fmt.Printf("there are %d pints in 1 gallon\n", Gallon/Pint)
	fmt.Printf("there are %d Quarts in 1 gallon\n", Gallon/Quart)

	// Fix the following statement to properly print the correct value
	fmt.Printf("there are %.1f gallons in a barrel\n", float64(Barrel)/float64(Gallon))

	// Fix the following print statements to be left padded with 6 leading zeros
	fmt.Printf("|%06d|%06d|%06d|%06d|%06d|\n", 25, 523, 9, 1048, 12345)
	fmt.Printf("|%06d|%06d|%06d|%06d|%06d|\n", 12345, 9, 1048, 523, 24)

	// Fix the following print statements to be right padded with 6 spaces
	fmt.Printf("|%-6d|%-6d|%-6d|%-6d|%-6d|\n", 25, 523, 9, 1048, 12345)
	fmt.Printf("|%-6d|%-6d|%-6d|%-6d|%-6d|\n", 12345, 9, 1048, 523, 24)

	// correct the order of the arguments in this print statement:
	fmt.Printf("2+2 = %[2]d. The sky is %[3]s. The mascot for Go is a %[1]s", "Gopher", 4, "Blue")
}

Comments

Comments in Go begin with a set of forward slashes (//) and continue to the end of the line. It is idiomatic to have a white space after the set of forward slashes: (//).

Go has two different comment styles.

// This is a comment

When commenting code, you should be looking to answer the why behind the code as opposed to the what or how. Unless the code is particularly tricky, looking at the code can generally tell what the code is doing or how it is doing it.

Block Comments

You can create block comments two ways in Go. The first is by using a set of double forward slashes and repeating them for every line.

// First line of a block comment
// Second line of a block comment

The second is to use opening tags (/*) and closing tags (*/). For documenting code, it is considered idiomatic to always use // syntax. You would only use the /* ... */ syntax for debugging, which we will cover later.

/*
Everything here
will be considered
a block comment
*/

Here is an example of a block comment that defines what is happening in the MustGet() function defined below:

// MustGet will retrieve a url and return the body of the page.
// If Get encounters any errors, it will panic.
func MustGet(url string) string {
 ...
}

It is common to see block comments at the beginning of exported functions in Go (these comments are also what generate your code documentation). Block comments are also used when operations are less straightforward and are therefore demanding of a thorough explanation. With the exception of documenting functions, you should try to avoid over-commenting the code and should tend to trust other programmers to understand Go unless you are writing for a particular audience.

Inline Comments

Inline comments occur on the same line of a statement, following the code itself. Like other comments, they begin with a set of forward slashes. Again, it’s not required to have a whitespace after the forward slashes, but it is considered idiomatic to do so.

Generally, inline comments look like this:

[code]  // Inline comment about the code

Inline comments should be used sparingly, but can be effective for explaining tricky or non-obvious parts of code. They can also be useful if you think you may not remember a line of the code you are writing in the future, or if you are collaborating with someone who you know may not be familiar with all aspects of the code.

For example, if you don’t use a lot of math in your Go programs, you or your collaborators may not know that the following creates a complex number, so you may want to include an inline comment about that:

z := x % 2  // Get the modulus of x

Inline comments can also be used to explain the reason behind doing something, or some extra information, as in:

x := 8  // Initialize x with an arbitrary number

Comments that are made in line should be used only when necessary and when they can provide helpful guidance for the person reading the program.

Commenting Out Code For Testing

In addition to using comments as a way to document code, you can also use opening tags (/*) and closing tags (*/) to create a block comment. This allows you to comment out code that you don’t want to execute while you are testing or debugging a program you are currently creating. That is, when you experience errors after implementing new lines of code, you may want to comment a few of them out to see if you can troubleshoot the precise issue.

Using the (/*) and (*/) tags can also allow you to try alternatives while you’re determining how to set up your code. Block comments can also be used to comment out code that is failing while you continue to work on other parts of your code.


func main() {
	/*
		In this example, we're commenting out the addTwoNumbers
		function because it is failing, therefore preventing it from executing.
		Only the multiplyTwoNumbers function will run

		a := addTwoNumbers(3, 5)
		fmt.Println(a)

	*/

	m := multiplyTwoNumbers(5, 9)
	fmt.Println(m)
}

Commenting out code should only be done during testing purposes. Do not leave snippets of commented out code in your final program.

GoDoc

Documenting your code is important. When Go was created, this was part of the design consideration. Thus, GoDoc was created to generate your documentation from your comments.

For an in depth look at formatting your comments to create good project documentation, read the Godoc: documenting Go code blog post.

Structs

A struct is a collection of fields, often called members (or attributes).

Structs are used to create custom complex types in Go.

When trying to understand structs, it helps to think of them as a blueprint for what the new type will do.

A struct definition, does NOT contain any data.

Defining A Struct

Structs may have zero or more fields.


type User struct {
	Name  string
	Email string
}

Initializing Structs

Without initial values:

u := User{}

With initial values:

u := User{
 Name:  "Homer Simpson",
 Email: "homer@example.com",
}

Fields can be referenced using a period and the field name.

fmt.Println(u.Name)

You can set as many (or as few) of the field values on a struct at initialization time as you want.

u := User{Email: "marge@example.com"}
u.Name = "Marge Simpson"

Initializing Structs

As we saw in the previous example, you can instantiate and initialize a struct in one line:

u := User{Name:  "Homer Simpson", Email: "homer@example.com"}

We did this by using the field:value syntax. You can also do this without the field name:

u := User{"Homer Simpson","homer@example.com"}

Both of these are valid, however, the later is considered bad practice as it can lead to future undesired refactoring.

If we were to change the struct in the future to this definition:

type User struct {
 ID    int
 Name  string
 Email string
}

The code would now no longer compile and we would get this error:

too few values in User literal

As a result, you may have several areas of your code that now need to be changed because you didn’t use the field:value syntax.

Struct Tags

Struct tags are small pieces of metadata attached to fields of a struct that provide instructions to other Go code that works with the struct.

A struct tag looks like this, with the tag offset with backtick ` characters:

type User struct {
    Name string `example:"name"`
}

Other Go code is then capable of examining these structs and extracting the values assigned to specific keys it requests. Struct tags have no effect on the operation of your code without some other code that examines them.

Struct Tags For Encoding

A common use for struct tags is for encoding the data of your struct to some type of other format. JSON, XML, Protobuf, etc are examples of these types of encoding.

If we look at the following code, we can see that by using the built in encoding/json package that we get less than optimal encoding:


type User struct {
	ID       int
	Name     string
	Phone    string
	Password string
}

func main() {
	u := User{
		ID:       1,
		Name:     "Rob Pike",
		Password: "goIsAwesome",
	}

	err := json.NewEncoder(os.Stdout).Encode(&u)
	if err != nil {
		log.Fatal(err)
	}
}

Output:


{"ID":1,"Name":"Rob Pike","Phone":"","Password":"goIsAwesome"}

While the encoder does emit json, it is not idiomatic. Here are the following problems:

  • Fields are not cased properly (they start with a capital letter)
  • Empty fields are still encoded
  • Sensitive informaiton such as the password was encoded

Using Struct Tags

If we give the proper struct tags, we can tell the encoder how we want the json to be serialized.


type User struct {
	ID       int    `json:"id"`
	Name     string `json:"name"`
	Phone    string `json:"phone,omitempty"`
	Password string `json:"-"`
}

func main() {
	u := User{
		ID:       1,
		Name:     "Rob Pike",
		Password: "goIsAwesome",
	}

	err := json.NewEncoder(os.Stdout).Encode(&u)
	if err != nil {
		log.Fatal(err)
	}
}

Output:


{"id":1,"name":"Rob Pike"}

The encoder now uses the proper cased names we provided in the struct tags. Additionally, we used the special case of omitempty to state that we don’t want the Phone field encoded if the value is empty, as well as the - to direct the encoder to never encode the Password field.

Each package that uses struct tags will have directions on how to specify tags for the proper behavior.

For a in depth look at options the json package uses for struct tags, you can refer to the Marshal Documentation.

Note: Go 1.27 (expected August 2026) ships a new encoding/json/v2 package that changes some behaviors, including making field name matching case-sensitive when decoding. This chapter teaches the original encoding/json package, which remains the default, and the material will be revisited after Go 1.27 ships.

There are many more edge cases and rules when it comes to encoding JSON with structs. We will cover all of those in depth in our “Encoding JSON” chapter.

Printing Structs

Using the %+v verb is helpful in showing the field names within a struct:


type User struct {
	First string
	Last  string
	Email string
}

u := User{First: "Cory", Last: "LaNou", Email: "cory@gopherguides.com"}
fmt.Printf("user: %v\n", u)

// using %+v shows the field names in the struct when it prints out
fmt.Printf("user: %+v\n", u)

Output:


user: {Cory LaNou cory@gopherguides.com}
user: {First:Cory Last:LaNou Email:cory@gopherguides.com}

Anonymous Structs

An anonymous struct is a struct type that is defined without a name. It’s declared and used in the same place.


// Anonymous struct - defined and used in one place
point := struct {
	X int
	Y int
}{
	X: 10,
	Y: 20,
}

fmt.Printf("Point: %+v\n", point)
fmt.Printf("X: %d, Y: %d\n", point.X, point.Y)

Output:


$ go run main.go
Point: {X:10 Y:20}
X: 10, Y: 20

Anonymous Struct Declaration

You can also declare an anonymous struct variable without immediate initialization:


// Anonymous structs can be declared without immediate initialization
var config struct {
	Host string
	Port int
}

config.Host = "localhost"
config.Port = 8080

fmt.Printf("Config: %+v\n", config)

Output:


$ go run main.go
Config: {Host:localhost Port:8080}

Anonymous Structs For JSON Parsing

Anonymous structs are commonly used for one-off JSON parsing when you don’t need a reusable type:


// Anonymous structs are great for one-off JSON parsing
data := []byte(`{"name": "Alice", "age": 30}`)

var person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

if err := json.Unmarshal(data, &person); err != nil {
	log.Fatal(err)
}

fmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age)

Output:


$ go run main.go
Name: Alice, Age: 30

Anonymous Structs In Table-Driven Tests

One of the most common uses of anonymous structs is for table-driven tests:


// Anonymous structs are ideal for table-driven tests
tests := []struct {
	name     string
	a, b     int
	expected int
}{
	{"positive numbers", 2, 3, 5},
	{"negative numbers", -1, -2, -3},
	{"zero", 0, 0, 0},
	{"mixed", -5, 10, 5},
}

for _, tt := range tests {
	result := Add(tt.a, tt.b)
	if result != tt.expected {
		fmt.Printf("FAIL %s: got %d, want %d\n", tt.name, result, tt.expected)
	} else {
		fmt.Printf("PASS %s\n", tt.name)
	}
}

Table-Driven Test Output


$ go run main.go
PASS positive numbers
PASS negative numbers
PASS zero
PASS mixed

This pattern is idiomatic in Go testing and allows easy addition of new test cases.

When To Use Anonymous Structs

Good use cases:

  • One-off JSON parsing/encoding
  • Table-driven tests
  • Temporary data grouping
  • Quick prototyping

Avoid when:

  • The struct is used in multiple places (define a named type)
  • You need methods on the struct
  • The code would be clearer with a named type

Reference Documents

The Go programming language has several good reference documents that can be referred to during your journey in mastering the language:

Exercise - Structs (10m)


package main

// declare a struct called Movie
// add the following fields:
// - Title (string), add a struct tag for the `json` type with a value of "title"
// - Released (bool), add a struct tag for the `json` type with a value of "released"
// - Length (int), add a struct tag for the `json` type with a value of "length"
// validate your struct tags by running `go vet` against your code

func main() {
	// declare a variable called "movie" of type "Movie"

	// Set the Title to "Wizard of Oz"
	// Set the Released variable to "true"
	// Set Length to 125

	// Print the value of "movie" out
	// hint: you can use fmt.Println(movie)
}

Solution - Structs


package main

import "fmt"

// declare a variable called "movie" of type "Movie"
// add the following fields:
// - Title (string), add a struct tag for the `json` type with a value of "title"
// - Released (bool), add a struct tag for the `json` type with a value of "released"
// - Length (int), add a struct tag for the `json` type with a value of "length"
// validate your struct tags by running `go vet` against your code
type Movie struct {
	Title    string `json:"title"`
	Released bool   `json:"released"`
	Length   int    `json:"length"`
}

func main() {
	// declare a variable called "movie"
	var movie Movie

	// Set the Title to "Wizard of Oz"
	// Set the Released variable to "true"
	// Set Length to 125
	movie.Title = "Wizard of Oz"
	movie.Released = true
	movie.Length = 125

	// Print the value of "movie" out
	// hint: you can use fmt.Println(movie)
	fmt.Println(movie)
}

You can vet your struct tags to ensure they are correct with the following command

go vet

Defining Custom Types

The `type` Keyword

The type keyword in Go is used to define new types. You’ve already seen it with structs:

type User struct {
    Name  string
    Email string
}

But type can also create new types based on existing types:

type UserID int
type Email string

This creates distinct types that are not interchangeable with their underlying types.

Defining Custom Types

You can create new types based on any existing type using type NewType ExistingType:


// Define new types based on existing types
type UserID int
type Email string

var id UserID = 42
var email Email = "user@example.com"

fmt.Printf("ID: %v (type: %T)\n", id, id)
fmt.Printf("Email: %v (type: %T)\n", email, email)

Output:


$ go run main.go
ID: 42 (type: main.UserID)
Email: user@example.com (type: main.Email)

Type Definitions Vs Type Aliases

Go has two ways to create new type names:

Type Definition (type Foo Bar) - Creates a new distinct type:

type Celsius float64  // New type, NOT the same as float64

Type Alias (type Foo = Bar) - Creates an alias (same type):

type Fahrenheit = float64  // Just another name for float64

Type Definition Vs Alias Example


// Type definition - creates a NEW distinct type
type Celsius float64

// Type alias - just another name for the same type
type Fahrenheit = float64

var c Celsius = 100.0
var f Fahrenheit = 212.0

// This works - Fahrenheit is just an alias for float64
var temp float64 = f
fmt.Printf("temp from alias: %v\n", temp)

// This DOESN'T work - Celsius is a different type
// temp = c  // compiler error: cannot use c (type Celsius) as type float64

// Need explicit conversion for type definitions
temp = float64(c)
fmt.Printf("temp from conversion: %v\n", temp)

Adding Methods To Custom Types

One powerful reason to define custom types is to add methods:


type Temperature float64

// Methods CAN be defined on custom types
func (t Temperature) Celsius() float64 {
	return float64(t)
}

func (t Temperature) Fahrenheit() float64 {
	return float64(t)*9/5 + 32
}

Using Methods On Custom Types


var temp Temperature = 100.0
fmt.Printf("%.1f°C = %.1f°F\n", temp.Celsius(), temp.Fahrenheit())

// But the underlying type (float64) does NOT get those methods
var f float64 = 100.0
_ = f
// f.Celsius() // ERROR: float64 has no method Celsius

Output:


$ go run main.go
100.0°C = 212.0°F

The underlying type (float64) does NOT gain these methods.

Methods Do NOT Promote

Critical: When you create a new type using type NewType ExistingType, the new type does NOT inherit methods from the base type.


type Distance float64

func (d Distance) String() string {
	return fmt.Sprintf("%.2f meters", d)
}

// Creating a new type based on Distance
type Height Distance

No Method Promotion Example


var d Distance = 100.5
fmt.Println(d.String()) // Works! Distance has String method

var h Height = 180.0
// h.String() // ERROR! Height does NOT have String method
// Methods do NOT promote from Distance to Height

// You must convert to Distance explicitly
fmt.Println(Distance(h).String()) // Works with conversion

Output:


$ go run main.go
100.50 meters
180.00 meters

Type Definitions Vs Embedding

This is different from embedding, where methods DO promote:


// Type definition - methods DON'T promote
type Height Distance // Height does NOT have Distance's methods

// Embedding - methods DO promote
type Person struct {
	Distance // Person DOES have Distance's methods via embedding
}

Comparison In Action


var h Height = 100
// h.String() // ERROR - Height has no String method
fmt.Printf("Height: %v (no String method)\n", h)

p := Person{Distance: 100}
fmt.Printf("Person: %s\n", p.String()) // Works! Method promoted from embedded Distance

Output:


$ go run main.go
Height: 100 (no String method)
Person: 100.00 meters

Type Safety With Custom Types

Custom types provide compile-time type safety - you can’t accidentally mix values:


type UserID int
type ProductID int

func GetUser(id UserID) {
	fmt.Printf("Getting user with ID: %d\n", id)
}

func GetProduct(id ProductID) {
	fmt.Printf("Getting product with ID: %d\n", id)
}

Type Safety Example


var uid UserID = 1
var pid ProductID = 1

GetUser(uid)    // Works
GetProduct(pid) // Works

// GetUser(pid) // Compiler error: cannot use pid (type ProductID) as type UserID
// GetProduct(uid) // Compiler error: cannot use uid (type UserID) as type ProductID

// Even though both are "1", they are different types!
// This prevents accidentally mixing user IDs with product IDs

Even though both UserID and ProductID are based on int, they cannot be mixed!

When To Use Custom Types

Use custom types for:

  1. Type Safety - Prevent mixing similar values (UserID vs ProductID)
  2. Method Attachment - Add behavior to primitive types
  3. Domain Modeling - Express intent clearly (Celsius vs Fahrenheit)
  4. Interface Implementation - Implement interfaces on primitives

Remember:

  • Type definitions create new distinct types
  • Type aliases are just alternative names
  • Methods do NOT promote from base types (unlike embedding)