This is the third entry of my weekly series Learning Go. Last week I covered control flow and a few of the primary control structures. I also touched on familiar territory; loops, the for statement, and conditional statements. This week I am diving head-first into types in Go, and what they are most commonly used with.
So, what is a “Type”?
determines the set of values together with operations and methods that are specific to those values
Think of a type like a family. They are a collection of like values, much like a family is a collection of folks who share genetics, traits, etc. In the same way, types can share operations and methods that only that type has access to.
Most commonly used types in Go are as follows:
- Boolean
- Numeric
- String
- Array
- Slice
- Map
- Struct
These are some more commonly used types, we will cover these in a later post:
- Pointer
- Function
- Interface
- Channel
Type declaration
To create a new type, you simply use the type keyword:
type (
LightsaberColor string
)
A new type, LightsaberColor
has now been created in your program. A few things to note that is happening in this declaration:
- this declaration binds an identifier (
LightsaberColor
) to a type name,string
. - there are two forms of type declarations, alias declarations and type definitions (the example above is a type definition)
An alias declaration also binds an identifier to the given type, and are not meant for everyday use. Their purpose was to support large refactors involving moving a type between multiple and/or large packages.
You can create an alias declaration like this:
type (
LightsaberColor = JediLightsaberColor
)
Notice that these types now denote the same type. JediLightsaberColor
can now be used as a replacement for LightsaberColor
.
Underlying type
Every type has what is called an underlying type. This is essentially the underbelly or the source of types that is bound to the declared type. Let me give you an example:
type (
BountyHunter string
)
This underlying type of BountyHunter
is string
. Common types such as: bool
, string
, int
are called predeclared identifiers. This just means that they are implicitly declared in the universe block (encompasses all Go source text).
Type identity
When discussing types, two can only be identical or different. Let me give you a few examples that will help differentiate the two.
Identical
package main
import (
"fmt"
)
type (
Jedi = bool
Yoda = bool
)
var j Jedi
var y Yoda
func main() {
fmt.Printf("%T\n", j)
// bool
fmt.Printf("%T", y)
// bool
}
Jedi
is of the typebool
,Yoda
is also of typebool
. This is also an example of an alias declaration.
Different
package main
import (
"fmt"
)
type (
Sith = string
)
type (
Emperor = Sith
Yoda string
)
var s Sith
var e Emperor
var y Yoda
func main() {
fmt.Printf("%T\n", s)
// string
fmt.Printf("%T\n", e)
// string
fmt.Printf("%T", y)
// main.Yoda
}
- although
Yoda
andEmperor
have the same underlying types, they were created in separate type definitions
Type definitions
creates a new, distinct type with the same underlying type and operations as the given type and binds an identifier to it
Let me break down this definition by showing you how to create a type definition
type (
HasTheForce bool
)
Here we have done a few things:
- created a distinct type
HasTheForce
- bound the identifier
HasTheForce
to it
this distinct type has the same type and its underlying type: bool
Boolean
represents a set of truth values denoted by predeclared constants
true
andfalse
Note: the predeclared boolean
type is bool
Numeric
represents a set of integers or floating-point values
These types get pretty extensive, for now, I will show the differences between integers and floating-point.
Integers
- whole numbers, no decimals
package main
import (
"fmt"
)
func main() {
x := 42
fmt.Printf("%T", x)
// int
}
Floating Point
- “real” numbers, contains decimals
package main
import (
"fmt"
)
func main() {
x := 42.12345
fmt.Printf("%T", x)
// float64
}
String
represents a set of string values, a string value is a (potentially empty) sequence of bytes
Things to note:
- the number of bytes is the “length” of the string
- the “length” can never be negative
- they are immutable - once created you can not change the contents
Note: the predeclared string
type is string
func main() {
x := "do or do not, there is no try"
fmt.Printf("%T", x)
// string
}
You can get the length of a string by using the built-in function len
:
func main() {
x := "do or do not, there is no try"
fmt.Println(len(x))
// 29
}
Array
numbered sequence of a single type (element type)
Things to note:
- the number of elements in an array is the length
- the length of an array can never be negative
- the length is a part of an array’s type - it’s value is of type
int
Here is an example of an array:
func main() {
var a [10]int
fmt.Printf("%T", a)
// [10]int
}
Note: we use the var
keyword here because we are declaring an array of type int
, we can not use the short variable declaration (:=
) here because we are binding a type to a
.
You can get the length of an array by using the built-in function len
(just like with strings):
func main() {
var a [10]int
fmt.Println(len(a))
// 10
}
Note: I am also passing the length
of the array by placing the value 10
in between the brackets.
I have found that arrays are not commonly used in Go, slice
is far more common.
Slice
continuous segments with an underlying type of array and gives access to a numbered sequence
Essentially, a slice allows you to gather values of the same type.
A common practice in declaring a slice is by using a composite literal
.
A composite literal is composed of a few things, the type and the value.
package main
import (
"fmt"
)
func main() {
// composite literal
cl := []int{4, 5, 6, 7, 8}
fmt.Println(cl)
// [1 2 3 4]
}
Let me explain what is happening:
- we declare the variable
cl
using a short variable declaration - we bind it’s value to the type
slice
that will contain the typeint
([]int
) - we then assign the values
1 2 3 4
tocl
by placing them between brackets{}
Note: when creating a composite literal all values have to be of the same type
Because slice
is built on top of an array, when the length of that slice changes a few things happen:
- a new array is created underneath
- old values are copied over
- the old array is thrown away
As you can assume, this can potentially take a lot of processing power. To avoid this, we can use the make
function.
package main
func main() {
a := make([]int, 10 , 10)
fmt.Println(a)
// [0 0 0 0 0 0 0 0 0 0]
}
A few things to note:
make
’s first parameter has to be the slice typemake
’s second parameter is the lengthmake
’s third and optional parameter is the capacity of theslice
Let’s see it in action.
package main
import (
"fmt"
)
func main() {
x := make([]int, 5, 5)
fmt.Println(x)
// [0 0 0 0 0]
}
As you can see the slice length and capacity is five and will print five 0
values. What happens if we try to go over the slice capacity?
package main
import (
"fmt"
)
func main() {
x := make([]int, 5, 5)
x[5] = 6
fmt.Println(x)
// [0 0 0 0 0]
}
Here, I am attempting to manually assign a value to the 5th indexed position of this slice. When I do I get this compile error:
panic: runtime error: index out of range [5] with length 5
Go not only prevents us from going off the rails with our code, but gives us precise control over our data.
In Summary
There are so many more features of slice
I want to discuss, and what’s funny is, I have not even touched on map
or struct
yet. I like to keep these posts at a 5-10 minute read; therefore, my next entry will be more on slice
, map
, and struct
. I am excited to share what I have learned with you. Until next time!