UP | HOME
Land of Lisp

Zhao Wei

How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

Practise on Golang

1. Introduction

This doc records down my practise on Golang from work. It includes the problems I met when I swtiched to Golang as a 3 years experienced developer.

2. Prepare develoment environment

2.1. Install Go

  1. Update ubuntu

    sudo apt-get -y update
    
    • Troubleshooting: invliad PPA error

      ls /etc/apt/sources.list.d
      sudo rm -i /etc/apt/sources.list.d/yarn.list
      sudo rm -i /etc/apt/sources.list.d/yarn.list.save
      
  2. Download go install package
    • Go to the download page and download the binary release suitable for your system.
    • Extract the archive file

      sudo tar -C /usr/local -xzf go1.16.6.linux-amd64.tar.gz
      
    • Make sure go/bin is in PATH

      export PATH=$PATH:/usr/local/go/bin
      
    • Check installed go version and its env

      go version
      go env
      

2.2. (optional) Set Go proxy for development in China

  1. What is Go proxy?
  2. Set proxy according to your location

    go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
    # Or other proxy
    go env -w GOPROXY=https://goproxy.io/zh/,direct
    
  3. Test proxy setting works

    time go get golang.org/x/tour
    
  4. To unset proxy

    go env -u GOPROXY
    
    # Now it should show default one: 
    go env | grep proxy
    # GOPROXY="https://proxy.golang.org,direct"
    

2.3. Setup project

  1. Create folder -tester
  2. Cd into -tester, then initialize the project as go module

    go mod init loadtest
    
    go get -u github.com/aws/aws-sdk-go/...
    
  3. Test aws configuration(~/.aws/config) since this work is related with using AWS SDK.

    package main
    
    import (
      "fmt"
      "github.com/aws/aws-sdk-go/aws"
      "github.com/aws/aws-sdk-go/aws/session"
    )
    
    func main() {
      sess, err := session.NewSession(&aws.Config{
        Region: aws.String("us-east-1")},
      )
    
      if err != nil {
        fmt.Println("session error:", err)
      }
    
      fmt.Printf("using session: %v\n", sess)
    }
    
  4. Then, go run main.go to see if it works.
  5. We will implement different packages(.go and *_test.go in one folder) under this loadtest module(the whole code repository). In one package, we use other packages like:

    package main
    
    import (
      "fmt"
      "loadtest/aws"
      "loadtest/cmd"
      "loadtest/tools"
        ...
    )
    

    Notice: you could not import main package into other package. So, keep main package simple.

2.4. About Golang SDK

2.4.1. The Go AWS SDK convention

  • In AWS Go SDK document, it uses a lot of pointers. And Go SDK provides helper functions which do the convertion between values to pointers and pointers to values. See: Value and Pointer Conversion Utilities.
  • Examples

    var strPtr *string
    
    // Without the SDK's conversion functions
    str := "my string"
    strPtr = &str
    
    // With the SDK's conversion functions
    strPtr = aws.String("my string")
    
    // Convert *string to string value
    str = aws.StringValue(strPtr)
    
    var strPtrs []*string
    var strs []string = []string{"Go", "Gophers", "Go"}
    
    // Convert []string to []*string
    strPtrs = aws.StringSlice(strs)
    
    // Convert []*string to []string
    strs = aws.StringValueSlice(strPtrs)
    

2.4.2. Unit test with AWS SDK

  • In Golang we use interface to decouple the dependencies of component. From AWS SDK, it provides a lot of useful interfaces for us to use.
  • Then, We just use those interface instead of concrete type in our struct. So later, we could mock API easily.

3. The practise from code

The Golang code I am writing is a loadtest script:

  • It utilizes existing external code, a javascript script tester which does end-to-end test.
  • It launches multiple instance of such testers.
  • It parses the stdout and stderr from each execution of E2E test.
  • It should monitor the execution status of loadtest, such as how many launched, executing, finished.
  • It should cancel all remaining tester from ctrl-C.

Basically it is a stander master-worker structure. This section records down the problems I met and Googled.

3.1. How to get project’s root from .git?

import "os/exec"

func getCurrentProjectRoot() (string, error) {
  cmdOut, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
  if err != nil {
    return "", errors.New("you are not in a project")
  }

  return strings.TrimSpace(string(cmdOut)), nil
}

3.2. How to execute external program?

3.2.1. Prepare the cmd to execute

import "os/exec"

