When it comes to declaring variables, to have them available to other files when the package is imported, declare the variable with a capital letter.
Lowercase variables are private.
// outside of func var greeting = "hello" func main() { // inside of func greeting := "Hello from Go" fmt.Println(test) fmt.Println(greeting) }
func main() { var a int a = 2 var b, c int b = 2 c = 3 d := 5 // same as var d = 5 var e = 10 // type is inferred // you'll need to use all the above variables // otherwise there will be a declaration err // thrown }
var floating float64 = 1.4
float64(variable)
// not available outside the package func main() { myFunction() } func myFunction() { fmt.Println("Running myFunction") } func MyPublicFunction() { fmt.Println("Running MyPublicFunction") }
You can enforce type safety for parameters by adding the type expected to the function func myFunc(test string, number int) {}
For returning a certain value, you can enfore this by the following
func myFunc(test string, number int) int {} func myFuncTwo(numberOne int, number int) (sum int) { return number + numberOne; } func myFuncThree(number int) (difference int) { difference = number + 4; }
package main import ( "fmt", "math", "log" ) func main() { squareRoot, err := squareRoot(-1) if err != nil { log.Fatal(err) } fmt.Println(squareRoot) } func squareRoot(x float64) (float64, error) { x < 0 { return 0, fmt.Errorf("Can't take a negative number") } return math.Sqrt(x), nil }
As for errors
package main import ( "fmt", "os" ) // panic errors - no good! func mainBad() { fileInfo, _ := os.Stat("existent.txt") fmt.Println(fileInfo.Size()) fileInfo, _ := os.Stat("nonexistent.txt") fmt.Println(fileInfo.Size()) } // instead, do this func main() { fileInfo, error := os.Stat("existent.txt") if error != nil { fmt.Println(error) } else { fmt.Println(fileInfo.Size()) } fileInfo, error := os.Stat("nonexistent.txt") if error != nil { fmt.Println(error) } else { fmt.Println(fileInfo.Size()) } }
go fmt <filename>
will update the file itself and it will nicely format it.
for i := 1; i <= 3; i++ { fmt.Println(i) }
if true { fmt.Println("You'll come here") } else if false { // ... } else { // ... }
Switch statements look like they do not need a break.
switch doorNumber { case 1: fmt.Println("new car ") case 2: // ... default: // ... }
You can create a pointer to a variable too.
package main import "fmt" func main() { var aValue float64 = 1.23 var aPointer *float64 = &aValue fmt.Println("aPointer", aPointer) fmt.Println("*aPointer", *aPointer) } /* Prints aPointer 0xc42000a3b8 *aPointer 1.23 */
There are situations where the pointer is better to use than using a value directly.
// Example package main import "fmt" // Not using the pointer func main() { num := 8.2 halve(num) fmt.Println(num) } func halve(number float64) { number = number / 2; fmt.Println(number) } // Using the pointer package main import "fmt" // Not using the pointer func main() { num := 8.2 halve(&num) fmt.Println(num) } func halve(number *float64) { *number = *number / 2 fmt.Println(*number) }
Slices are used more commonly used than arrays, but given they are built on arrays, we'll explore arrays first.
func main() { var months [3]string // array of three strings months[0] = "Jan" months[1] = "Feb" months[2] = "Mar" fmt.Println(months[0]) // also could be months := [3]string{"Jan", "Feb", "Mar"} for i := 0; i < len(months); i++ { fmt.Println(months[i]) } // another way to loop through the array for i, month := range months { fmt.Println(month) } // omit the index for _, month := range months { fmt.Println(month) } }
The main limitation arrays are used are because you cannot assign values to an array larger than it's initial allocated memory size.
Slices also represent an array. Slices are easier to work with.
While len
shows the length, cap
shows the capacity of how it can grow.
append
can be used to append to a slice.
package main import "fmt" func main() { a := [5]int{0,1,2,3,4} s1 := a[0:3] s2 := a[2:5] fmt.Println(a, s1, s2) // prints [0 1 2 3 4] [0 1 2] [2 3 4] a[2] = 88 fmt.Println(a, s1, s2) // prints [0 1 88 3 4] [0 1 88] [2 3 88] s1 = s1[0:4] fmt.Println(a, s1, s2) // prints [0 1 88 3 4] [0 1 88 3] [88 3 4] s2 = s2[0:4] // throws an error s2 = append(s2, 5) // returns a new slice fmt.Println(a, s1, s2) // prints [0 1 88 3 4] [0 1 88 3] [88 3 4 5] s2[0] = 999 // prints [0 1 88 3 4] [0 1 88 3] [999 3 4 5] // Declaring an array on its own s3 := []int{1, 2, 3} fmt.Println(s3) // prints [1 2 3] s3 = append(s3, 4, 5) fmt.Println(s3) // [1 2 3 4 5] }
Slices are good for storing collections, but the only way to get elements back is by the index.
While in most collections you have dictionaries, hashes, hash maps, Go refers to these data structures as Maps
.
func main() { ages := map[string]float64{} ages["Alice"] = 12 ages["Bob"] = 9 fmt.Println(ages) // prints map[Alice:12 Bob:9] }
Similar to arrays or slices, we can use a literal to prefill the values.
func main() { ages := map[string]float64{"Alice":12, "Bob":9} fmt.Println(ages) // prints map[Alice:12 Bob:9] for name, age := range ages { fmt.Println(name, age) } for _, age := range ages { fmt.Println(age) } for name := range ages { fmt.Println(name) } }
We can use the type
keyword to define a type and it's underlying type.
package main import "fmt" type Minutes int type Hours int func main() { minutes := Minutes(37) hours := Hours(37) fmt.Println(minutes, hours) }
We can also compare custom types to their underlying type. However two custom types with the same underlying type cannot be compared.
The more commonly used aggregate type is a Struct which is how we can base custom types on them.
Define new behaviours for types.
package main import ( "fmt" "strings" ) type Title string // the following has an extra reciever arg t func (t Title) FixCase() string { return strings.Title(string(t)) } func main() { name := Title("the matrix") fixed := name.FixCase() fmt.Println(fixed) }
package main import ( "fmt" ) type Hours int func (h *Hours) Increment() Hours { *h = (*h + 1) % 24 return *h } func main() { hours := Hours(23) hours.Increment() fmt.Println(hours) // Prints "0" }
package main import "fmt" type Monitor struct { Resolution string Connector string Value float64 // fields with a name and a type } func main() { monitor := Monitor{} monitor.Resolution = "1080p" monitor.Connector = "HDMI" monitor.Value = 249.99 fmt.Println(monitor.Resolution, monitor.Connector, monitor.Value) // could also go monitor := Monitor{"1080p", "HDMI", 249.99} }
If you init and allocate a struct, it will initialise with some default values.
You can also add a "exported" get and set method to help enforce the concept of private variables.
package main import ( "fmt" ) type Clock struct { Hours int Minutes int } // DEFINE A "Noon" FUNCTION HERE func Noon(hours int, min int) Clock { c := Clock{} c.Hours = 12 c.Minutes = 0 return c } func main() { c := Noon(12, 10) fmt.Println(c) }
When you have a concrete type, you know what it is and what it can do. An interface is defining what something is but not what it can do.
type FourLegged interface { Walk() Sit() }
Implementation in practise
type Part interface { Specs() string Price() string } func Summary(part Part) string { return part.Specs() + "/n" + part.Price() }
If we create slice with that type as well, then we can use that slice for anything that satisfies the interface. This could allow us to slice, append to the slice and use the range to iterate through.
// calendar.go package calendar import "fmt" type Calendar struct { Year int Month int Day int } func (c Calendar) Display() { fmt.Printf("%04d-%02d-%02d", c.Year, c.Month, c.Day) } // clock.go package clock import "fmt" type Clock struct { Hours int Minutes int } func (c Clock) Display() { fmt.Printf("%02d:%02d", c.Hours, c.Minutes) } // schedule.go package schedule // DECLARE A Displayable INTERFACE HERE type Displayable interface { Display() } // DECLARE A Print FUNCTION HERE func Print(display Displayable) { display.Display() }
package main import ( "fmt" "time" ) func longTask() { fmt.Println("Starting long task") time.Sleep(3 * time.Second) fmt.Println("Long task finished") } func main() { go longTask() go longTask() go longTask() time.Sleep(4 * time.Second) // just for show - we can use channels instead }
We can't simply try to use the time.Sleep with the keyword go given that the go routine doesn't give a value right away.
package main import ( "fmt" "math/rand" "time" ) func longTask() int { delay := rand.Intn(5) fmt.Println("Starting long task") time.Sleep(time.Duration(delay) * time.Second) fmt.Println("Long task finished") return delay } func main() { rand.Seed(time.Now().Unix()) time := longTask() fmt.Println("Took", time, "seconds") }
What we can do instead is use a channel to pass a message back to the main go routine.
package main import ( "fmt" "math/rand" "time" ) // notice we get rid of the int return value func longTask(channel chan int) { delay := rand.Intn(5) fmt.Println("Starting long task") time.Sleep(time.Duration(delay) * time.Second) fmt.Println("Long task finished") channel <- delay } func main() { rand.Seed(time.Now().Unix()) channel := make(chan int) for i := 1; i <= 3; i++ { go longTask(channel) } // uses the arrow prefix for i := 1; i <= 3; i++ { fmt.Println("Took", <-channel, "seconds") } }
package channels func readFromChannel() string { // CREATE A CHANNEL FOR string VALUES HERE channel := make(chan string) // HERE, CALL writeToChannel AS A GOROUTINE, AND PASS IT YOUR CHANNEL go writeToChannel(channel) // HERE, READ A STRING FROM YOUR CHANNEL AND RETURN IT return <-channel }
In the course, we've looked at: