Skip to content

02. Week 2 Golang Basic 3

Tran Phong Phu edited this page Apr 15, 2019 · 6 revisions

Deferred Functions Calls

Go has a special statement called defer that schedules a function call to be run after the function completes. Consider the following example:

package main
import "fmt"
func first() {
  fmt.Println("First")
}
func second() {
  fmt.Println("Second")
}
func main() {
  defer second()
  first()
}

This program prints First followed by Second.

A defer statement is often used with paired operations like open and close, connect and disconnect, or lock and unlock to ensure that resources are released in all cases, no matter how complex the control flow. The right place for a defer statement that releases a resource is immediately after the resource has been successfully acquired.

Below is the example to open a file and perform read/write action on it. In this example there are often spots where you want to return early.

Without defer

func ReadWrite() bool {
  file.Open("file")

  if failureX {
    file.Close()   //And here...
    return false
  }
  if failureY {
    file.Close()  //And here...
    return false
  }
  file.Close()  //And here...
  return true
}

A lot of code is repeated here. To overcome this Go has the defer statement. The code above could be rewritten as follows. This makes the function more readable, shorter and puts the Close right next to the Open.

With defer

func ReadWrite() bool {
  file.Open("file")
  defer file.Close()   //file.Close() is added to defer list
  // Do your thing
  if failureX {
    return false   // Close() is now done automatically
  }
  if failureY {
    return false   // And here too
  }
  return true   // And here
}

This has various advantages

  • It keeps our Close call near our Open call so it's easier to understand.
  • If our function had multiple return statements (perhaps one in an if and one in an else), Close will happen before both of them.
  • Deferred Functions are run even if a runtime panic occurs.
  • Deferred functions are executed in LIFO order, so the above code prints: 4 3 2 1 0.
  • You can put multiple functions on the "deferred list", like this example.
package main
import "fmt"
func main() {
  for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
  }
}

Panic and Recover

The built-in type system of GO Language catches many mistakes at compile time, but unable to check mistakes like an out-of-bounds array, access or nil pointer deference which require checks at run time. GO does not have an exception mechanism you can't throw exceptions. During the execution when Go detects these mistakes, it panics and stops all normal execution, all deferred function calls in that goroutine are executed and finally program crashes with a log message. This log message usually has enough information to analyze the root cause of the problem without running the program repeatedly, so it should always be included in a bug report about a panicking program.

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function X calls panic, execution of X stops, any deferred functions in X are executed normally, and then X returns to its caller. To the caller, X then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by run-time errors, such as out-of-bounds array accesses.

Not all panics come from the run-time. The built-in panic function may be called directly; it accepts any value as an argument. A panic is usually the best thing to do when some "impossible" situation happens, for instance, execution reaches a case that logically can't happen:

package main
import "fmt"
func main() {
  var action int
  fmt.Println("Enter 1 for Student and 2 for Professional")
  fmt.Scanln(&action)
  /*  Use of Switch Case in Golang */  
  switch action {
    case 1:
      fmt.Printf("I am a  Student")
    case 2:
      fmt.Printf("I am a  Professional")
    default:
      panic(fmt.Sprintf("I am a  %d",action))
  }
  fmt.Println("")
  fmt.Println("Enter 1 for US and 2 for UK")
  fmt.Scanln(&action)
  /*  Use of Switch Case in Golang */  
  switch action{
    case 1:
      fmt.Printf("US")
    case 2:
      fmt.Printf("UK")
    default:
      panic(fmt.Sprintf("I am a  %d",action))
  }
}

In above program program will stop execution after first switch-case if user enters any other value other that 1 or 2.

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

package main
import "fmt"
func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("Recovered in:", err)
    }
  }()
  var action int
  fmt.Println("Enter 1 for Student and 2 for Professional")
  fmt.Scanln(&action)
  /*  Use of Switch Case in Golang */  
  switch action {
    case 1:
      fmt.Printf("I am a  Student")
    case 2:
      fmt.Printf("I am a  Professional")
    default:
      panic(fmt.Sprintf("I am a  %d",action))
  }
}