func PrepareTester(workerVersion, jsonFile string) *exec.Cmd {
  rootPath, err := getCurrentProjectRoot()
  if err != nil {
    panic(err)
  }

  // cmd used on command-line: node index.js -j input_clouddash_dev.json -w Worker.3100 -m C -p zeus
  app := "node"
  arg0 := "index.js"
    ...

  cmd := exec.Command(app, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
  cmd.Dir = rootPath + "/code"

  return cmd
}

3.2.2. Execute exec result in real-time

In this work, the tester’s execution will take for a while, and it is neccessary to view the complete output on the terminal while saving its output into files.

  • We need to save output from stderr into a file.
  • We need to combine stdout and stderr to save it together into a file while displaying it on console.
  • Because there are two data streams: stderr, and stderr + stdout. We use two goroutine to handle them.
  • We start exec, wait it’s done.

Code does write stderr, stdout into files

cmd := cmd.PrepareTester(workerVersion, jsonFile)
cmdStdout := make(chan string, 10)

outf, _ := os.Create(filepath.Join(logFolder, fmt.Sprintf("%v-out.txt", id)))
errf, _ := os.Create(filepath.Join(logFolder, fmt.Sprintf("%v-err.txt", id)))
defer outf.Close()
defer errf.Close()

var wg sync.WaitGroup
wg.Add(1)

// write exec's stdout into stdout file 
go func() {
    readAndWriteToFiles(stdoutIn, cmdStdout, []*os.File{outf})
    wg.Done()
}()
wg.Add(1)

// write exec's stderr into stderr file as well as into stdout file.
go func() {
    readAndWriteToFiles(stderrIn, cmdStdout, []*os.File{outf, errf})
    wg.Done()
}()
wg.Wait()
cmd.Wait()
  • We also write both stderr and stdout into a channel, then we could read from that channel and do further process for exec’s output. For example, print out on console or save them for further process.
  • A helper function: read from io.Reader and write them to os.File and to chan string

    func readAndWriteToFiles(r io.Reader, ch chan<- string, fs []*os.File) {
      buf := make([]byte, 512)
      for {
        n, err := r.Read(buf[:])
        if n > 0 {
          d := buf[:n]
    
          for _, f := range fs {
    	f.Write(d)
          }
          ch <- string(d)
        }
        if err != nil {
          if err == io.EOF {
    	break
          }
        }
      }
    }
    

3.3. Process JSON

I would say the only downside of using comparing with Javascript is it is not very convenient to process JSON. In Golang, JSON object is represent as a combination of map[string]interfaces{} and []interface{}.

3.3.1. Encod JSON to bytes

  • For example, Golang’s net/http module needs the body of http to be bytes, so we need to encode JSON object into bytes.

    client := &http.Client{
        Timeout: time.Millisecond * 2500,
    }
    
    contract := map[string]interface{}{
        "workerInfo": map[string]string{"workerVersion": workerVersion},
        "userInfo":   map[string]string{"userId": userId},
        "clientInfo": map[string]string{"clientId": "-tester", "clientVersion": "1.3.4"},
    }
    body := map[string]interface{}{
        "Contract": contract,
    }
    reqBody, _ := json.Marshal(body)
    
    req, _ := http.NewRequest("POST", url+"/init", bytes.NewBuffer(reqBody))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("x-ams-apiKey", apiKey)
    
    resp, err := client.Do(req)
    if err != nil {
        log.Println(err)
        return sessionResult, false
    }
    
    defer resp.Body.Close()
    
    • First json.Marshal, then bytes.NewBuffer.

3.3.2. Decode byte to JSON object

  • For example, the response we got from http post contain the

    type InitSessionResult struct {
      SessionId string
      Expiry         int64 // in Millisecond
      ExpiryTime     time.Time
    }
    
    resBody, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println("InitSessionId response Status:", resp.Status)
    
    var sessionResult InitSessionResult
    err = json.Unmarshal(resBody, &sessionResult)
    
    if err == nil {
        sessionResult.ExpiryTime = time.Unix(sessionResult.Expiry/1000, 0)
    }
    
    • If we know the JSON response structure from which we will parse, it becomes easy: we just json.Unmarshal it. And it will fill our struct autmatically based some rules (see encoding/json for detail).
  • For cases where we don’t know the JSON response structure. (TODO)

3.3.3. Reference

3.4. How to do something related with time

3.4.1. How to convert from Unix timestamp to time.Time

// Here, the timestamp I got is Millisecond
time.Unix(*r.Timestamp/1000, 0)
  • func Unix(sec int64, nsec int64) Time

3.4.2. How to convert from time.Time to Unix timestamp

  • func (t Time) Unix() int64, the number of second
  • func (Time) UnixMilli, the number of milliseconds elapsed since January 1, 1970 UTC
  • func (Time) UnixNano

3.4.3. How to format time

func fromTimeToStr(t time.Time, format string, asUTC bool) string {
  if asUTC {
    return fmt.Sprintf("%v", t.In(time.UTC).Format(format))
  } else {
    return fmt.Sprintf("%v", t.In(time.Local).Format(format))
  }
}

3.4.4. How to parse a string to time.Time

  • func Parse(layout, value string) (Time, error)

    startAt, err := time.Parse("2006-01-02 15:04", startStr)
    if err != nil {
        log.Fatal("time format is not correct, exit...")
    }
    
  • func ParseInLocation(layout, value string, loc *Location) (Time, error)

3.4.5. How to format time

func formatTime(t time.Time) string {
    // setUTC is a global variable control whether we want is a time relative to UTC
  return fromTimeToStr(t, "2006-01-02 15:04:05.99-0700", setUTC)
}

func fromTimeToStr(t time.Time, format string, asUTC bool) string {
  if asUTC {
    return fmt.Sprintf("%v", t.In(time.UTC).Format(format))
  } else {
    return fmt.Sprintf("%v", t.In(time.Local).Format(format))
  }
}

3.4.6. How to set duration

  1. Set time duration for specific number, such as 30 seconds

    var testerNoResponseLimit time.Duration = 30 * time.Second
    
  2. Set time duration between one timestamp

    begin := time.Now()
    // ...do something
    time.Since(begin)
    
  3. Set time duration between two timestamp using Sub

    // use latest time - a timestamp set before
    time.Now().Sub(startAt)
    
  4. From a time at one side of duration to get another time.

    fiveMinsAgo := time.Now().Add(-5 * time.Minute)
    
  5. How to multiple duration by integer?
    Usually we got a variable as a number and we want to multiple it by unit of duration, such as seconds or mins. For a variable as number, we need to convert it to time.Duration explicitly.

    start := time.Now().Add(-time.Duration(parsedInteger) * time.Minute)
    

3.4.7. How to postpone execution

// postpone is time.Duration
time.Sleep(postpone)

3.4.8. How to repeated do something with time.Duration

// First setup a ticker
updateTicker := time.NewTicker(5 * time.Second)

go func() {
    // stop it using defer
    defer updateTicker.Stop()
    for {
	select{
	case<-updateTicker.C:
	    //... doSomething 
	case<-done:
	    // cancel the it by close done channel
	    return
	}
    }
}()

3.5. How to loop each attribute of struct

For example, we want to convert a struct into []string such that it could be write into CSV files (encoding/csv need data to be []string)

type LoadtestRecord struct {
  Id               int
  Timestamp        time.Time
  TesterName       string
  Guid             string
  Linetype         string
  OpenPartDuration time.Duration
  CompleteDuration time.Duration
  Text             string
  SinceBegin       time.Duration
}

func (l LoadtestRecord) toStrings() []string {
  v := reflect.ValueOf(l)
  typeOfS := v.Type()

  result := []string{}
  for i := 0; i < v.NumField(); i++ {

    k := typeOfS.Field(i).Name
    v := v.Field(i).Interface()

    if k == "Timestamp" {
      result = append(result, formatTime(v.(time.Time)))
    } else if k == "OpenPartDuration" {
      result = append(result, fmt.Sprintf("%v", int64(v.(time.Duration)/time.Millisecond)))
    } else if k == "CompleteDuration" {
      result = append(result, fmt.Sprintf("%v", int64(v.(time.Duration)/time.Millisecond)))
    } else if k == "Id" || k == "SinceBegin" {
      continue
    } else {
      str := v.(string)
      result = append(result, removeLastStr(strings.TrimSpace(str), "\n"))
    }
  }

  return result
}
  • Here, we define a method on the type LoadtestRecord such it we could convert subset of its attributes into []string to save it into CSV file.

3.6. How to build command-line tools

So easy in Golang

var n, p, t int
var minNumOfEC2, maxNumOfEC2 int
var stackEnv, workerVersion, jsonFile string
// var setUTC bool
flag.IntVar(&n, "n", 1, "number of testers to launch per path")
flag.IntVar(&p, "p", 1, "how many patch")
flag.IntVar(&t, "t", 5, "time interval(seconds) between patch")
flag.StringVar(&stackEnv, "e", "dev", "stack stackEnv, for example: dev, preprod...etc")
flag.StringVar(&workerVersion, "w", "Worker.3100", "the worker version used for tester, by default it will use Worker.3100")
flag.StringVar(&jsonFile, "j", "input_clouddash_dev.json", "the json file contains the APIs used by tester, by default it will use input_clouddash_dev.json")
flag.BoolVar(&setUTC, "utc", false, "time displayed in log will be using UTC by default, -utc=true to set timestamp in UTC")
flag.Parse()

3.7. How to write unit test

  • ref: Structuring Tests in Go
  • ref: 5 simple tips and tricks for writing unit tests in #golang
  • ref: Interface in Go
  • ref: Mocking Out the AWS SDK for Go for Unit Testing
  • Summary
    1. How to structure go test
      1. Put test code in the same package as target code.
      2. Naming convention: if target code is main.go, then test code is main_test.go
    2. How to use interface to mock dependencies during test.
      1. Ensure dependencies are passed as parameters of function(dependency injection makes function testable).
      2. Identify the methods of dependency.
      3. Create interface in which those dependent methods are defined.
      4. Re-define function parameter as interface instead of concrate type.
      5. In test code
        • Define Mocking type
        • Make Mocking type implement the interface (those dependent methods).
        • The caller declares the Mocking type and pass it into target testing function.
    3. What is the pattern when mock AWS SDK in go (in a function it calls methods from a Go SDK’s Client)
      1. Make testing function injectable: two approaches

        • One is pass Client directly as parameter of the function.
        • Another is define a struct which contains that Client. Then, define function as method of such struct.

        In anyway, we don’t use concrate Client type, we use AWS’s interface type.

      2. Here, the steps of identify and create our own interface to hold those methods are ignored. Because AWS already defines those interfaces, we just need to import them and use.
      3. In test code
        • Define Mocking type
        • Make Mocking type implement the interface (those dependent methods).
        • Define testing cases as slice in which it contains different cases for input and expecting output
        • Use for ... range to loop those cases, in each of them:
          • Initialize Mocking type with different input
          • Pass initialized Mocking type as parameter of testing function or as attribute of struct.
          • During the testing, the testing function is got executed which calls those dependent methods
          • That is OK, because Mocking type implements those method from interface.

4. Understand Reader and Writer interface

In Golang, io.Reader and Writer are interfaces, all methods related with I/O are just streams of bytes getting passed around.

4.1. Understand interface

In Golang standard library, a lot of components are interfaces. For example

type Example struct {

}
func (e *Example) Write(p byte[]) (n int, err error) {

}
func (e *Example) Read(p byte[]) (n int, err error) {

}
  • A interface will group none, or multiple functions.
  • A type implements those functions as methods satisfies that interface. Example implements Read/Write interface.
  • If we use type conversion, Example could be type of Reader && Writer.

4.2. Io.Reader

For a type of function as a reader, it must implement method Read(p []byte) from interface io.Reader.

type Reader interface {
    Read(p []byte) (n int, err error)
}
  • Read() will read up to len(p) into p, when possible.
  • After a Read() call, n may be less then len(p).
  • Upon error, Read() may still return n bytes in buffer p. For instance, when reading from a TCP socket that is abruptly closed. You may choose to keep the bytes in p or retry.

4.3. Io.Writer

4.4. Use pipe to switch from Writer to Reader

Sometimes a function’s parameters only accept Writer, but we want a Reader. The solution is io.Pipe().

4.5. An implementation of io.Reader which filters out non-alphabetic characters from its stream.

type alphaReader struct {
  src string
  cur int
}

func newAlphaReader(src string) *alphaReader {
  return &alphaReader{src: src}
}

func alpha(r byte) byte {
  if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
    return r
  }
  return 0
}

