Editor’s note: This article was reviewed on 14 January 2022 to update outdated information and to add the section “Convert an interface to a struct in Golang.”
Go is a type-safe, statically typed, compiled programming language. The type system, with types denoted by type names and type declarations, is designed to prevent occurrences of unchecked runtime type errors.
In Go, there are several built-in types for identifiers, also known as predeclared types. They include Boolean, string, numeric (float32
, float64
, int
, int8
, int16
, int32
, complex
), and so many other types. Additionally, there are composite types, which are composed from predeclared types.
Composite types are mainly constructed using type literals. They include arrays, slices, interfaces, structs, functions, map types, and more. In this article, we’ll focus on struct
and interface
types in Go.
In this tutorial, we’ll cover the following:
To easily follow along with this tutorial, it is important to have a basic understanding of Go. It is advisable to already have Go installed on your machine to run and compile the code.
However, for the sake of simplicity and for the purpose of this post, we will use the Go Playground, an online IDE for running Go code.
Go is a modern, fast, and compiled language (that is, machine code generated from source code). With support for concurrency out of the box, it is also applicable in areas relating to low-level computer networking and systems programming.
To explore some of its features, let us go ahead and learn how to set up our development environment. To do so, install the Go binaries based on your operating systems.
The Go workspace folder contains the bin
, pkg
, and src
directories. In earlier Go versions (pre-version 1.13), source code was written inside the src
directory, which contains Go source files because it needs a way to find, install, and build source files.
This requires us to set the $GOPATH
environment variable on our development machines, which Go uses to identify the path to the root folder of our workspace.
Therefore, to create a new directory inside our workspace, we must specify the full path like this:
$ mkdir -p $GOPATH/src/github.com/firebase007
$GOPATH
can be any path on our machine, usually $HOME/go
, except the path to the Go installation on our machine. Inside the specified path above, we can then have package directories and, subsequently, .go
files in that directory.
The bin
directory contains executable Go binaries. The go
toolchain, with its sets of commands, builds and installs binaries into this directory. The tool offers a standard way of fetching, building, and installing Go packages.
The pkg
directory is where Go stores a cache of precompiled files for the subsequent compilation. More detailed information on how to write Go code with $GOPATH
can be found here.
Note that, however, in newer Go versions, specifically from 1.13 and onward, Go introduced Go modules with the go.mode
file, which we’ll review in the next section.
Programs are grouped as packages for encapsulation, dependency management, and reusability. Packages are source files stored in the same directory and compiled together.
They are stored inside a module, where a module is a group of related Go packages that performs specific operations.
Note that a Go repository typically contains only one module, which is located at the root of the repository. However, a repository can also contain more than one module.
Nowadays, with the introduction of Go modules in version 1.13 and above, we can run and compile a simple Go module or program like this:
retina@alex Desktop % mkdir examplePackage // create a directory on our machine outside $GOPATH/src retina@alex Desktop % cd examplePackage // navigate into that directory retina@alex examplePackage % go mod init github.com/firebase007/test // choose a module path and create a go.mod file that declares that path go: creating new go.mod: module github.com/firebase007/test retina@Terra-011 examplePackage % ls go.mod
Assuming test
is the name of our module above, we can go ahead and create a package directory and create new files inside the same directory. Let’s look at a simple example below:
retina@alex examplePackage % mkdir test retina@alex examplePackage % ls go.mod test retina@alex examplePackage % cd test retina@alex test % ls retina@alex test % touch test.go retina@alex test % ls test.go retina@alex test % go run test.go Hello, Go retina@alex test %
The sample code inside the test.go
file is shown below:
package main // specifies the package name import "fmt" func main() { fmt.Println("Hello, Go") }
Note that the go.mod
file declares the path to a module, which also includes the import path prefix for all packages within the module. This corresponds to its location inside a workspace or in a remote repository.
Just like the type system in other languages, Go’s type system specifies a set of rules that assign a type property to variables, functions declarations, and identifiers. Types in Go can be grouped into the following categories below:
String types represent a set of string values, which is a slice of bytes in Go. They are immutable or read-only once created. Strings are defined types because they have methods attached to them
Boolean types are denoted by the predeclared constants true
and false
.
Numeric types represent sets of integer or floating-point values. They include uint8
(or byte
), uint16
, uint32
, uint64
, int8
, int16
, int32
(or rune
), int64
, float32
, float64
, complex64
, and complex128
.
These types are further categorized into signed integers, unsigned integers, and real and complex numbers. They are available in different sizes and are mostly platform-specific. More details about numeric types can be found here.
An array type is a numbered collection of elements of the same type. Basically, they are building blocks for slices.
Arrays are values in Go, which means that when they are assigned to a variable or passed as an argument to a function, their original values are copied, not their memory addresses.
A slice is just a segment of an underlying array, or, basically, references to an underlying array. []T
is a slice with elements of type T
.
A pointer type is a reference type that denotes the set of all pointers to variables of a given type. Generally, pointer types hold a memory address of another variable. The zero value of a pointer is nil
More details about other types, like maps, functions, channels, and more, can be found in the types section of the language spec. As mentioned earlier, we are going to focus on the interface and struct types in this article.
Go has struct types that contain fields of the same or different types. Structs are basically a collection of named fields that have a logical meaning or construct, wherein each field has a specific type.
We can liken structs to objects or structures comprising of different fields.
Generally, struct types are combinations of user-defined types. They are specialized types because they allow us to define custom data types in such cases where the built-in types are not sufficient.
Let’s use an example to better understand this. Let’s say we have a blog post that we intend to publish. Using a struct type to represent the data fields would look like this:
type blogPost struct { author string // field title string // field postId int // field } // Note that we can create instances of a struct types
In the above struct definition, we added different field values. Now, to instantiate or initialize the struct using a literal, we can do the following:
package main import "fmt" type blogPost struct { author string title string postId int } func NewBlogPost() *blogPost { return &blogPost{ author: "Alexander", title: "Learning structs and interfaces in Go", postId: 4555, } } func main() { var b blogPost // initialize the struct type fmt.Println(b) // print the zero value newBlogPost := *NewBlogPost() fmt.Println(newBlogPost) // alternatively b = blogPost{ // author: "Alex", title: "Understand struct and interface types", postId: 12345, } fmt.Println(b) } //output {Alexander Learning structs and interfaces in Go 4555} { 0} // zero values of the struct type is shown {Alex Understand struct and interface types 12345}
Here is a link to the playground to run the above code.
We can also use the dot, .
, operator to access individual fields in the struct type after initializing them. Let’s see how we would do that with an example:
package main import "fmt" type blogPost struct { author string title string postId int } func main() { var b blogPost // b is a type Alias for the BlogPost b.author= "Alex" b.title="understand structs and interface types" b.postId=12345 fmt.Println(b) b.author = "Chinedu" // since everything is pass by value by default in Go, we can update this field after initializing - see pointer types later fmt.Println("Updated Author's name is: ", b.author) }
Again, here is a link to run the code snippet above in the playground. Further, we can use the short literal notation to instantiate a struct type without using field names, as shown below:
package main import "fmt" type blogPost struct { author string title string postId int } func main() { b := blogPost{"Alex", "understand struct and interface type", 12345} fmt.Println(b) }
Note that with the approach above, we must always pass the field values in the same order in which they are declared in the struct type. Also, all the fields must be initialized.
Finally, if we have a struct type to use only once inside a function, we can define them inline, as shown below:
package main import "fmt" type blogPost struct { author string title string postId int } func main() { // inline struct init b := struct { author string title string postId int }{ author: "Alex", title:"understand struct and interface type", postId: 12345, } fmt.Println(b) }
Note that we can also initialize struct types with the new
keyword. In that case, we can do the following:
b := new(blogPost)
Then, we can use the dot, .
, operator to set and get the values of the fields, as we saw earlier. Let’s see an example:
package main import "fmt" type blogPost struct { author string title string postId int } func main() { b := new(blogPost) fmt.Println(b) // zero value b.author= "Alex" b.title= "understand interface and struct type in Go" b.postId= 12345 fmt.Println(*b) // dereference the pointer } //output &{ 0} {Alex understand interface and struct type in Go 12345}
Note that as we can see from the output, by using the new
keyword, we allocate storage for the variable, b
, which then initializes the zero values of our struct fields — in this case (author="", title="", postId=0)
.
This then returns a pointer type, *b
, containing the address of the above variables in memory.
Here is a link to the playground to run the code. More details about the behavior of the new
keyword can be found here.
In our earlier examples, we used Go’s default behavior, wherein everything is passed by value. With pointers, this is not the case. Let’s see with an example:
package main import "fmt" type blogPost struct { author string title string postId int } func main() { b := &blogPost{ author:"Alex", title: "understand structs and interface types", postId: 12345, } fmt.Println(*b) // dereference the pointer value fmt.Println("Author's name", b.author) // in this case Go would handle the dereferencing on our behalf }
Here is a link to the playground to run the code.
We’ll understand the benefits of this approach as we proceed with the section on methods and interfaces.
Earlier we mentioned that struct types are composite types. Therefore, we can also have structs that are nested inside other structs. For example, suppose we have a blogPost
and an Author
struct, defined below:
type blogPost struct { title string postId int published bool } type Author struct { firstName, lastName, Biography string photoId int }
Then, we can nest the Author
struct in the blogPost
struct like this:
package main import "fmt" type Author struct { firstName, lastName, Biography string photoId int } type blogPost struct { author Author // nested struct field title string postId int published bool } func main() { b := new(blogPost) fmt.Println(b) b.author.firstName= "Alex" b.author.lastName= "Nnakwue" b.author.Biography = "I am a lazy engineer" b.author.photoId = 234333 b.published=true b.title= "understand interface and struct type in Go" b.postId= 12345 fmt.Println(*b) } // output &{{ 0} 0 false} // again default values {{Alex Nnakwue I am a lazy engineer 234333} understand interface and struct type in Go 12345 true}
Here is the link to run the code in the playground.
In Go, there is a concept of promoted fields for nested struct types. In this case, we can directly access struct types defined in an embedded struct without going deeper, that is, doing b.author.firstName
. Let’s see how we can achieve this:
package main import "fmt" type Author struct { firstName, lastName, Biography string photoId int } type BlogPost struct { Author // directly passing the Author struct as a field - also called an anonymous field orembedded type title string postId int published bool } func main() { b := BlogPost{ Author: Author{"Alex", "Nnakwue", "I am a lazy engineer", 234333}, title:"understand interface and struct type in Go", published:true, postId: 12345, } fmt.Println(b.firstName) // remember the firstName field is present on the Author struct? fmt.Println(b) } //output Alex {{Alex Nnakwue I am a lazy engineer 234333} understand interface and struct type in Go 12345 true}
Here is a link to the playground to run the code.
Note that Go does not support inheritance, but rather composition. We have seen an example of how we created a new struct in an earlier section with the help of composition.
In the coming sections, we will also learn more about how these concepts can be applied to interface types and how we can add behavior to struct types with methods.
It’s important to note that field names can be specified either implicitly with a variable or as embedded types without field names. In this case, the field must be specified as a type name, T
, or as a pointer to a non-interface type name *T
.
Other considerations include the following:
More details about the struct type can be found here in the language specification.
Methods in Go are special kinds of functions with a receiver.
A method set of a type, T
, that consists of all methods declared with receiver types, T
. Note that the receiver is specified via an extra parameter preceding the method name. More details about receiver types can be found here.
In Go, we can create a type with a behavior by defining a method on that type. In essence, a method set is a list of methods that a type must have to implement an interface. Let’s look at an example:
// BlogPost struct with fields defined type BlogPost struct { author string title string postId int } // Create a BlogPost type called (under) Technology type Technology BlogPost
Note that we are using a struct type here because we are focusing on structs in this article. Methods can also be defined on other named types:
// write a method that publishes a blogPost - accepts the Technology type as a pointer receiver func (t *Technology) Publish() { fmt.Printf("The title on %s has been published by %s, with postId %d\n" , t.title, t.author, t.postId) } // alternatively similar to the above, if we choose not to define a new type func (b *BlogPost) Publish() { fmt.Printf("The title on %s has been published by %s, with postId %d\n" , t.title, b.author, b.postId) } // Create an instance of the type t := Technology{"Alex","understand structs and interface types",12345} // Publish the BlogPost -- This method can only be called on the Technology type t.Publish() // output The title on understand structs and interface types has been published by Alex, with postId 12345
Here is a link to the playground to run the code.
Methods with pointer receivers work on both pointers or values. However, that is not true the other way around.
In Go, interfaces serve a major purpose of encapsulation and allow us to write cleaner and more robust code. By doing this, we only expose methods and behavior in our program.
As we mentioned in the last section, method sets add behavior to one or more types. However, interface types define one or more method sets.
A type, therefore, is said to implement an interface by implementing its methods. In that light, interfaces enable us to compose custom types that have a common behavior.
Method sets are basically method lists that a type must have for that type to implement that interface.
For instance, say we have two or more structs types implement the same method with the same return types, we can go ahead and create an interface type with this method set, since it is common to one or more struct types.
In Go, interfaces are implicit. This means that if every method belonging to the method set of an interface type is implemented by a type, then that type is said to implement the interface. To declare an interface:
type Publisher interface { publish() error }
In the publish()
interface method we set above, if a type (for example, a struct) implements the method, then we can say the type implements the interface. Let’s define a method that accepts a struct type blogpost
below:
func (b blogPost) publish() error { fmt.Println("The title has been published by ", b.author) return nil } <
Now to implement the interface:
package main import "fmt" // interface definition type Publisher interface { Publish() error } type blogPost struct { author string title string postId int } // method with a value receiver func (b blogPost) Publish() error { fmt. Printf("The title on %s has been published by %s, with postId %d\n" , b.title, b.author, b.postId) return nil } func test(){ b := blogPost{"Alex","understanding structs and interface types",12345} fmt.Println(b.Publish()) d := &b // pointer receiver for the struct type b.author = "Chinedu" fmt.Println(d.Publish()) } func main() { var p Publisher fmt.Println(p) p = blogPost{"Alex","understanding structs and interface types",12345} fmt.Println(p.Publish()) test() // call the test function } //output <nil> The title on understanding structs and interface types has been published by Alex, with postId 12345 <nil> The title on understanding structs and interface types has been published by Alex, with postId 12345 <nil> The title on understanding structs and interface types has been published by Chinedu, with postId 12345 <nil>
Here is a link to the playground to run the code.
We can also alias interface types like this:
type publishPost Publisher // alias to the interface defined above - mostly suited for third-party interfaces
However, note that if more than one type implements the same method, the method set can construct an interface type.
This allows us to pass that interface type as an argument to a function that intends to implement that interface’s behavior. This way, we can achieve polymorphism.
Unlike functions, methods can only be called from an instance of the type they were defined on.
The benefit is that instead of specifying a particular data type we want to accept as an argument to functions, it would be nice if we could specify the behavior of the objects that must be passed to that function as arguments.
Let’s look at how we can use interface types as arguments to functions. To begin, let’s add a method to our struct type:
package main import "fmt" type Publisher interface { Publish() error } type blogPost struct { author string title string postId int } func (b blogPost) Publish() error { fmt.Printf("The title on %s has been published by %s\n" , b.title, b.author) return nil } // Receives any type that satisfies the Publisher interface func PublishPost(publish Publisher) error { return publish.Publish() } func main() { var p Publisher fmt.Println(p) b := blogPost{"Alex","understand structs and interface types",12345} fmt.Println(b) PublishPost(b) } //output <nil> {Alex understand structs and interface types 12345} The title on understand structs and interface types has been published by Alex
Here is the link to run the code on the playground.
As we previously mentioned, we can pass a method receiver either by value or by pointer type. When we pass by value, we store a copy of the value we are passing.
This means that when we call the method, we don’t change the underlying value. However, when we pass by pointer semantics, we directly share the underlying memory address, and thus, the location of the variable declared in the underlying type.
As a reminder, though, a type is said to implement an interface when it defines method sets available on the interface type.
Again, types are not required to nominate that they implement an interface; instead, any type implements an interface, provided it has methods whose signature matches the interface declaration.
Finally, we will look at the signature for embedding interface types in Go. Let’s use a dummy example:
//embedding interfaces type interface1 interface { Method1() } type interface2 interface { Method2() } type embeddedinterface interface { interface1 interface2 } func (s structName) method1 (){ } func (s structName) method2 (){ } type structName struct { field1 type1 field2 type2 } // initialize struct type inside main func var e embeddedinterface = structName // struct initialized e.method1() // call method defined on struct type
As a rule of thumb, when we start to have multiple types in our package implemented with the same method signatures, we can then begin to refactor our code and use an interface type. Doing so avoids early abstractions.
An empty interface contains zero methods. Note that all types implement the empty interface.
This means that if you write a function that takes an empty interface{}
value as a parameter, you can supply that function with any value/method.
Interfaces also generally belong in the package that uses values of the interface type and not the package that implements those values.
And finally, the zero value of an interface is nil
. More details about the interface type can be found here in the language specification.
There are cases when we intend to derive a concrete type say a struct from an empty interface or an interface type. In Go, we can check for the equality of types via type assertions.
From Effective Go, to cast an interface to a struct, we can make use of the syntax notation below:
v = x.(T)
Here, x
is the interface type and T
is the actual concrete type. In essence, T
must implement the interface type of x
.
Note that x
is usually a dynamic type, and its value is known at runtime. Therefore, Go panics if the type assertion is invalid.
To check for correctness and avoid a type mismatch, we can go further and make use of the syntax notation below:
v, ok = x.(T)
In this case, the value of ok
is true
if the assertion holds. Let’s see a trivial example of using type assertions to work with both structs and interfaces below:
package main import "fmt" type blogPost struct { Data interface{} postId int } func NewBlogPostStruct() interface{} { return &blogPost{postId: 1234, Data: "Alexander"} } func main() { blogPost := NewBlogPostStruct().(*blogPost) fmt.Println(blogPost.Data) } //returns Alexander
Notice that from the above blogPost
struct, we need to ensure that we set the Data
field to the type we expect; in our case, we use a string.
As we learned, interface types can store the copy of a value or a value can be shared with the interface by storing a pointer to the value’s address.
One important thing to note about interface types is that it is advisable not to focus on optimizing too early, as we do not want to define interfaces before they are used.
The rules for determining interface adherence or usage are based on method receivers and how the interface calls are made. Read more about this in the Go code review and comments section here.
A quite confusing rule about pointers and values for method receivers is that while value methods can be invoked on both pointers and values, pointer methods can only be invoked on pointers.
For receiver types, if a method needs to mutate the receiver, the receiver must be a pointer.
Extra details about interface types can be found ineffective Go. Specifically, you can take a look at interfaces and methods, interface checks, and interface conversions and type assertions.
Type assertions are more like operations applied to an underlying value of an interface type. Essentially, it is a process for extracting the values of an interface type. They are represented as x.(T)
, where the value x
is an interface type.
Again, thanks for reading and please feel free to add questions or comments in the comment section below, or reach out on Twitter. Go forth and keep learning!🙂
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.