Concurrency

Popular programming languages such as Java and Python implement concurrency by using threads. Go takes a different route. Go uses the concurrency model called Communicating Sequential Processes (CSP). Networking, client-server or distributed computing programs that execute different pieces of code simultaneously, possibly on different processors or computers. Concurrency is a built-in feature of Go, and the Go runtime has great control over the programs that run with its concurrency features. Many other programming languages have third-party libraries (or extensions), but inherent concurrency is something unique to modern languages, and it is a core feature of Go's design. The basic building blocks Go proposes for structuring concurrent programs are goroutines and channels.

What are goroutines?

The parts(threads) of an application(process) that run concurrently are called goroutines in Go or you can say a concurrently executing function in Go is called a goroutine. Goroutines let you run functions independent of each other.

Consider a program that has two functions, one function writes some output and other one that writes some other output, and assume that neither function calls each other. In a sequential program may call one function and then call the other, but in a concurrent program with two or more goroutines, calls to both functions can be active at the same time.

When a function is created as a goroutine, it's treated as an independent unit of work that gets scheduled and then executed on an available logical processor. The Go runtime scheduler which manages all the goroutines that are created and need processor time. The scheduler sits on top of the operating system, binding operating system's threads to logical processors which, in turn, execute goroutines. There is no one-to-one correspondence between a goroutine and an operating system thread. The scheduler controls everything related to which goroutines are running on which logical processors at any given time.

To run a function as a goroutine, call that function prefixed with the go statement.

Here is the example code block:

printCountry() // A normal function call that executes printCountry synchronously and waits for completing it
go printCountry() // A goroutine that executes printCountry asynchronously and doesn't wait for completing it

The difference between a normal function call and a goroutine is that a goroutine is created with the go statement. An executable Go program does have at least one goroutine; the goroutine that calls the main function is known as the main goroutine.

package main
  
import (
  "fmt"
  "time"
  "sync"
)   
  
type testConcurrency struct {
  min int
  max int
  country string
}
  
func printCountry(test *testConcurrency, groupTest *sync.WaitGroup) {   
  for i :=test.max ; i>test.min; i-- { 
    time.Sleep(1*time.Millisecond)
    fmt.Println(test.country)
  }

  fmt.Println()
  groupTest.Done()
}
  
func  main() {
  groupTest := new(sync.WaitGroup)
    
  japan := new(testConcurrency)
  china := new(testConcurrency)
  india := new(testConcurrency)

  japan.country = "Japan"
  japan.min = 0
  japan.max = 5

  china.country = "China"
  china.min = 0
  china.max = 5

  india.country = "India"
  india.min = 0
  india.max = 5

  go printCountry(japan, groupTest)
  go printCountry(china, groupTest)
  go printCountry(india, groupTest)

  groupTest.Add(3)
  groupTest.Wait()
}

What is wait group ?

Go's standard library provides several useful tools for working with synchronization. One that frequently comes in handy is sync.WaitGroup, a tool for telling one goroutine to wait until other goroutines complete.

A wait group is a message-passing facility that signals a waiting goroutine when it's safe to proceed. To use it, you tell the wait group when you want it to wait for something, and then you signal it again when that thing is done. A wait group doesn't need to know more about the things it's waiting for other than

  1. the number of things it's waiting for
  2. when each thing is done. You increment the first with groupTest.

Add, and as your task completes, you signal this with groupTest.Done. The groupTest.Wait function blocks

This program also synchronizes the execution using sync.WaitGroup while executing the goroutines; here function main is waiting for completion of the execution of goroutines using sync.WaitGroup.

go printCountry(japan, groupTest)
go printCountry(china, groupTest)
go printCountry(india, groupTest)

The program uses the WaitGroup type of sync package, which is used to wait for the program to finish all goroutines launched from the main function. Otherwise the goroutines would be launched from main function and then terminate the program before completing the execution of goroutines. The Wait method of the WaitGroup type waits for the program to finish all goroutines. The WaitGroup type uses a counter that specifies the number of goroutines, and Wait blocks the execution of the program until the WaitGroup counter is zero.

