A Tutorial for Learning Structs in Go
Updated by Linode Contributed by Mihalis Tsoukalos
Introduction
Go’s array, slice, and map types can be used to group multiple elements, but they cannot hold values of multiple data types. When you need to group different types of variables and create new data types, you can use structs.
NoteGo does not have a concept of classes from other object oriented languages. Structs will be used in similar ways as classes, with important differences. For example, there is no class inheritance feature in Go.
In this guide you will:
- Review a simple introductory struct and learn about basic struct semantics.
- Find out how to use pointers to structs.
- Implement methods with structs.
- Explore the different ways to instantiate structs.
- Read through an example of how to encode and decode JSON with structs.
Before You Begin
To run the examples in this guide, your workstation or server will need to have Go installed, and the go
CLI will need to be set in your terminal’s PATH:
- If you use Ubuntu, follow our How to Install Go on Ubuntu guide.
- Follow the Getting Started guide on Golang’s website to install on other operating systems.
If you prefer to experiment with Go without installing it first, you can run the examples found in this guide using the Go Playground.
An introductory-level knowledge of Go is assumed by this guide. If you’re just getting started with Go, check out our Learning Go Functions, Loops, and Errors tutorial.
NoteThis guide was written with Go version 1.13.
A Simple Struct
The various elements of a struct are called the fields of the struct. The following Go program defines and uses a new struct type called Employee
, which is composed of an employee’s first name and their employee ID. The program then instantiates this type:
- employee.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func main() { var nathan Employee fmt.Println(nathan) nathan = Employee{FirstName: "Nathan", employeeID: 8124011} fmt.Println(nathan) var heather Employee = Employee{FirstName: "Heather"} fmt.Println(heather) mihalis := Employee{"Mihalis", 1910234} fmt.Println(mihalis) }
NoteStructs, in particular, and Go types, in general, are usually defined outside themain()
function in order to have a global scope and be available to the entire Go package, unless you want to clarify that a type is only useful within the current scope and is not expected to be used elsewhere in your code.
The output of employee.go
will be:
go run employee.go
{ 0}
{Nathan 8124011}
{Heather 0}
{Mihalis 1910234}
The example illustrates some (but not all) of the ways a struct can be created:
When the variable
nathan
is defined, it is not assigned a value. Go will assign the default zero value to any fields that are not given values. For a string, the zero value is the empty string, which is why a blank space appears to the left of the0
in the first line of the output.One way to create a struct is to use a struct literal, as shown on line 15. When using a struct literal, you supply a comma-delimited list of the field names and the values they should be assigned.
When using a struct literal in this way, you do not need to specify all of the fields, as shown on line 18. Because the employeeID for
heather
was not defined, it takes on the zero value (for an integer, this is0
).Lastly, you can also use a struct literal without listing the fields’ names, as shown on line 21. The values for the fields will be assigned according to the order that the fields are defined in the struct type definition. You must supply values for all of the fields in order to use this syntax.
Note
Themihalis
variable is defined using the:=
syntax, which infers theEmployee
type for the variable from the assigned value.
Comparing Structs
Structs can be compared for equality. Two structs are equal if they have the same type and if their fields’ values are equal.
- employee.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func main() { employee1 := Employee{"Heather", 1910234} employee2 := Employee{"Heather", 1910234} fmt.Println(employee1 == employee2) }
The output of employee.go
will be:
go run employee.go
true
NoteStructs cannot be ordered with operators like greater-than>
or less-than<
.
Accessing Fields
You can access a specific field using the struct variable name followed by a .
character followed by the name of the field (also referred to as dot notation). Given an Employee
variable named mihalis
, the struct’s two fields can be individually accessed as mihalis.FirstName
and mihalis.employeeID
:
- employee.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func main() { mihalis := Employee{"Mihalis", 1910234} fmt.Println("My name is", mihalis.FirstName, "and my employee ID is", mihalis.employeeID) }
The output of employee.go
will be:
go run employee.go
My name is Mihalis and my employee ID is 1910234
Public and Private Fields
In order to be able to use a struct and its fields outside of the Go package where the struct type is defined, both the struct name and the desired field names must begin with an uppercase letter. Therefore, if a struct has some field names that begin with a lowercase letter, then these particular fields will be private to the Go package that the struct is defined. This is a global Go rule that also applies to functions and variables.
To illustrate, consider these two Go files:
- employee/employee.go
-
1 2 3 4 5 6
package employee type Employee struct { FirstName string employeeID int }
- main.go
-
1 2 3 4 5 6 7 8 9 10 11
package main import ( "fmt" . "./employee" ) func main() { mihalis := Employee{"Mihalis", 1910234} fmt.Println("My name is", mihalis.FirstName, "and my employee ID is", mihalis.employeeID) }
NoteIn this example,employee.go
is created within anemployee
directory.
The output of main.go
will be:
go run main.go
# command-line-arguments
./main.go:9:31: implicit assignment of unexported field 'employeeID' in employee.Employee literal
./main.go:10:80: mihalis.employeeID undefined (cannot refer to unexported field or method employeeID)
This error reflects the fact that employeeID
has a lowercase name and is not an exported field of the Employee
struct.
Value Semantics
By default, when a struct is assigned to a variable, it is copied. Consider this example:
- employee.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func main() { employee1 := Employee{"Nathan", 8124011} fmt.Println("employee1:", employee1) employee2 := employee1 employee2.FirstName = "Andy" employee2.employeeID = 1231410 employee1.FirstName = "Nate" fmt.Println("employee1:", employee1) fmt.Println("employee2:", employee2) }
The output of employee.go
will be:
go run employee.go
employee1: {Nathan 8124011}
employee1: {Nate 8124011}
employee2: {Andy 1231410}
The employee2 := employee1
assignment creates a copy of employee1
and saves it in employee2
. Changing the employee1
variable will not affect the contents of employee2
after the assignment.
Value Semantics with Functions
A struct can be passed to a function. By default, the struct will be copied to its function argument variable. Consider this example:
- employee.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func ChangeEmployeeID(e Employee, newID int) { e.employeeID = newID } func main() { employee1 := Employee{"Nathan", 8124011} fmt.Println(employee1) ChangeEmployeeID(employee1, 1012843) fmt.Println(employee1) }
The output of employee.go
will be:
go run employee.go
{Nathan 8124011}
{Nathan 8124011}
Calling the ChangeEmployeeID
function has no effect on the value of employee
outside of the function scope. As a result, the output of the print statement on line 20 will be the same as the output of line 18’s print statement.
Pointers and Structs
As Go supports pointers, you can create pointers to structs. The use of pointer structs is illustrated in pointers.go
.
- pointers.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func main() { var employeePointer1 *Employee = &Employee{"Nathan", 1201921} fmt.Println("Getting a specific struct field:", (*employeePointer1).FirstName) fmt.Println("With implicit dereferencing:", employeePointer1.FirstName) employeePointer2 := employeePointer1 employeePointer2.FirstName = "Nate" fmt.Println("FirstName for employeePointer2:", employeePointer2.FirstName) fmt.Println("FirstName for employeePointer1:", employeePointer1.FirstName) }
The output of pointers.go
will be:
go run pointers.go
Getting a specific struct field: Nathan
With implicit dereferencing: Nathan
FirstName for employeePointer2: Nate
FirstName for employeePointer1: Nate
employeePointer1
points to the memory location of the struct created with the struct literal on line 13. Inserting an ampersand (&
) before the struct literal (e.g. Employee{"Nathan", 1201921}
) indicates that the memory location for it should be assigned.
Line 14 shows how to dereference the pointer by inserting a *
before the variable name, which tells Go to return the struct located at the memory location of your pointer. Surrounding this with parentheses and then using dot notation (e.g. (*employeePointer1).FirstName
) allows you to access fields within the struct.
However, Go allows you to implicitly dereference a pointer to a struct in this circumstance. This means that you can simply use normal dot notation (e.g. employeePointer1.FirstName
) to access fields, even if your struct variable is a pointer.
Lines 17-20 show that creating a second pointer to a struct allows you to manipulate that struct from another variable. In this case, the value of the FirstName
field for employeePointer1
has been updated after it was assigned through employeePointer2
on line 18. This is in contrast with the value semantics demonstrated previously.
Pointers and Structs and Functions
Passing a pointer to a struct as an argument to a function will allow you to mutate that struct from inside the function scope. Consider this example:
- pointers.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func ChangeEmployeeID(e *Employee, newID int) { e.employeeID = newID } func main() { employeePointer1 := &Employee{"Nathan", 8124011} fmt.Println(*employeePointer1) ChangeEmployeeID(employeePointer1, 1012843) fmt.Println(*employeePointer1) }
The output of pointers.go
will be:
go run pointers.go
{Nathan 8124011}
{Nathan 1012843}
Alternatively, using this code in the main
function instead will produce identical results:
- pointers.go
-
1 2 3 4 5 6
func main() { employee1 := Employee{"Nathan", 8124011} fmt.Println(employee1) ChangeEmployeeID(&employee1, 1012843) fmt.Println(employee1) }
Methods
Go methods allow you to associate functions with structs. A method definition looks like other function definitions, but it also includes a receiver argument. The receiver argument is the struct that you wish to associate the method with.
Once defined, the method can be called using dot-notation on your struct variable. Here’s an example of what this looks like:
- method.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func (e Employee) PrintGreeting() { fmt.Println("My name is", e.FirstName, "and my employee ID is", e.employeeID) } func main() { employee1 := Employee{"Nathan", 8124011} employee1.PrintGreeting() }
The output of method.go
will be:
go run method.go
My name is Nathan and my employee ID is 8124011
The receiver argument is listed in parentheses, prior to the function name, and has the syntax (variableName Type)
; see line 12 for an example of this.
Pointers and Methods
Using a pointer as the receiver type will allow you to mutate the pointed-to struct from within the method’s scope:
- method.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func (e *Employee) ChangeEmployeeID(newID int) { e.employeeID = newID } func main() { var employeePointer1 *Employee = &Employee{"Nathan", 8124011} fmt.Println(*employeePointer1) employeePointer1.ChangeEmployeeID(1017193) fmt.Println(*employeePointer1) }
The output of method.go
will be:
go run method.go
{Nathan 8124011}
{Nathan 1017193}
You can also call a method with a pointer-type receiver on a normal non-pointer struct variable. Go will automatically convert the non-pointer struct variable to its memory location, and the struct will still be mutated within the function scope. This example will produce identical results to the one above:
- method.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func (e *Employee) ChangeEmployeeID(newID int) { e.employeeID = newID } func main() { var employee1 Employee = Employee{"Nathan", 8124011} fmt.Println(employee1) employee1.ChangeEmployeeID(1017193) fmt.Println(employee1) }
Creating Structs
In addition to the struct literal syntax used so far, there are a few other common ways to create a struct:
Constructor Functions
One common pattern for creating structs is with a “constructor” function. In Go, this is just a normal function that returns a struct, or a pointer to a struct. This example will demonstrate returning a pointer to a struct:
- constructor.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
package main import ( "fmt" ) type Employee struct { FirstName string employeeID int } func NewEmployee(name string, employeeID int) *Employee { if employeeID <= 0 { return nil } return &Employee{name, employeeID} } func main() { employeePointer1 := NewEmployee("Nathan", 8124011) fmt.Println(*employeePointer1) }
This approach for creating new struct variables allows you to check whether the provided information is correct and valid in advance; for example, the above code checks the passed employeeID
from lines 13 to 15. Additionally, with this approach you have a central point where struct fields are initialized, so if there is something wrong with your fields, you know exactly where to look.
NoteFor those of you with a C or C++ background, it is perfectly legal for a Go function to return the memory address of a local variable. Nothing gets lost, so everybody is happy!
Using the new Keyword
Go supports the new
keyword that allows you to allocate new objects with the following syntax:
-
1
variable := new(StructType)
new
has these behaviors:
new
returns the memory address of the allocated object. Put simply,new
returns a pointer.new
allocates zeroed storage.Note
Usingnew
with a struct type is similar to assigningstructType{}
to a variable. In other words,t := new(Telephone)
is equivalent tot := Telephone{}
.
The following code example explores this behavior in more depth:
- new.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
package main import ( "encoding/json" "fmt" ) func prettyPrint(s interface{}) { p, _ := json.MarshalIndent(s, "", "\t") fmt.Println(string(p)) } type Contact struct { Name string Main Telephone Tel []Telephone } type Telephone struct { Mobile bool Number string } func main() { contact := new(Contact) telephone := new(Telephone) if contact.Main == (Telephone{}) { fmt.Println("contact.Main is an empty Telephone struct.") } fmt.Println("contact.Main") prettyPrint(contact.Main) if contact.Tel == nil { fmt.Println("contact.Tel is nil.") } fmt.Println("contact") prettyPrint(contact) fmt.Println("telephone") prettyPrint(telephone) }
NoteTheprettyPrint()
function is just used for printing the contents of a struct in a readable and pleasant way with the help of thejson.MarshalIndent()
function.
Executing new.go
will generate the following output:
go run new.go
contact.Main is an empty Telephone struct.
contact.Main
{
"Mobile": false,
"Number": ""
}
contact.Tel is nil.
contact
{
"Name": "",
"Main": {
"Mobile": false,
"Number": ""
},
"Tel": null
}
telephone
{
"Mobile": false,
"Number": ""
}
- As
Record.Tel
is a slice, its zero value isnil
. Lines 34-36 show that comparing it tonil
returns true. Record.Main
is aTelephone
struct, so it cannot be compared tonil
– it can only be compared toTelephone{}
, as demonstrated in lines 28-30.
Structs and JSON
Structs are really handy when we have to work with JSON data. This section is going to present a simple example where a struct is used for reading a text file that contains data in the JSON format and for creating data in the JSON format.
- json.go
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
package main import ( "encoding/json" "fmt" "os" ) type Record struct { Name string Surname string Tel []Telephone } type Telephone struct { Mobile bool Number string } func loadFromJSON(filename string, key interface{}) error { in, err := os.Open(filename) if err != nil { return err } decodeJSON := json.NewDecoder(in) err = decodeJSON.Decode(key) if err != nil { return err } in.Close() return nil } func saveToJSON(filename *os.File, key interface{}) { encodeJSON := json.NewEncoder(filename) err := encodeJSON.Encode(key) if err != nil { fmt.Println(err) return } } func main() { arguments := os.Args if len(arguments) == 1 { fmt.Println("Please provide a filename!") return } filename := arguments[1] var myRecord Record err := loadFromJSON(filename, &myRecord) fmt.Println("JSON file loaded into struct:") if err == nil { fmt.Println(myRecord) } else { fmt.Println(err) } myRecord = Record{ Name: "Mihalis", Surname: "Tsoukalos", Tel: []Telephone{Telephone{Mobile: true, Number: "1234-5678"}, Telephone{Mobile: true, Number: "6789-abcd"}, Telephone{Mobile: false, Number: "FAVA-5678"}, }, } fmt.Println("struct saved to JSON:") saveToJSON(os.Stdout, myRecord) }
The
loadFromJSON()
function is used for decoding the data of a JSON file according to a data structure that is given as the second argument to it.- We first call
json.NewDecoder()
to create a new JSON decoder variable that is associated with a file. - We then call the
Decode()
function for actually decoding the contents of the file and putting them into the desired variable. The function uses the empty interface type (
interface{}
) in order to be able to accept any data type.Note
You will learn more about interfaces in a forthcoming guide.
- We first call
The
saveToJSON()
function creates a JSON encoder variable namedencodeJSON
, which is associated with a filename, which is where the data is going to be put.- The call to
Encode()
is what puts the data into the desired file after encoding it. - In this example,
saveToJSON()
is called usingos.Stdout
, which means that data is going to standard output. - Last, the
myRecord
variable contains sample data using theRecord
andTelephone
structs defined at the beginning of the program. It is the contents of themyRecord
variable that are processed bysaveToJSON()
.
- The call to
Run the JSON Example
For the purposes of this section we are going to use a simple JSON file named record.json
that has the following contents:
- record.json
-
1 2 3 4 5 6 7 8 9
{ "Name":"Mihalis", "Surname":"Tsoukalos", "Tel":[ {"Mobile":true,"Number":"1234-567"}, {"Mobile":true,"Number":"1234-abcd"}, {"Mobile":false,"Number":"abcc-567"} ] }
Executing json.go
and processing the data found in record.json
will generate the following output:
go run json.go record.json
{Mihalis Tsoukalos [{true 1234-567} {true 1234-abcd} {false abcc-567}]}
{"Name":"Mihalis","Surname":"Tsoukalos","Tel":[{"Mobile":true,"Number":"1234-5678"},{"Mobile":true,"Number":"6789-abcd"},{"Mobile":false,"Number":"FAVA-5678"}]}
Next Steps
Structs are a versatile Go data type because they allow you to create new types by combining existing data types. If you feel confident in the topics covered in this tutorial, try exploring our other guides on the Go language.
More Information
You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.
Join our Community
Find answers, ask questions, and help others.
This guide is published under a CC BY-ND 4.0 license.