Go Basics; Data Types, Variables, and Collections

Go Basics; Data Types, Variables, and Collections

Β·

8 min read

Featured on Hashnode

Today we're going to take a look at the basic building block of Go; Data Types, Variables, and Collections.

As we go through this information please use the Go Playground to try out what I'm showing you on your own, this is a great way to learn.

Let's dive right in...

Primitives

The first things I look at when picking up a new language are the basic building block of that language. These are called primitives. Primitives are the core types of data that the language knows how to work with.

In Go there are four types of primitives;

  1. Boolean
  2. Numeric
  3. String
  4. Error

Most of these primitive groups contain multiple ways to represent their data.

Boolean Primitives

By definition, boolean types only have a singular way to represent their data. It is either true or false.

var t bool = true
var f bool = false

Numeric Primitives

These primitives can represent many different types and sizes of numeric values.

  • Integers (int) - Represents negative and positive values
  • Unsigned integers (uint) - Represents only positive values
  • Floating-point numbers (float) - Represents numbers with decimal points
  • Complex numbers (complex64) - Represents real and imaginary values

You might have noticed how the complex type has a 64 at the end. This signifies the number of bits used to store those values. If you do not specify the bit size, the compiler will do a pretty good job of filling that part in for you. However, you may specify the bit size on any of the numeric primitives if you need to ensure that they are of a specific size.

  • Integers: int, int8, int16, int32, or int64
  • Unsigned Integers: uint, uint8, uint16, uint32, or uint64
  • Floating-Points: float, float32, or float64
  • Complex: complex64, or complex128
var i int = -15
var u uint = 12
var f float = 3.14
var c complex64 = complex(1, 2)

String Primitives

These primitives can represent letters, words, sentences, and other text-based values.

  • Strings (string)
  • ASCII Characters (byte) -> Alias for uint8
  • Unicode Characters (rune) -> Alias for int32
var s string = "Hello, World!"

var b byte = 97
fmt.Printf("%c\n", b) //-> a

var r rune = '\U0001F44B'
fmt.Printf("%c\n", r) //-> πŸ‘‹πŸ»

Bytes and Runes are commonly used in arrays to store specifically encoded versions of strings.

var str string = "Hello, World!"

ascii := []byte(str)
fmt.Println(ascii) //ASCII encoded version of str

unicode := []rune(str)
fmt.Println(unicode) //Unicode encoded version of str

Error Primitives

Errors handling is a core part of developing in Go, thus error is a primitive type in the language.

Even though error is technically a primitive type in Go, it is actually just a wrapper around string with some useful helpers.

var myError error = errors.New("There was an error")
fmt.Println(myError.Error())

Error() automatically get's called if we just use fmt.Println(myError). This is useful for creating our own custom error types that we can check against after calling functions where an error could occur. This way we can check for types of errors, rather than having to do string comparisons in code to see what the error was.

type customError struct {
  err string
  code uint
}

func (e *customError) Error() string {
  return fmt.Sprintf("%s with code: %d", e. err, e.code)
}

Declaring Variables

Now that we've seen the different primitives we can learn about how to use them to store values of those types. We've already seen that above, but let's talk more specifically about what we were doing in those examples.

In Go there are 2 ways to declare variables;

  1. Standard Declaration
  2. Implicit Initialization

Standard Declaration

This is the most verbose method of declaring and defining a variable.

var life int 
life = -42

With this method, we use the keyword var to specify we are going to be declaring a variable. Then we specify the name of the variable followed by the type. We can then use that variable and assign it a value later. This can all be done in a single line if you're going to define the variable at the same time you declare it.

var life int = -42

This is a bit better, but we can get it shorter.

Implicit Initialization

The Go compiler can already tell that the value -42 should be stored in a variable of type int. This means we can use the implicit initialization format and let the compile figure it out for us.

life := -42

This is how you will most likely declare your variables unless you want to be very specific about the type, or if the type could be ambiguous to the compiler (e.g. if you wanted life := 42 to be an unsigned integer you would have to use var life uint = 42 since the compiler will try to use integers first).