groupTest.Add(3)

The Add method is used to add a counter to the WaitGroup so that a call to the Wait method blocks execution until the WaitGroup counter is zero. Here a counter of three is added into the WaitGroup, one for each goroutine.

groupTest.Wait()

When the Wait method is called inside the main function, it blocks execution until the WaitGroup counter reaches the value of zero and ensures that all goroutines are executed.

When you run the program, the output would vary each time because the execution is randomly delaying inside the functions.

Working with Channels

Go provides a mechanism called a channel that is used to share data between goroutines. When you execute a concurrent activity as a goroutine a resource or data needs to be shared between goroutines, channels act as a conduit(pipe) between the goroutines and provide a mechanism that guarantees a synchronous exchange. Data type need to be specified at the time of declaration of a channel. We can share Values and pointers of built-in, named, struct, and reference types. Data are passed around on channels: only one goroutine has access to a data item at any given time: so data races cannot occur, by design.

There are two types of channels based on their behavior of data exchange: unbuffered channels and buffered channels. An unbuffered channel is used to perform synchronous communication between goroutines while a buffered channel is used for perform asynchronous communication. An unbuffered channel provides a guarantee that an exchange between two goroutines is performed at the instant the send and receive take place. A buffered channel has no such guarantee.

A channel is created by the make function, which specifies the chan keyword and a channel's element type.

Here is the code block that creates an unbuffered and buffered channel:

unbuffered := make(chan int) // Unbuffered channel of integer type
buffered := make(chan int, 10)  // Buffered channel of integer type

The use of the built-in function make to create both an unbuffered and buffered channel. The first argument to make requires the keyword chan and then the type of data the channel will allow to be exchanged.

Here is the code block to send a value into a channel requires the use of the <- operator:

goroutine1 := make(chan string, 5) // Buffered channel of strings.
goroutine1 <- "Australia" // Send a string through the channel.

A goroutine1 channel of type string that contains a buffer of 5 values. Then we send the string "Australia" through the channel.

Here is the code block that receives values from a channel:

data := <-goroutine1 // Receive a string from the channel.

The <- operator is attached to the left side of the channel variable(goroutine1), to receive a value from a channel.

Unbuffered channels

In unbuffered channel there is no capacity to hold any value before it's received. In this type of channels both a sending and receiving goroutine to be ready at the same instant before any send or receive operation can complete. If the two goroutines aren't ready at the same instant, the channel makes the goroutine that performs its respective send or receive operation first wait. Synchronization is fundamental in the interaction between the send and receive on the channel. One can't happen without the other.

Buffered channels

In buffered channel there is a capacity to hold one or more values before they're received. In this types of channels don't force goroutines to be ready at the same instant to perform sends and receives. There are also different conditions for when a send or receive does block. A receive will block only if there's no value in the channel to receive. A send will block only if there's no available buffer to place the value being sent.

Simple program to demonstrate use of Buffered Channel

package main
 
import (
  "fmt"  
  "math/rand"
  "sync"
  "time"
)
 
var goRoutine sync.WaitGroup
 
func main(){
  rand.Seed(time.Now().Unix())

  // Create a buffered channel to manage the employee vs project load.
  projects := make(chan string,10)

  // Launch 5 goroutines to handle the projects.
  goRoutine.Add(5)
  for i :=1; i <= 5; i++ {
    go employee(projects, i)
  }

  for j :=1; j <= 10; j++ {
    projects <- fmt.Sprintf("Project :%d", j)
  }

  // Close the channel so the goroutines will quit    
  close(projects)
  goRoutine.Wait()
}
 
func employee(projects chan string, employee int) {
  defer goRoutine.Done()
  for {
    // Wait for project to be assigned.
    project, result := <-projects

    if result==false {
      // This means the channel is empty and closed.
      fmt.Printf("Employee : %d : Exit\n", employee)
      return
    }

    fmt.Printf("Employee : %d : Started   %s\n", employee, project)

    // Randomly wait to simulate work time.
    sleep := rand.Int63n(50)
    time.Sleep(time.Duration(sleep) * time.Millisecond)
    // Display time to wait
    fmt.Println("\nTime to sleep",sleep,"ms\n")

    // Display project completed by employee.
    fmt.Printf("Employee : %d : Completed %s\n", employee, project)
  }
}

