Golang datatypes

From wikinotes

Documentation

types https://pkg.go.dev/go/types@go1.18.3
primitive types https://pkg.go.dev/builtin@go1.18.3#pkg-types

Literal Types

// multiline string
`string     
literal
wow`        

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

Local Primitives

Go lets you create and modify your own local varitions of it's types.
You can then add methods within your local scope.

type Int int

func (i Int) Double() Int {
    return i * 2
}

func main() {
    five := Int(5)
    fmt.Println(five.Double() == 10)
}

Generic Types

// a re-usable type-alias
type Number interface {
	int | int8 | int16 | int32 | float32 | float64
}

// use of a type-alias
func Divide[N Number](a, b N) N {
	return a / b
}

If you need to do a type-conversion to satisfy the interface, call the generic type.
(example is contrived)

func ReturnParam[N int](a N) N {
    return N(a)  // casts 'a' to 'N'
}

interface{} will accept any type of parameter (since anything matches it).

// single param
func Print(val interface{}) {
    fmt.Printf("%v", val)
}

// you may also encounter untyped slices
// -- you may need to convert your type to []interface{} for it to work
func Print(val ...[]interface{}) {
    fmt.Print(val...)
}
names := []interface{}{"alex", "will"}
Print(names...)

// or similarly, prepending to variadic type
names := []interface{}{"will", "maize"}
allNames := append([]interface{}{"alex"}, names...) // prepend alex in new slice
fmt.Print(allNames...)

Boolean

var condition bool = true

Text

String (UTF-8)

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

  • Strings are immutable.
  • Strings are UTF-8
  • Strings are pointers to arrays of bytes (lightweight as params)
  • String utils available in the strings library

Core Usage

var name string = "will"                           // string
name := "will"                                     // string
fmt.Sprintf("[%s] %s", "error", "thing happened")  // no string interpolation, use Sprintf

len("ʞ")          // 2 (number of bytes)
len([]rune("ʞ"))  // 1 (number of characters)  (NOTE: compiler optimizes with 'range s')
for i := range {  // iterates over position of first byte in each character
    // ...
}

Reader/Writer Interfaces

// io.Reader
strings.NewReader("abc")

// io.Writer
strings.Builder{}
b.Write([]byte("abc")
b.String()

// implements io.Reader/io.Writer
buf := bytes.NewBuffer(nil)
buf.WriteString("abc")
out := make([]byte, 5)
bytes, err := buf.Read(out)

As Byte Slices

// 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)
stringfrombytes := string([]byte{"hi"})  // bytes as string

Operators

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

golang doesn't have string interpolation, but os.Expand() implements simple templating

os.Expand() example

func expander(s string) string {
    switch s {
        case "user":
            return "will"
        case "msg":
            return "how are you?"
        default:
            return "unknown"
    }
}
os.Expand("Hello ${user}. ${msg}", expander)  // "Hello will. how are you?"

Rune (UTF-32)

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

Native format of data streams, binary data.
Frequently used to store characters.

  • Interchangeable with uint8
  • Utils for manipulating bytes are in the bytes library

Float

float32
float64

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

Dates, Time, Timezones

See a list of ready-made time.parse format constants here.

  • Time stores a timeozone localized date/time
  • Duration stores a time delta
  • Location stores a timezone

Notable Dates

time.Now()       // now
time.Unix(0, 0)  // unix epoch

Manipulating Dates

import "time"

dur, _ := time.ParseDuration("60m") // delta of 1hr
now := time.Now()                   // timezone aware
inAnHour := now.Add(dur)            // add an hour
lessAnHour := now.Sub(dur)          // subtract an hour
t := now.Truncate(time.Hour)        // round down to nearest hour

Creating Dates

zone, _ := time.LoadLocation("UTC")
dt := time.Date(1970, 1, 1, 12, 0, 0, 0, zone)

Parsing/Formatting Dates

dt, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z07:00")
datestr := dt.Format(time.RFC3339)

Format strings are written literally rather than printf style signage.
Here are the available glyphs.

Year:                      "2006" "06"
Month:                     "Jan" "January"
Textual day of the week:   "Mon" "Monday"
Numeric day of the month:  "2" "_2" "02"
Numeric day of the year:   "__2" "002"
Hour:                      "15" "3" "03" (PM or AM)
Minute:                    "4" "04"
Second:                    "5" "05"
AM/PM mark:                "PM"

Pointers

uintptr          // pointer type (any word-size)

var a int = 123
var b *int = &a  // declare a pointer for an 'int' type
a == *b          // de-reference 'b' to get value
&a == b          // reference 'a' to get memory address

pointer aritmetric is not supported by go, if required you can workaround this in the unsafe package.

You can initialize a type using a pointer to it

type Foo struct {
    a int
}

func main() {
    var foo = *Foo
    foo = new(Foo)   // create instance of 'foo'

    foo.a = 1        // assign pointer foo.a
    (*foo).a = 1     // assign pointer foo.a
}

Enums (Iota)

iota will auto-increment the value of the assignment,
allowing you to use constants similar to an enum

type Colour int8
const (
    _ Colour = iota   // 'Colour' defines the type, 'iota' increments the value
    red
    green
    blue
)

func setColour(c Colour) {
    // ...
}

The type this constant takes is inferred based on the types it is used with.

const (
    red = iota  // 0 (because var usage below)
    green       // 1
    blue        // 2
)

var color int = red  // types within the group will be int now


Since the type here is inferred, comparing a variable with no value to the first entry will return true. for this reason, the first const in a iota group is generally discarded or used as an error value.

// ex.
//   const ( red = iota; green; blue )
//   var i int
//   i == red // returns true   <--- Not what we want!!
const (
    _ = iota  // this is our trash value -- it's inaccessible
    red   // 1 (if with assignment as int)
    green // 2
    blue  // 3
)

Set iota initial-value

const (
    _ = iota + 10
    red   // 11
    green // 12
    blue  // 13
)

Assignment Patterns (ex. unit increases)

const (
    _ = iota
    KB = 1 << (10 * iota)  // 1024
    MB                     // 1024^2
    GB                     // 1024^3
)

Permission Bitmasks

const (
  r = 1 << iota  // 0b001
  w              // 0b010
  x              // 0b100
)

userPermissions := read | write
if (userPermissions & read) {
    fmt.Println("user can read file!")
}

Collections

Arrays

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

building arrays

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"
family[len(family) - 1]                // last item in array

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"},
}

