Go Practices 101
Theme: dark/light
Go Optimizations 101,
Go Details & Tips 101
and Go Generics 101
are all updated for Go 1.24 now.
The most cost-effective way to get them is through this book bundle
in the Leanpub book store.
TapirMD - a powerful, next-generation markup language that simplifies content creation (much more powerful than markdown).
You can experience it online
here.
This article will introduce all kinds of types in Go and the concepts regarding Go type system. Without knowing these fundamental concepts, it is hard to have a thorough understanding of Go.
Note, byte
is a built-in alias of uint8
, and rune
is a built-in alias of int32
. We can also declare custom type aliases (see below).
Except
string types, Go 101 article series will not try to explain more on other basic types.
The 17 built-in basic types are predeclared types.
Go supports the following composite types:
-
-
-
-
-
array types - fixed-length container types.
-
slice type - dynamic-length and dynamic-capacity container types.
-
map types - maps are associative arrays (or dictionaries). The standard Go compiler implements maps as hashtables.
-
channel types - channels are used to synchronize data among goroutines (the green threads in Go).
-
interface types - interfaces play a key role in reflection and polymorphism.
Unnamed composite types may be denoted by their respective type literals. Following are some literal representation examples of all kinds of unnamed composite types (name and unnamed types will be explained below).
// Assume T is an arbitrary type and Tkey is
// a type supporting comparison (== and !=).
*T // a pointer type
[5]T // an array type
[]T // a slice type
map[Tkey]T // a map type
// a struct type
struct {
name string
age int
}
// a function type
func(int) (bool, string)
// an interface type
interface {
Method0(string) int
Method1() (int, bool)
}
// some channel types
chan T
chan<- T
<-chan T
Each of the above mentioned basic and composite types corresponds to one kind of types. Besides these kinds, the unsafe pointer types introduced in the
unsafe
standard package also belong to one kind of types in Go. So, up to now (Go 1.24), Go has 26 kinds of types.
(Type definition, or type definition declaration, initially called type declaration, was the only type declaration way before Go 1.9. Since Go 1.9, type definition has become one of two forms of type declarations. The new form is called type alias declaration, which will be introduced in a section below.)
In Go, we can define new types by using the following form. In the syntax, type
is a keyword.
// Define a solo new type.
type NewTypeName SourceType
// Define multiple new types together.
type (
NewTypeName1 SourceType1
NewTypeName2 SourceType2
)
New type names must be identifiers. But please note that, type names declared at package level can't be
init
.
The second type declaration in the above example includes two type specifications. If a type declaration contains more than one type specification, the type specifications must be enclosed within a pair of ()
.
Note,
-
a new defined type and its respective source type in type definitions are two distinct types.
-
two types defined in two type definitions are always two distinct types.
-
the new defined type and the source type will share the same underlying type (see below for the definition of underlying types), and their values can be converted to each other.
-
types can be defined within function bodies.
Some type definition examples:
// The following new defined and source types are all
// basic types. The source types are all predeclared.
type (
MyInt int
Age int
Text string
)
// The following new defined and source types are all
// composite types. The source types are all unnamed.
type IntPtr *int
type Book struct{author, title string; pages int}
type Convert func(in0 int, in1 bool)(out0 int, out1 string)
type StringArray [5]string
type StringSlice []string
func f() {
// The names of the three defined types
// can be only used within the function.
type PersonAge map[string]int
type MessageQueue chan string
type Reader interface{Read([]byte) int}
}
Please note that, from Go 1.9 to Go 1.17, the Go specification ever thought predeclared built-in types are defined types. But since Go 1.18, the Go specification clarifies that predeclared built-in types are not defined types.
Since Go 1.18, Go has supported custom generic types (and functions). A generic type must be instantiated to be used as value types.
A generic type is a defined type and its instantiated types are named types (named types are explained in the next section).
The other two important concepts in custom generics are constraints and type parameters.
This book doesn't talk about custom generics in detail. Please read the
Go Generics 101 book on how to declare and use generic types and functions.
Before Go 1.9, the terminology named type is defined accurately in Go specification. A named type was defined as a type that is represented by an identifier. Along with the custom type alias feature introduced in Go 1.9 (see the next section), the named type terminology was ever removed from Go specification and it was replaced by the defined type terminology. Since Go 1.18, along with the introduction of custom generics, the named type terminology has been added back to Go specification.
A named type may be
-
-
a defined (non-custom-generic) type;
-
an instantiated type (of a generic type);
-
a type parameter type (used in custom generics).
Other value types are called unnamed types. An unnamed type must be a composite type (not vice versa).
Since Go 1.9, we can declare custom type aliases by using the following syntax. The syntax of alias declaration is much like type definition, but please note there is a =
in each type alias specification.
type (
Name = string
Age = int
)
type table = map[string]int
type Table = map[Name]Age
Type alias names must be identifiers. Like type definitions, type aliases can also be declared within function bodies.
A type name (or literal) and its aliases all denote an identical type. By the above declarations, Name
is an alias of string
, so both denote the same type. The same applies to the other three pairs of type notations (either names or literals):
-
Age
and int
-
table
and map[string]int
-
Table
and map[Name]Age
In fact, the literals map[string]int
and map[Name]Age
also both denote the same type. So, similarly, aliases table
and Table
also denote the same type.
Note, although a type alias always has a name, it might denote an unnamed type. For example, the table
and Table
aliases both denote the same unnamed type map[string]int
.
In Go, each type has an underlying type. Rules:
-
for built-in types, the respective underlying types are themselves.
-
for the Pointer
type defined in the unsafe
standard code package, its underlying type is itself (at least we can think so. In fact, the underlying type of the unsafe.Pointer
type is not well documented. We can also think the underlying type is *T
, where T
represents an arbitrary type). unsafe.Pointer
is also viewed as a built-in type.
-
the underlying type of an unnamed type, which must be a composite type, is itself.
-
in a type declaration, the newly declared type and the source type have the same underlying type.
Examples:
// The underlying types of the following ones are both int.
type (
MyInt int
Age MyInt
)
// The following new types have different underlying types.
type (
IntSlice []int // underlying type is []int
MyIntSlice []MyInt // underlying type is []MyInt
AgeSlice []Age // underlying type is []Age
)
// The underlying types of []Age, Ages, and AgeSlice
// are all the unnamed type []Age.
type Ages AgeSlice
How can an underlying type be traced given a user declared type? The rule is, when a built-in basic type or an unnamed type is met, the tracing should be stopped. Take the type declarations shown above as examples, let's trace their underlying types.
MyInt → int
Age → MyInt → int
IntSlice → []int
MyIntSlice → []MyInt → []int
AgeSlice → []Age → []MyInt → []int
Ages → AgeSlice → []Age → []MyInt → []int
In Go,
-
types whose underlying types are bool
are called boolean types;
-
types whose underlying types are any of the built-in integer types are called integer types;
-
types whose underlying types are either float32
or float64
are called floating-point types;
-
types whose underlying types are either complex64
or complex128
are called complex types;
-
integer, floating-point and complex types are also called numeric types;
-
types whose underlying types are string
are called string types.
An instance of a type is called a 'value' of the type. Values of the same type share some common properties. A type may have many values. One of them is the zero value of the type.
Each type has a zero value, which can be viewed as the default value of the type. The predeclared
nil
identifier can used to represent zero values of slices, maps, functions, channels, pointers (including type-unsafe pointers) and interfaces. For more information on
nil
, please read
nil in Go.
Function literals, as the name implies, are used to represent function values. A
function declaration is composed of a function literal and an identifier (the function name).
Composite literals are used to represent values of struct types and container types (arrays, slices and maps), Please read
structs in Go and
containers in Go for more details.
There are no literals to represent values of pointers, channels and interfaces.
At run time, many values are stored somewhere in memory. In Go, each of such values has a direct part. However, some of them have one or more indirect parts. Each value part occupies a continuous memory segment. The indirect underlying parts of a value are referenced by its direct part through (
safe or
unsafe) pointers.
The terminology
value part is not defined in Go specification. It is just used in Go 101 to make some explanations simpler and help Go programmers understand Go types and values better.
When a value is stored in memory, the number of bytes occupied by the direct part of the value is called the size of the value. As all values of the same type have the same value size, we often simply call this the size of the type.
We can use the Sizeof
function in the unsafe
standard package to get the size of any value.
Go specification doesn't specify value size requirements for non-numeric types. The requirements for value sizes of all kinds of basic numeric types are listed in the article
basic types and basic value literals.
For a pointer type, assume its underlying type can be denoted as *T
in literal, then T
is called the base type of the pointer type.
More information on pointer types and values can be found in the article
pointers in Go.
A struct type consists a collection of member variable declarations. Each of the member variable declarations is called a "field" of the struct type. For example, the following struct type Book
has three fields, author
, title
and pages
.
type Book struct {
author string
title string
pages int
}
More information on struct types and values can be found in the article
structs in Go.
The signature of a function type is composed of the input parameter definition list and the output result definition list of the function.
The function name and body are not parts of a function signature. Parameter and result types are important for a function signature, but parameter and result names are not important.
Please read
functions in Go for more details on function types and function values.
In Go, some types can have
methods. Methods can also be called member functions. The method set of a type is composed of all the methods of the type.
Interface values are values whose types are interface types.
Each interface value can box a non-interface value in it. The value boxed in an interface value is called the dynamic value of the interface value. The type of the dynamic value is called the dynamic type of the interface value. An interface value boxing nothing is a zero interface value. A zero interface value has neither a dynamic value nor a dynamic type.
An interface type can specify zero or several methods, which form the method set of the interface type.
If the method set of a type, which is either an interface type or a non-interface type, is the super set of the method set of an interface type, we say the type
implements the interface type.
For a (typed) non-interface value, its concrete value is itself and its concrete type is the type of the value.
A zero interface value has neither concrete type nor concrete value. For a non-zero interface value, its concrete value is its dynamic value and its concrete type is its dynamic type.
Arrays, slices and maps can be viewed as formal container types.
Sometimes, string and channel types can also be viewed as container types informally.
Each value of a formal or informal container type has a length.
More information on formal container types and values can be found in the article
containers in Go.
If the underlying type of a map type can be denoted as map[Tkey]T
, then Tkey
is called the key type of the map type. Tkey
must be a comparable type (see below).
The types of the elements stored in values of a container type must be identical. The identical type of the elements is called the element type of the container type.
-
For an array type, if its underlying type is [N]T
, then its element type is T
.
-
For a slice type, if its underlying type is []T
, then its element type is T
.
-
For a map type, if its underlying type is map[Tkey]T
, then its element type is T
.
-
For a channel type, if its underlying type is chan T
, chan<- T
or <-chan T
, then its element type is T
.
-
The element type of any string type is always byte
(a.k.a. uint8
).
Channel values can be viewed as synchronized first-in-first-out (FIFO) queues. Channel types and values have directions.
-
A channel value which is both sendable and receivable is called a bidirectional channel. Its type is called a bidirectional channel type. The underlying types of bidirectional channel types are denoted by the chan T
literal.
-
A channel value which is only sendable is called a send-only channel. Its type is called a send-only channel type. Send-only channel types are denoted by the chan<- T
literal.
-
A channel value which is only receivable is called a receive-only channel. Its type is called a receive-only channel type. Receive-only channel types are denoted by the <-chan T
literal.
More information on channel types and values can be found in the article
channels in Go.
Currently (Go 1.24), Go doesn't support comparisons (with the ==
and !=
operators) for values of the following types:
-
slice types
-
map types
-
function types*. any struct type with a field whose type is incomparable and any array type which element type is incomparable.
Above listed types are called incomparable types. All other types are called comparable types. Compilers forbid comparing two values of incomparable types.
Note, incomparable types are also called uncomparable types in many articles.
The key type of any map type must be a comparable type.
Go is not a full-featured object-oriented programming language, but Go really supports some object-oriented programming elements. Please read the following listed articles for details:
Before Go 1.18, the generic functionalities in Go were limited to built-in types and functions. Since Go 1.18, custom generics has already been supported. Please read the
generics in Go article for built-in generics and the
Go Generics 101 book for custom generics.
The digital versions of this book are available at the following places:
Tapir, the author of Go 101, has been on writing the Go 101 series books
and maintaining the go101.org website since 2016 July.
New contents will be continually added to the book and the website from time to time.
Tapir is also an indie game developer.
You can also support Go 101 by playing
Tapir's games
(made for both Android and iPhone/iPad):
Individual donations via PayPal are also welcome.
Articles in this book:
-
Become Familiar With Go Code