Golang datatypes
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 stringOperators
msg := "hello " + "world" // concatenationgolang doesn't have string interpolation, but
os.Expand()
implements simple templatingos.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 auint32
)
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,807unsigned
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 existBytes
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 float64math/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 + 2iYou 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/timeDuration
stores a time deltaLocation
stores a timezoneNotable Dates
time.Now() // now time.Unix(0, 0) // unix epochManipulating 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 hourCreating 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 addresspointer 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 enumtype 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) == 2While 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-introA 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 slicesnumbers := []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 oneCapacity, 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"] == 2Go 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 identicalmap 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 keyStructs
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 initbind 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 = 4Embedding Interfaces
you can embed an interface to inherit methods from that object
See golang interfacesEmbedding object as Interface and Struct
you can wrap an entire object by embedding both it's interface, and it's struct.
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] }