array functions

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

// starting at 'length', add items to the array, growing if necessary
// (roughly doubles ex. 2, 4, 8, 16, ...)
// (returns new slice, does NOT mutate)
newNums = append(numbers, 4, 5)
newNums = 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 increments that make sense to your program.

// 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

Maps

Maps are unordered, and must be homogenous.
They can be nested.

building maps

var user_ids = map[string]int{
    "Alex": 1,
    "Will": 2,
    // ...
}

user_ids["Alex"] == 2

Go only supports comparable datatypes as keys.
There is no magic method you can use to make a type comparable, the comparable types are strictly:

valid key types

// primitives
booleans
integers
floats
strings
pointers
arrays
channels

// compound
interface values # if all fields are same type, and values are identical

map methos

delete(myMap, "myKey")     // remove item from map

len(myMap)                 // number of items

val = myMap["myKey"]       // retrieve value from map (zero type if not set)
val, ok := myMap["myKey"]  // retrieve value from map, with boolean indicating if map contained key

Structs

structs are mutable compound objects with fixed fields.
Field names follow the same casing rules for exported/private fields.
struct values are stored directly (not by reference) - copied structs will have separate values.

Basics

buiding structs

// declared struct
type Animal struct {
    age int       // lowercase, so private outside of package
    Name string   // uppercase, so exported outside of package
}

// anonymous struct
pet := struct(age int, name string){
  age: 2,
  name: "maize"
}

initializing structs

pet := Animal{age: 2, name: "maize"}  // keyword init
pet := Animal{2, "maize"}             // positional init

bind methods to struct

type User struct {
    id int
    name string
}

func (u *User) SetName(name string) {
    u.name = name
}


struct tags

// struct tags are often used to provide extra info to serializers
type Roommate struct {
    FirstName string `Roommmate's firstname`   // text between `` is a tag
}

// tags can be accessed programmatically
type := reflect.TypeOf(Roommate{})
field, _ := type.FieldByName("Name")
fmt.Println(field.Tag)

Embedding Structs

embedding structs allows you to inherit their field assignments.

type Animal struct {
    age int
    Name string
}

type Cat struct {
    Animal         // <-- include all fields from Animal
    whiskers int
}

// initializing in constructor
// requires you to assign values on the embedded struct
Cat{
  Animal: Animal{age: 2, Name: "maize"}
  whiskers: 4,
}

// outside of constructor,
// changing values can be done without knowledge of composition
cat := Cat{}
cat.age = 2
cat.Name = "maize"
cat.whiskers = 4

Embedding Interfaces

you can embed an interface to inherit methods from that object
See golang interfaces

Embedding object as Interface and Struct

You can technically wrap an entire object by embedding both it's interface, and it's struct.
However, the object will need to be embedded twice, and access will be provided through the embedded object.
Instead of doing this, you probably want to expose the inner-object as a field on the object

package main

import (
	"fmt"
	"os/exec"
)

// part of exec.Cmd's interface
type Cmd interface {
	String() string
}

// type alias for exec.Cmd (we cannot embed two of same name)
type cmdStruct = exec.Cmd

// embed exec.Cmd twice, once for the interface, once for the struct
type MyCmd struct {
	Cmd
	cmdStruct
}

func main() {
	cmd := exec.Command("netstat", "-an")
	myCmd := MyCmd{Cmd: cmd, cmdStruct: *cmd}
	fmt.Println(cmd.String()) // 'netstat -an'
	fmt.Println(cmd.Args)     // [netstat -an]

	fmt.Println(myCmd.Cmd.String()) // 'netstat -an'
	fmt.Println(myCmd.Args)         // [netstat -an]
}