- Much of this overview is taken from A Tour of Go
- Go Time Podcast
- Originally developed by Google to address criticisms of other languages. The designers were primarily motivated by their shared dislike of C++.
- Statically typed
- Similar to C (but memory safe, with garbage collection and structural typing)
- Great for concurrency
- v1.0 was publicly released in March 2012
- Readability is high (i.e. it's easy to reason what a program is meant to do)
- Cross platform - Write code once, compile to your target and run.
- It's easy to learn and use, but is still powerful
- If you have some programming experience you could learn the syntax in about a day and start being productive in it right after.
- Like any language, you'll become more productive as you learn the standard library and other popular packages
- The community is awesome and is growing every day
- Makes concurrency easy (easier) - but be careful not to go crazy with this at first
- Concurrency is also easier to reason about (IMO) than that of dotnet's async-await
- Write your test files along side you package files (no testing framework needed). Test package is included in the standard library.
Go is often called the "Devops" language, but that's a myth (though there is a ton of devops tooling written in Go). It's good for just about anything you want to do.
- Web Backends
- Cli Tools
- Docker, K8s, and many more are written in Go
- Helpful Packages
- MicroControllers
- tiny go - Go for small spaces
- Web Assembly
- "Serverless"
- AWS Lamda, GCP Functions, others
- Some Links
- Anywhere that you need a scripting language
- Go can be compiled to target nearly any operating system, so it's perfect for scripting.
- Just about anything you can think of.
- Go programs are made up of packages
- Programs start running in main()
- Builds produce a single binary
package main
import "fmt"
func main() {
fmt.Println("Hello, SDG!")
}-
Packages named after folders (typically)
- Files in the same folder are all a part of the same package (unless it is named [package name]_test)
-
Downloading packages
go get <package name>
-
Running a go program
go run <package name>.go- executable programs start in main, so the file could be server.go, main.go, foobar.go, etc. so long as it has the main function
- typically main.go
-
Building binary
go build .
-
Running a binary
.\<binary name>
-
Use modules for version control
go mod init <name of your package>- Using Go Modules
-
Errors, not exceptions
-
Go doesn't have exceptions, it has errors, which are explicitly returned.
-
IMO, this is better than try-catch-finally, because it forces you to acknowledge and handle or explicitly ignore the error where as languages that allow for exceptions can obfuscate the exceptions that are thrown if they are not documented.
package data import ( "fmt" "github.com/pkg/errors" ) // For custom errors, can use errors.New() // Good idea to export these so that they can be checked for // ErrSomeCustom is a custom error var ErrSomeCustom = errors.New("Some Custom Error") func getPersonById(id string) *person, error { person, err := db.GetPerson(id) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("Could not get person with id: %s from database", id)) } return person, nil }
-
-
Go doesn't have generics yet. Because of this an empty interface is often used in a function signature so that it can take in any type (every type meets the requirements of any empty interface)
func Print(a interface{}) (n int, err error){} func Println(a ...interface{}) (n int, err error){}
- Hopefully we'll have generics soon.
- Database
- Migrate
- Useful for handling database migrations
- Migrate
- Web
- gqlgen
- Generate boilerplate code for GraphQL based on your schema
- gqlgen
- Environment configuration
- Testing
- dockertest
- Useful for running docker containers during test
- I use this to spin up a postgres container, then run my migrations on it when testing my database access. Testing against the real thing is alway preferable to testing against some in memory version of it (to me anyways).
- require
- Useful for assertions. There's also a package called assert that's exactly the same, except that require will stop your tests as soon as you have a failure.
- mock
- Useful for mocking.
- dockertest
Note, this section has a lot of sudo code for brevity
- Go only has 25 keywords
- C# has 110
- JavaScript has 64
- Go has 45 operators and punctuation
// define package that file belongs to
package main
// import packages that source file depends on
import (
"fmt"
"math/rand"
)
// function declaration
func main() {
fmt.Println("The random number is", rand.Intn(10))
}// variable declaration
var y = 1
// or
x := 2
// constant declaration
const z = 3package main
import "fmt"
func main() {
// The type *T is a pointer to a T value. Its zero value is nil.
// The & operator generates a pointer to its operand.
// The * operator denotes the pointer's underlying value.
// Go has no pointer arithmetic.
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer - prints 42
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i - prints 21
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j - prints 73
}package main
import "fmt"
// Struct declaration
// A struct is a collection of fields.
type pet struct {
name string
age int
}
func newPet(name string, age int) *pet {
p := person{name: name, age: age}
return &p
}
func main() {
// How to create new structs:
fmt.Println(pet{"Max", 5}) //{Max 5}
fmt.Println(pet{name: "Buddy", age: 10}) //{Buddy 10}
// Omited fields will be zero valued
fmt.Println(pet{name: "Shadow"}) //{Shadow 0}
// An & prefix yields a pointer to the struct.
fmt.Println(&pet{name: "Honey", age: 14}) //&{Honey 14}
// access struct fields with a dot
fluffy := pet{name: "Fluffy", age: 7}
fmt.Println(fluffy.name) //Fluffy
// You can also use dots with struct pointers - the pointers are automatically dereferenced.
fluffyPointer := &fluffy
fmt.Println(fluffyPointer.age)
// Structs are mutable
fluffyPointer.age = 8
fmt.Println(fluffyPointer.age) //8
}package messenger
// Note: Capitalized structs, interfaces, consts, vars and functions are exported for use in other packages. Exported fields should be commented.
// Mesage type
type Message struct {
from string
to string
content string
}
// Interface declaration
// An interface type is defined as a set of method signatures.
// MessageService sends messages
type MessageService interface {
SendMessage(msg message) error
}
// Example interface implementation:
// To implement an interface, implement all the methods in the interface
// Notice how the implementation is implicit, not explicit
type messageService struct {
client *SES // AWS Simple Email Service
}
// Note: This is sudo code
func (ms *messageService) SendMessage(msg) error {
err := ms.client.SendMessage(msg)
return err
}- The type [n]T is an array of n values of type T.
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1]) //Hello World
fmt.Println(a) //[Hello World]
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes) //[2 3 5 7 11 13]- For a more in depth overview of slices, check out slices in A Tour of Go
- The type []T is a slice with elements of type T.
- Slices are references to the underlying array. Changing the elements of a slice modifies the corresponding elements of its underlying array. Other slices that share the same underlying array will see those changes.
- Form slice like this: a[low : high]
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s) //[3 5 7]nums := []int{}
// append to a slice
nums = append(nums, 1)
nums = append(nums, 2, 3)
fmt.Println(nums) //[1 2 3]- Slices have length and capacity
- Zero value of a slice is nil
- Maps are similar to dictionaries
- They map keys to values
- Zero value of a map is nil
- The make function returns a map of the given type, initialized and ready for use.
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
// Using map literal
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
// insert or update an element
m[key] = elem
// retrieve an element
elem = m[key]
// delete an element
delete(m, key)
// check that an element exists in the map
// If key is in m, ok is true. If not, ok is false.
elem, ok = m[key]// A familiar for loop (init, exit condition, post)
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum) //45
// init and post statements are optional
// (i.e. for is go's while)
sum = 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum) //1024
// Loop forever by omiting the exit condition
for {
}
// Range
// Loop over a slice
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
// i - index
// v - copy of the element in the array
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
// Output:
// 2**0 = 1
// ... omitted
// 2**7 = 128
// If you only want the index:
i := range pow
// If you only want the value:
_, v := range powif x < 0 {
// Do something
}
if x < 0 {
// Do x < 0 stuff
} else {
// Do x >= 0 stuff
}
if x < 0 {
// Do x < 0 stuff
} else if x == 0 {
// Do x == 0 stuff
} else {
// Do x > 0 stuff
}// Switch does top down evaluation, stopping when case succeeds
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}- A defer statement defers the execution of a function until the surrounding function returns.
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// Output:
// hello
// world- defers can be stacked
func main() {
fmt.Println("counting")
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
// Output:
// counting
// done
// 2
// 1
// 0- Panic is used to create a run time error
- Handle run time panics with the built-in recover function
func main() {
defer func() {
str := recover()
fmt.Println(str)
}()
panic("PANIC")
}
// Output:
// PANICfunc main(){
fmt.Println("Hello World")
}func add(x, y int) int {
return x + y
}Don't use this unless it's dead simple. It worsens readability
func f2() (r int) {
r = 1
return
}func f() (int, int) {
return 5, 6
}Used to pass an indeterminante number of args
func add(args ...int) int {
total := 0
for _, v := range args {
total += v
}
return total
}
// Note: This is how fmt.Println is implemented:
func Println(a ...interface{}) (n int, err error)Note: Methods are functions
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}There are two reasons to use a pointer reciever
- The first is so that the method can modify the value that its receiver points to.
- The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs()) // Before scaling: &{X:3 Y:4}, Abs: 5
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs()) //After scaling: &{X:15 Y:20}, Abs: 25
}func main() {
add := func(x, y int) int {
return x + y
}
fmt.Println(add(1,1)) //2
}func main() {
x := 0
increment := func() {
x++
}
increment()
increment()
fmt.Println(x) //2
}func makeHelloFunc() func() {
return func() {
fmt.Println("Hello")
}
}
func main() {
x := makeHelloFunc()
x()
}
// Output:
// HelloFunctions can call themselves
func factorial(x uint) uint {
if x == 0 {
return 1
}
return x * factorial(x-1)
}- A goroutine is a lightweight thread managed by the Go runtime.
go f(x, y, z) // starts a new go routine
- Evaluation of f, x, y, z happen in the current goroutine and the execution of f happens in the new goroutine.
- Any function can become a go routine just by using the
gokeyword - You can kind of think of
goas Go's async. We'll look at some examples.
- Channels are a typed conduit through which you can send and receive values with the channel operator, <-. They are useful for syncing and allowing different goroutines to communicate.
- By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
ch := make(chan int) // Make a channel of type int
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and assign value to v.- Channels can be buffered. Provide the buffer length as the second argument to make to initialize a buffered channel
- Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.
ch := make(chan int, 100)- Senders can close (
close(c)) a channel to indicate that no more values will be sent - Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression:
v, ok := <-ch
// ok is false if there are no more values to receive and the channel is closed.- The loop for i := range c receives values from the channel repeatedly until it is closed.
for i := range c {
fmt.Println(i)
}- The select statement lets a goroutine wait on multiple communication operations.
- The default case in a select is run if no other case is ready.
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
default:
// Runs if no other case is ready
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}- Chanels can be send only and recieve only
chan<- Tsend only<-chan Trecieve only- If you can't remember which is which, look at the arrow.
- Pointing into
chan- send - Pointing away from
chan- recieve
- Pointing into
- Useful for when you want to be sure that a function can only read or write (not both) to a channel
func SendOnly(pings chan<- string) {
// pings is a send only chan
}
func RecieveOnly(pongs <-chan string) {
// pongs is a recieve only chan
}