Golang datatypes

From wikinotes
Revision as of 21:36, 29 May 2022 by Will (talk | contribs) (→‎Slices)

Literal Types

"foo"       // string
123         // int (cpu-arch dependent bitsize)
3.14        // float64
6.2e25      // float64
true, false // bool

Boolean

var condition bool = true

Text

String

Go's strings are bytestrings by default.
The are treated as arrays of bytes.

  • Strings are immutable.
  • Strings are UTF-8

TODO:

write some best pratices for handling multi-byte characters

var name string = "will"                 // string
name := "will"                           // string

// slices
var ascii_code uint8 = name[1]           // 2nd char from string
var char string = string(ascii_code)     // get string from UTF-8 codepoint number

bytestring := []byte("hi") == [104, 105] // string as bytes (uint8)

// operators
msg := "hello " + "world"                // concatenation

Rune

Runes are UTF-32 strings.
(Each character is a uint32)

Numeric

Integers

Integer sizes are expressed by their bit-size.

signed

int     //  (however many bits your CPU word-size is)
int8    //                       128 - 127
int16   //                    32,768 - 32,767
int32   //             2,147,483,648 - 2,147,483,647
int64   // 9,223,372,036,854,775,808 - 9,223,372,036,854,775,807

unsigned

uint     // (however many bits your CPU word-size is)
uint8    // 0 - 255
uint16   // 0 - 65,535
uint32   // 0 - 4,294,967,295
// uint64 does not exist

Bytes

Same as uint8.

math/big

Slow, but handles numbers of any size.

Complex Numbers

Go lets you represent complex/imaginary numbers.

var num complex64 = 1 + 2i
var num complex128 = 1 + 2i

var num complex64 = 2i            // alternative for 1 + 2i
var num complex64 = complex(1, 2) // alternative for 1 + 2i

You can also extract the complex/imaginary part of the number.

var num complex64 = 1 + 2i
real(num) // the real number component
imag(num) // the imaginary number component

Pointers

uintptr  // a pointer of however many bits your CPU word-size is

Collections

Arrays

Arrays are statically-sized, homogenous collections of items,
stored contiguously in memory.

family := [2]string{"will", "alex"}    // create 2-item array with these elements
family := [...]string{"will", "alex"}  // create array whose size matches provided elements

var family [2]string                   // create 2-item array, then assign elements
family[0] = "will"
family[1] = "alex"

var families string[3][2]              // create an empty nested array

families := [...]string{               // create nested array using literals
  [...]string{"will", "alex"},
  [...]string{"tom", "fiona"},
  [...]string{"alex", "morgan"},
}

len(family) == 2

While arrays are mutable, when assigning an array to a new variable,
you're actually duplicating the array.

numbers := [...]int{1, 2, 3}
copy_of_numbers := numbers
copy_of_numbers[1] = 9        // <-- does not change 'numbers'

fmt.Println(numbers)          // [1 2 3]
fmt.Println(copy_of_numbers)  // [1 9 3]

Rather than passing array-data to a function, in most cases, you probably want to pass a pointer to it.

numbers := [...]int{1, 2, 3}
ref_to_numbers := &numbers   // <-- get pointer to 'numbers'
ref_to_numbers[1] = 9

fmt.Println(numbers)          // [1 9 3]
fmt.Println(ref_to_numbers)   // &[1 9 3]

Slices

The Slice datastructure is a resizable abstraction built overtop of an array.
see https://go.dev/blog/slices-intro

A slice's datastructure is composed of:

  • pointer to array element
  • length of slice
  • capacity (length) of underlying array

Some properties:

  • Assigning a slice to another variable will not duplicate the data
  • Changing a slice will change the underlying array
  • Resizing a slice is fairly cheap. Methods exist to duplicate array into a larger one when required.


building slices

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

// build an empty slice
numbers = make([]int, 3, 3)  // `make(type, length, [capacity])`
numbers = make([]int, 3)     // if not specified, capacity matches lenth

// build slices from arrays, or other slices
numbers := [...]{1, 2, 3, 4, 5}     // array {1, 2, 3, 4, 5}
mySlice := numbers[:2]              // slice {1, 2, 3}

slice functions

len(numbers) == 3     // length
cap(numbers) == 3     // capacity

append(numbers, 4, 5)           // starting at 'length', add items to the array, growing if necessary
append(numbers, ...otherSlice)  // concatenate/extend another slice to an existing one

Capacity, and Growing Slices

One interesting property of append, is that it assumes you want to grow the underlying array.
This makes it convenient to start with length 0, and grow your array as needed.
This can be costly, since each appended item will create a new array, and copy the old one into it.

You can manipulate the slice's capacity to grow pre-allocate arrays, and grow your slice in efficient increments.

// Yuck -- each append creates a new (len+1) array, and copies old array into it
numbers = make(int[], 0)  // length=0, capacity=0
append(numbers, 1)
append(numbers, 2)
// Nice - we won't need to resize array until we add the 6th item
numbers = make(int[], 0, 5)
append(numbers, 1)
append(numbers, 2)
// Nicer - once we hit the 6th item, let's grow in increments of the capacity
numbers := make([]int, 0, 5)
numbers = append(numbers, 1, 2, 3, 4, 5)
buf := make([]int, len(numbers), cap(numbers) * 2)
copy(buf, numbers)
numbers = buf

fmt.Println(numbers)      // [1 2 3 4 5]
fmt.Println(cap(numbers)) // 10

Compound Types