func (a *alphaReader) Read(p []byte) (int, error) {
  if a.cur >= len(a.src) {
    return 0, io.EOF
  }

  x := len(a.src) - a.cur
  n, bound := 0, 0
  if x >= len(p) {
    bound = len(p)
  } else if x <= len(p) {
    bound = x
  }

  buf := make([]byte, bound)
  for n < bound {
    if char := alpha(a.src[a.cur]); char != 0 {
      buf[n] = char
    }
    n++
    a.cur++
  }
  copy(p, buf)
  return n, nil
}

func main() {
  reader := newAlphaReader("Hello! It's 9am, where is the sun?")
  p := make([]byte, 4)
  for {
    n, err := reader.Read(p)
    if err == io.EOF {
      break
    }
    fmt.Print(string(p[:n]))
  }
  fmt.Println()
}
  • Notice the display of []byte as string in different terminal shows different result:

    fmt.Println(string([]byte{111, 73})) // on terminal, it is displayed as oI
    fmt.Println(string([]byte{111, 0, 0, 0, 0, 73})) // on terminal it is displayed as oI, but for some other terminal, it is displayed as oI
    

5. Build code into executable binary file

While the go run command is a useful shortcut for compiling and running a program when you’re making frequent changes, it doesn’t generate a binary executable. (See: Compile and install the application)

  1. ref: How To Build Go Executables for Multiple Platforms on Ubuntu 16.04
  2. create script

    #!/usr/bin/env bash
    package=$1
    if [[ -z "$package" ]]; then
        echo "usage: $0 <package-name>"
        exit 1
    fi
    package_split=(${package//\// })
    package_name=${package_split[-1]}
    
    platforms=("windows/amd64" "windows/386" "linux/amd64")
    
    for platform in "${platforms[@]}"
    do
        platform_split=(${platform//\// })
        GOOS=${platform_split[0]}
        GOARCH=${platform_split[1]}
        output_name=$package_name'-'$GOOS'-'$GOARCH
        if [ $GOOS = "windows" ]; then
            output_name+='.exe'
        fi
    
        env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package
        if [ $? -ne 0 ]; then
            echo 'An error has occurred! Aborting the script execution...'
            exit 1
        fi
    done
    
  3. Use the above script to build out loadtest file is just

    ./go-executable-build.sh main/mLoadTest.go
    

    It will generate the following files for the platforms we defined

    mLoadTest.go-linux-amd64
    mLoadTest.go-windows-386.exe
    mLoadTest.go-windows-amd64.exe
    

6. Concurrency in Go

I choose go to do this loadtest job because of this. There are several common patterns.

6.1. How to receive signal from event, such as Ctr-C

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

done := make(chan interface{})

go func() {
    select {
    case <-time.After(globalTimeoutLimit):
	fmt.Println("timeout globablly, cancel all")
	close(done)
    case <-interrupt:
	// use interrupt to gracefully cancel all tester jobs
	fmt.Println("interrupt, cancel all")
	close(done)
    }
}()

6.2. From master spawn workers and cancel them when needed

func observingEC2Instances(done <-chan interface{}, stackEnv string, minNumOfEC2, maxNumOfEC2 *int) {
  // observing the changes of EC2
  observeEC2Ticker := time.NewTicker(10 * time.Second)
  fname := filepath.Join(logFolder, fmt.Sprintf("number-of-EC2-instance.txt"))
  f, _ := os.Create(fname)

  defer observeEC2Ticker.Stop()
  defer f.Close()
  fmt.Println("Observing number of EC2 instances")

  num := aws.GetNumOfEC2(fmt.Sprintf("ecs-%v", stackEnv))
  f.Write([]byte(fmt.Sprintf("NumEC2s\tTime\n")))

  tStr := formatTime(time.Now())
  f.Write([]byte(fmt.Sprintf("%v\t%v\n", num, tStr)))

  *minNumOfEC2 = num
  *maxNumOfEC2 = num
  for {
    select {
    case <-done:
      return
    case <-observeEC2Ticker.C:
      num = aws.GetNumOfEC2(fmt.Sprintf("ecs-%v", stackEnv))
      tStr = formatTime(time.Now())
      f.Write([]byte(fmt.Sprintf("%v\t%v\n", num, tStr)))
      if num > *maxNumOfEC2 {
	*maxNumOfEC2 = num
      }
      if num < *minNumOfEC2 {
	*minNumOfEC2 = num
      }
    }
  }
}
  • We usually pass done := make(chan interface{}) into each worker’s goroutine.
  • Then in it, use for...select pattern to do cleanup operation once we receive messages from done.
  • Here, we could observe the number of EC2 instance with time interval and cancel it if we want in a goroutine.

7. Summary

  • The biggest thought about Golang is its standard library can do so many things. A lot of them need third party library in Javascript.
  • Its concurrency pattern is great, for a structure of tree shape concurrency, it is managable. But it soon becomes complicated if we want to form a net of goroutines. Erlang/Elixir is much better to compose complicated newtork of concurrency, especially if we want execute them across on different host.