Getting started with Go: Structs, Pointers and Interfaces

Mike Dyne
Evendyne
Published in
5 min readDec 29, 2022

In this article, we will explore some of the fundamental concepts of Go, including structs, pointers, and interfaces, and how they can be used to build applications. Structs allow you to define composite types, pointers allow you to reference values in memory, and interfaces allow you to define a set of methods that a type must implement. Understanding these concepts is essential for becoming proficient in Go.

A simple interface definition in Go

Structs

Structs in Go are used to define composite types, which allow you to group together related values and create custom data types. Structs are similar to classes in other languages, but they do not support inheritance or polymorphism.

A struct is defined using the type and struct keywords, followed by a set of field names and their associated types, enclosed in curly braces. Here is an example of a struct that represents a point in two-dimensional space:

type Point struct {
X int64
Y int64
}

You can create a new instance of a struct by using the new keyword, followed by the name of the struct type. For example:

p := new(Point)

In Go, you can define a custom new function to allocate memory for a new instance of a type. This is often used as an alternative to the built-in new function, which returns a pointer to a newly allocated zero value of the type:

type MyType struct {
// fields
}

func newMyType() *MyType {
t := &MyType{}
// Initialize fields
return t
}

You can then use this function to create a new instance of MyType like this:

myType := newMyType()

This approach can be useful if you want to initialize the fields of the new instance with non-zero values.

Alternatively, you can use the shorthand syntax := to create and initialize a struct in one line:

p := Point{X: 10, Y: 20}

You can access the fields of a struct using the dot notation. For example:

fmt.Println(p.X) // prints 10

You can also modify the fields of a struct using the same notation:

p.X = 30

It’s worth noting that Go also supports anonymous fields, which allow you to embed one struct type into another without giving it a name. This can be useful for creating more compact and reusable structs, as well as for creating inheritance-like relationships between structs.

For example, you might define a Shape struct that includes an anonymous field of type Point, which represents the coordinates of the shape:

type Shape struct {
Point
Color string
}

You can then create a new Shape instance and access the fields of the embedded Point struct using the dot notation:

s := Shape{Point{X: 10, Y: 20}, "red"}
fmt.Println(s.X, s.Y, s.Color) // prints 10 20 red

In addition to anonymous fields, Go also supports methods on struct types, which allow you to define behavior for your custom data types. The so-called receiver functions are defined using the func keyword and have a special receiver parameter, which is written between the func keyword and the method name. The receiver parameter specifies the struct that the method is attached to and can be accessed using the . notation.

For example, you might define a method on the Shape struct that calculates the area of the shape:

func (s *Shape) Area() int64 {
return s.X * s.Y
}

You can then call the Area method on a Shape instance:

area := s.Area()
fmt.Println(area) // prints 200

Pointers

Pointers in Go allow you to reference a value stored in memory. A pointer holds the memory address of a value. You can create a pointer to a value by using the & operator, followed by the name of the value. For example:

x := 10
ptr := &x

You can access the value of a pointer (dereference it) by using the * operator, followed by the name of the pointer. For example:

fmt.Println(*ptr) // prints 10

You can also modify the value of a pointer by using the * operator:

*ptr = 20

Pointers are often used with structs to modify the fields of a struct. For example:

p := Point{X: 10, Y: 20}
ptr := &p
ptr.X = 30

In Go, when you pass a variable to a function as an argument, the function receives a copy of the variable, rather than a reference to the original. This behaviour is called pass-by-value.

func addOne(x int64) int64 {
x++
return x
}

func main() {
x := 5
fmt.Println(addOne(x)) // Output: 6
fmt.Println(x) // Output: 5
}

Here the addOne function receives a copy of the x variable when it is called. When the function increments x, it is only modifying the copy of the variable, not the original. As a result, the value of x in the main function is unchanged.

You can also pass a variable to a function by reference (pass-by-reference), rather than by value, using pointers. This allows the function to modify the original variable, rather than just a copy of it.

func addOne(p *int64) {
*p++
}

func main() {
x := 5
addOne(&x)
fmt.Println(x) // Output: 6
}

In this snippet above, the addOne function receives a pointer to an int64 as an argument. The addOne function increments the value of x by one by incrementing *p.

Pointers in Go are type-safe, which means that you can only dereference a pointer of the correct type. This helps prevent type-related bugs in your code. In Go, slices, maps, and channels are all reference types, which means that they are passed by reference when used as function arguments. However, they are not pointers in the same way that a pointer to a struct is.

Interfaces

Interfaces in Go allow you to define a set of methods that a type must implement. An interface is defined using the type and interface keywords, followed by a set of method names and their associated types, enclosed in curly braces. Here is an example of an interface that defines a simple "writer" interface:

type Writer interface {
Write([]byte) (int, error)
}

A type can implement an interface by defining all of the methods in the interface. For example:

type File struct {
// ...
}

func (f *File) Write(p []byte) (int, error) {
// ...
}

You can use an interface value to call the methods defined in the interface. For example:

w := &File{} // implements Writer interface
w.Write([]byte("hello, world"))

Interfaces are a key feature of Go’s type system and are used extensively in the standard library and in many popular Go packages.

They provide a way to abstract away the details of a type and allow you to write code that is flexible and easy to maintain. Interfaces can also be used to mock out dependencies in tests, making it easier to test individual components in isolation. Also, Polymorphism can be achieved in Go with using interfaces: writing code that is flexible and can work with multiple types. For example, you can write a function that takes an interface as an argument, and then call that function with different types that implement the interface. Interfaces can also be used to decouple components and make them more modular, allowing you to easily swap out different implementations at runtime.

That’s A Wrap

I hope this gives you a better understanding of how structs, pointers, and interfaces work in Go, as well as some of the ways in which they can be used to build programs. In the next article, we’ll explore error handling.

You can find more about Evendyne here.

--

--

Mike Dyne
Evendyne

I write articles in various software engineering topics. Read every story from me by subscribing here: https://medium.com/@mikedyne/membership