Quick Notes on Go - Part 1

In this post, I share some notes on Go.



Collections

- We have three options:
  • Arrays: Like C++ arrays. Fixed size. 
  • Slices: Like C++ Vectors.
  • Maps: Hash maps
- [] vs. [...]:
  • [...]myArray{1, 2, 3} is an array
  • []mySllice{1, 2, 3} is a slice
- Slice has length and capacity. You can specify both at initialization:
  • x := make ([]int, length, capacity)
- To copy a slice, use copy function: copy(x, source)

- Map:
  • map[keyType]valueType
    • map[int][]string is a map of int to slice of string.
  • You can initialize like {"key": value, }
  • Or make:  make(map[string]int, 10)
  • To insert: myMap["newKey"] = value 
  • To delete: delete(myMap, key)
  • To search: Use comma Ok idiom: if v, ok := myMap[key]; ok { 
- If you have map as a struct member, you have to make sure to initialize it.
Example:
type MyType struct {
myMap map[string]int
}

func main() {
myType := MyType{}
myType.myMap = make(map[string]int)
myType.myMap["age"] = 20
 
}
or  do this:
func main() {
myType := MyType{myMap: map[string]int{}}
myType.myMap["age"] = 20
 
}
- We don't have Sets. Two ways to simulates:
  • Map to boolean: easier to use.
Example
mySet := map[int]bool
mySet[23] = true //to insert to set
if mySet[23] { //to check membership
  • Map to struct{}: less space.
Example:
mySet := map[int]struct{}
mySet[23] = struct{}{} //to insert 
if _, ok := mySet[23]; ok { //to check membership

- Map is implemented as a pointer to a struct. 
  • That's why when although passing is by value in Go, passing a map to a function and changing its entries, changes the original map.
  • Also, you cannot compare two maps, as the comparison compares their pointers.
- Slices are implemented as a pointer to an array, length, and capacity of the slices. 
  • Thus, updating entries of a passed slice changes the original slice as well. 
  • If you append to a slice in a function, it does not adds the entry to the original slice, as the original slice has different array or length, and those are not updated by the append inside the function.
So be careful when you pass maps or slices to functions.

- Note that comparing two instances of an struct that has a slice or map member is also not possible. 

Control Structures

- You can define a variable in the if statement and use in the entire if else block.
Example: Note that n is defined in if, but it is accessible in entire block! 
if n := rand.Intn(100); n == 0 {
    fmt.Println("It is zero")
} else if n > 50 {
    fmt.Println("It is higher than 50", n)
} else {
    fmt.Println("It is in (0, 50]", n)
}

- For is the only loop. You use it in four different ways:
  1. Normal for statement: for i := 1; i < n; i++ { 
  2. Like while loop: for condition { 
  3. Infinite loop: for { 
  4. for-rage: for i, v := range mySlice/myMap/channel { 
- You can break or continue for loops. You can add label like myLabel: and break or continue to a label like this continue myLabel. It is useful when you have nested loops. 

- You can use goto myLabel, as well.

- switch:
  • No fall-through: so you don't need to put break after each case. 
    • Instead, if you do want fall-through, you can add fallthrough statement at the end of your case.
  • blank switch: You don't switch on anything. You may define a variable, and write conditions in the cases. 
    • It is like if else. 

Functions

- Functions are pass-by-value.
- No named or optional parameters. If you want those, you can use a struct to package your parameters. 
- You can use variadic parameters:
Example:  func myfunc(vals ...int)
Now, vals is a slice in the body. 
  • You can call myFunc with various number of variables.
  • You can also pass a slice to it. However, note that to that, you have to put ... after your parameter name like this: myfunc(mySlice...)
- You can return multiple return values. If you don't want to use a value, use _. 

- Functions are values. You can assign a function to a variable.
Example: var myVar = func myFunc() { 

- You can define a function type. 
Example: type myFuncType func(.. 

- Closure: Functions defined inside a function are called closure.
  • The closure captures the variable declared in the defining function.
  • Example use case: When you want to call a piece of logic multiple types but all in the scope of a function, you can define your logic in a closure to limit the number of function definitions in the package.
  • Example: defer routine that is called after return statement is done. 
    • You can have multiple defers. They will be executed in LIFO manner.
- You can pass a function as a parameter to another function, or return it as a return value. The function that receives or return another function is called a higher-order function.

- To document a function add // right above the function and start with the function name.
//MyFunction does this
func MyFunction() { 

Pointers

- new keyword is rarely used:
  • Do this instead: myPointer := &myType{}
  • Escape Analysis: You can return a pointer to a local variable! The Go compiler will convert your variable to pointer automatically. This is called escape analysis, i.e., your variable escape the stack and goes to heap.
    • Go's escape analysis is not perfect, but the good things is that it never causes invalid pointers, but it can escape a variable to heap when it is not needed, which is not end of the word. So you can rely on it confidently.
- interface{} is like C++'s void*, pointer to everything. You can get the actual type with .(type)

Methods

- To define methods for a type, you can pass the type by value or reference. Use reference when your method should mutate your struct instance.
Example: 
func (t myType) myMethod {. //you can think of it like c++ const method
or 
func (t *myType) myMethod {. //a method that can mutate state of t.

- You can define method to be called without any issue when called on a nil instance of your type! 
Example:
func (t *myType) myMethod { 
   if (t == nil) {
      // do something here

- You can use methods as functions. You can things of method as function + a state

- Embedding: No inheritance in Go. Instead, Go has first-class support for embeding
  • You can embed another type in another type and access its methods and value directly.

Interfaces

- Interface is implemented with two pointers: one for the type, and one for the actual instance.

- Warning: For an interface to be nil, both of its pointer must be nil. Thus, we have the following sort of unexpected behavior:

var interfaceInstance MyInterface
var myStruct MyStruct 
interfaceInstance = myStruct 

Now, myStruct is nil, but interfaceInstance is NOT nil, because its type pointer is not nil and referring to MyStruct!

It is important especially when you define custom error types implementing the error interface.
Example: Suppose MyError is my custom error type that implements the error interface.

func MyFunc() error {
    var myError MyError
    ..
    return myError
}

Now, the error returned from MyFunc is never nil! Thus, when you don't have error, explicitly return nil.

- You can embed an interface in a struct. Doing so, makes your struct to be treated as a struct that implements the interface, but there is not implementation for the interface methods. We use it for tests. If you do that, make sure you implements those methods of the the interface that will be called during the test.

- Suppose you have a struct that implements interface A, but you need a struct that implements both A and B interfaces. You can do this:

type ImplementingAandB struct {
    ImplementingJustA
}

func (i ImplementingAandB) BMethod () {
    //implement the additional method in B for a struct that implement A
}

func GetImplementingAandB(a ImplementingJustA) AB {
    return ImplementingAandB{a}
}

Now, whenever you need AB, but you have instance a of ImplementingJustA, you can use GetImplementingAandB(a).

- Note when you implement an interface, you can implement it for the value of your type or for the pointer of your type. If you implement it for the pointer type, then only pointer considered by the Go compiler to be implementing the interface. 

func (m *MyType) InterfaceFunc() {
}

- Type-safe duck typing: 
  • Duck typing: "If it walks and quacks like a duck, it is a duck" So we don't need to explicitly say a struct implements an interface like in Java.
  • We still have interfaces in Go, but they are defined by the client code, instead of the provider code.
    • By doing so, the client specifies what it needs.
    • If a class happens to be implementing that interfaces, Go does not complain. Thus, the provider class does not need to explicitly say "Hey I implement this interface"
    • Benefits:
      • At one hand, we have type safety and can know what a client needs without reading the entire client code.
      • On the other hand, we provide code is decoupled from the interface, so we have higher flexibility. 
- Functions can implement an interface!
Example:

The interface that we want:
type MyInterface interface {
   ReqFunc(int a)
}

Now, we define a function type:
type MyFuncType  func(int a) 

And, implement the interface: To implement we just call our function.
func (f MyFuncType) ReqFunc (int a) {
   f(a)
}

Now, any function with (int a) signature, can be converted to MyFuncType and be used a function that implement MyInterface interface. So with this trick, we made it possible to use any function with the required signature to be used as a function implementing our interface.

Dependency Injection

- Dependency injection means: when your code depends on something, you specify it explicitly, and let the code that calls your code inject dependencies that your code needs. 

- The code that needs dependencies, explicitly specifies its dependencies via interfaces.

Dependent Code:
type Printer interface {
   print(s string) error
}

MyStruct depends on Printer:
type MyStruct {
   printer Printer
}

func MyStructFactory(p Printer) {
   return MyStruct{
      printer: p,
   }
}

Provider Code:

Suppose we have this third-party code. Note that it does not implement any interface and we don't like to change this third-party code.

func print(s string) {
       fmt.Println(s)
}

Client Code:
In the client code this is what we wish to do: We want to use the dependent code (MyStruct) and inject the print function above to it.

To do that, we do this:
  • We define a function type that implements the Printer interface:
type PrinterFunc func(s string)

func (f PrinterFunc) print(s string) {
   f.print(s)
}
  • Now, by converting print to PritnerFunc, we can use it an instance of Printer interface:
client code {
   printerInstance := PrinterFunc(print)
   
   //injection
   MyStructVar := MyStructFactory(printerInstance)

   //now we can use MyStructVar that uses our injected Printer.
}

Go to Part 2

Comments

Popular posts from this blog

In-memory vs. On-disk Databases

ByteGraph: A Graph Database for TikTok

Amazon DynamoDB: ACID Transactions using Timestamp Ordering

Eventual Consistency and Conflict Resolution - Part 1