Every time you run this program the output for this program will be different this is because of the random nature of the program and the Go scheduler. In above program, a buffered channel of type string is created with a capacity of 10. WaitGroup is given the count of 5, one for each goroutine. 10 strings are sent into the channel to simulate or replicate project for the goroutines. Once the last string is sent into the channel, the channel is going to be closed and the main function waits for all the project to be completed.

Logging Go Programs

The standard library package log provides a basic infrastructure for log management in GO language that can be used for logging our GO programs. The main purpose of logging is to get a trace of what's happening in the program, where it's happening, and when it's happening. Logs can be providing code tracing, profiling, and analytics. Logging(eyes and ears of a programmer) is a way to find those bugs and learn more about how the program is functioning.

To work with package log, we must add it to the list of imports:

import (
  "log"
)

In its simplest usage, it formats messages and sends them to Standard Error check below example:

// Program in GO language to demonstrates how to use base log package.
package main
import (
  "log"
)
func init(){
  log.SetPrefix("LOG: ")
  log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
  log.Println("init started")
}
func main() {
  // Println writes to the standard logger.
  log.Println("main started")
 
  // Fatalln is Println() followed by a call to os.Exit(1)
  log.Fatalln("fatal message")
 
  // Panicln is Println() followed by a call to panic()
  log.Panicln("panic message")
}

After executing this code, the output would look something like this:

go run main.go 

LOG: 2019/03/15 17:09:39.866969 /Users/tranphongphu/workspace/golang-training/simple-logging/main.go:11: init started
LOG: 2019/03/15 17:09:39.867044 /Users/tranphongphu/workspace/golang-training/simple-logging/main.go:15: main started
LOG: 2019/03/15 17:09:39.867050 /Users/tranphongphu/workspace/golang-training/simple-logging/main.go:18: fatal message
exit status 1

Sending messages to Standard Error is useful for simple tools. When we're building servers, applications, or system services, we need a better place to send your log messages. Here all error messages are all sent to Standard Error, regardless of whether the message is an actual error or an informational message.

The standard log entry contains below things:

  • a prefix (log.SetPrefix("LOG: "))
  • a datetime stamp (log.Ldate)
  • full path to the source code file writing to the log (log.Llongfile)
  • the line of code performing the write and finally the message.

This pieces of information are automatically generated for us, information about when the event happened and information about where it happened.

Println is the standard way to write log messages. Fatalln or any of the other "fatal" calls, the library prints the error message and then calls os.Exit(1), forcing the program to quit. Panicln is used to write a log message and then issue a panic which may unless recovered or will cause the program to terminate.

Program in GO language with real world example of logging.

Now i am taking a real world example and implementing above log package in my program. For example i am testing an SMTP connection is working fine or not. For test case I am going to connect to a SMTP server "smtp.smail.com" which is not exist, hence program will terminate with a log message.

// Program in GO language with real world example of logging.
package main
 
import (
"log"
"net/smtp"
)
func init(){
  log.SetPrefix("TRACE: ")
  log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
  log.Println("init started")
}
func main() {
  // Connect to the remote SMTP server.
  client, err := smtp.Dial("smtp.smail.com:25")
  if err != nil { 
    log.Fatalln(err)
  }
  client.Data()
}
go run main.go 
TRACE: 2019/03/15 17:21:15.966047 /Users/tranphongphu/workspace/golang-training/real-
world-logging/main.go:11: init started
TRACE: 2019/03/15 17:21:15.968969 /Users/tranphongphu/workspace/golang-training/real-
world-logging/main.go:17: dial tcp: lookup smtp.smail.com: no such host
exit status 1

The above program is throwing fatal exception from log.Fatalln(err).