Pointer Variables

Up until now, we have only been using variables to store values of our primitive types. These values are store safely in the memory of the computer and our variables are used to access them.

Go also has a way of specifically access the address of those values in memory, these are called pointers. Pointers hold the address of a value, while variables hold the values themselves.

To get the address of an existing variable we use the special "addressof" operator &. This allows us to create a pointer to that variable. In order to get or set the value at that address, we use another special pointer "dereferencing" operator *.

a := 97
p := &a
fmt.Println(p) //Prints the address in memory where 97 is stored
fmt.Println(*p) //Prints the value 97 from the address

Unlike some other languages with pointers, in Go there is no pointer arithmetic.

Constant Variables

In Go, you can create constant variables with the const keyword. These variables cannot be changed after they are declared. Constant variables are declared using "Constant Expressions", which must be able to be determined at compile time.

You can create "implicitly typed constants" that will have their types cast automatically in each expression where they are used through the rest of your code. Or you can give your constants a static type so they would have to manually type cast if you wanted to use them as a different type.

const (
  life = 42 //implicit const
  pi float32 = 3.14159 //static const
)

So far we've seen how to define single variables, declare their types, create pointers to them, and even prevent them from being changed by using const. But not all of our data will live as single variables in our code. We need a way to group these variables and give them more context. Next, let's look at adding our variables to collections.

Collections

Collections in Go are simple a way for us to group data and give them more context in our applications.

There are 4 types of collections in Go;

  1. Structs
  2. Arrays
  3. Slices 4 Maps

Structs

Structs are how we combine data into a single package. If you're familiar with other languages, these are similar to objects. Structs are made of typed fields that can hold data that is shipped around our application together. For example here is what a simple User struct might look like;

type User struct {
  FirstName string
  LastName string
  Age uint
  Email string
}

Now that we have defined our User struct, here is how you can use it.

me := User {
  FirstName = "Josh",
  LastName = "Maxwell",
  Age = 30,
  Email = "jmaxwell@cctechwiz.com",
}

Ok now that we have a User declared and defined, here is how it would look to use that use somewhere;

func greetUser(u User) {
  fmt.Printf("Hello %s, how is it being %d years old?", u.FirstName, u.Age)
}

In the next post, we'll dive more into making structs more useful by adding methods to them. But for now, structs are how we package different data up together in a single context.

Arrays

Arrays are a fixed-size, index-based, collection of data of the same type. For example, to store 3 integers into an array we can do the following.

arr := [3]int{1, 2, 3}

Arrays being index-based means that we can access individual members of the array using their index (Note: indexes start at 0).

first := arr[0] //This will set first = 1

We can also modify the data in an array using the same method.

Arrays can be updated, but they cannot change sizes. In order to accomplish that we need to use a slice.

Slices

Slices are a dynamically sized, index-based, collection of data of the same type. Slices are backed by arrays, but Go automatically takes care of creating new backing arrays to accommodate the growing size of the struct.

slice := []int{1, 2, 3}
slice = append(slice, 4, 5, 6)

Slices can be accessed or modified based on their indices just like arrays.

If we want to use something other than an index to access a collection of values, that's where we need to use a map.

Maps

Maps are a dynamically sized, key-based, collection of data of the same type. This means that we can decide what the key should be for a map, then "map" those keys to their values. For example, say we want to find a family member's age by their name.

familyMap := map[string]int {
  "Josh": 30,
  "Jess": 29,
  "Penny": 6,
  "Mae": 2,
}

fmt.Println(familyMap["Josh"])

There are other things we can do with arrays, slices, and maps, but we'll leave those for another day.

Conclusion

We've covered quite a bit so far about the basic building block of Go. Take some time to play with these on your own. Next time we'll dive deeper into making our applications actually do interesting things with; Functions, Methods, and Interfaces.

In the meantime, use the Go Playground to try out some of what we learned today.

Until next time, be kind to yourself and others, and keep learning.

Β