Golang by Example

Logo

Benchmarking

In principal, benchmarking is still a part of testing. It is used to measure the performance of a function or a piece of code. In Go, you can use the testing package to create benchmarks. A benchmark function should start with the word Benchmark and take a pointer to testing.B as its only argument. Here's an example of a simple benchmark:


  // file: main_test.go
  package main

  import (
    "testing"
  )

  func BenchmarkPrimeNumbers(b *testing.B) {
    for i := 0; i < b.N; i++ {
      primeNumbers(100)
    }
  }

To run the benchmark, you can use the go test command with the -bench flag. This will run all benchmarks in your package:

  $ go test -bench=.
  goos: darwin
  goarch: arm64
  pkg: example.com/prime
  BenchmarkPrimeNumbers-8   	1000000	      0.00123 ns/op
  PASS
  ok  	example.com/prime	1.234s

The output shows the number of iterations and the time taken for each iteration. This information can help you identify performance bottlenecks in your code.

What the heck is b.N?

The b.N variable is a special variable provided by the testing package that represents the number of iterations to run in the benchmark. The Go testing framework automatically determines the value of b.N based on the performance of the code being benchmarked. It will run the benchmark multiple times, adjusting b.N to get a more accurate measurement of the performance.

The testing framework automatically sets b.N by running your benchmark with increasing values until achieving statistical significance. It starts with 1 and increases until the benchmark runs for about 1 second, which is why your code must execute the tested function exactly b.N times:


    // sample code borrowed from
    // https://betterstack.com/community/guides/scaling-go/golang-benchmarking/
    func BenchmarkSomething(b *testing.B) {
        // Optional setup code

        // Reset the timer if setup took significant time
        b.ResetTimer() 

        for i := 0; i < b.N; i++ {
            // Code you want to measure
        }
    }

If you testing a function that's having cleanup code, you must call b.ResetTimer() to reset the timer before running the benchmark loop. This is important because any setup code that runs before the loop can skew the results. By calling b.ResetTimer(), you ensure that only the code inside the loop is measured.


    // sample code borrowed from
    // https://betterstack.com/community/guides/scaling-go/golang-benchmarking/
    func BenchmarkComplexOperation(b *testing.B) {
        // Setup
        data := createLargeDataset()

        // Reset the timer to exclude setup time
        b.ResetTimer()

        for i := 0; i < b.N; i++ {
            processData(data)
        }

        // Optionally pause timer during cleanup
        b.StopTimer()
        cleanupResources()
    }


Load Testing with k6

Load testing is a type of performance testing that simulates a large number of users accessing your application simultaneously. This helps you identify how your application behaves under heavy load and whether it can handle the expected traffic.

This is really useful when you are building a web application or an API. One of the frameworks that you can use for load testing is k6. It is a modern load testing tool that allows you to write tests in JavaScript and run them in a distributed manner. You can use k6 to simulate thousands of virtual users and measure the performance of your application under load.

Assume you have simple API written with Gin and deployed to public using example.com domain:


  package main

  import "github.com/gin-gonic/gin"

  func main() {
    router := gin.Default()
    router.GET("/ping", func(c *gin.Context) {
      c.JSON(200, gin.H{
        "message": "pong",
      })
    })
    router.Run() // listen and serve on 0.0.0.0:8080
  }

You then write a k6 script to test this API endpoint:


  // file:: script.js
  import http from "k6/http";
  import { check, sleep } from "k6";

  // Test configuration
  export const options = {
    thresholds: {
      // Assert that 99% of requests finish within 3000ms.
      http_req_duration: ["p(99) < 3000"],
    },
    // Ramp the number of virtual users up and down
    stages: [
      { duration: "30s", target: 15 },
      { duration: "1m", target: 15 },
      { duration: "20s", target: 0 },
    ],
  };

  // Simulated user behavior
  export default function () {
    let res = http.get("https://example.com/ping");
    // Validate response status
    check(res, { "status was 200": (r) => r.status == 200 });
    sleep(1);
  }

You can run this script using the k6 command line tool:


  $ k6 run script.js

This will simulate 15 virtual users hitting the /ping endpoint of your API. Or you can run it in distributed mode using the k6 cloud command:


  $ k6 cloud script.js

More information about k6 can be found in the k6 documentation.

💡 Quiz
What is the purpose of the k6 tool?

Table of contents

Hello World - Begin with the classic Hello World program
Primitives - Learn about the basic data types in Go
Flow Control - Controlling the flow of your program
Struct - All about struct
Functions - Define and call functions
Methods and Interfaces - Methods and interfaces in Go
Error Handling - Handling errors idiomatically
Concurrency - Goroutines and channels
Anti Patterns - Anti patterns in Go
Libraries - Standard library and third-party libraries
Testing - Writing unit tests with `go test`
Benchmarking - Performance benchmarks
Containerization - Dockerize your Go app
What's Next? - Explore the next steps in your Golang journey


Share to: