Golang by Example

Logo

Libraries

We shouldn't reinvent the wheel. Go has a rich standard library and a vibrant ecosystem of third-party libraries. Before writing your own code, check if there is already a library that does what you need. This can save you time and effort and help you avoid common pitfalls.

Standard Library

The Go standard library is one of the most powerful features of the language. It provides a wide range of packages for common tasks, such as (not exhaustively):

time

The time package provides functionality for measuring and displaying time. It includes support for time zones, durations, and formatting.

strings

The strings package provides functions for manipulating UTF-8 encoded strings. It includes functions for searching, replacing, and splitting strings, as well as for changing the case of characters.

strconv

The strconv package provides functions for converting between strings and other basic data types, such as integers and floating-point numbers. It includes functions for parsing and formatting numbers, as well as for converting between different number bases.

os

The os package provides a platform-independent interface to operating system functionality, such as file and directory manipulation, environment variables, and process management.

log

The log package provides a simple logging interface for writing log messages to standard output or to a file. It includes support for different log levels, such as info, warning, and error.

errors

The errors package provides functions for creating and manipulating error values. It includes support for wrapping errors with additional context and for checking the type of an error.

math

The math package provides basic constants and mathematical functions. It includes support for trigonometric functions, logarithms, and other common mathematical operations.

Third-Party Libraries

zerolog

zerolog is a fast and simple logger for Go. It is designed to be easy to use and provides a simple API for logging messages at different levels (info, debug, error, etc.). It also supports structured logging, which allows you to include additional context with your log messages. The plus side compared to standard log is that it is much faster, has a smaller memory footprint, and is more flexible.


    package main

    import (
        "github.com/rs/zerolog"
        "github.com/rs/zerolog/log"
    )

    func main() {
        // Set the global log level to info
        zerolog.SetGlobalLevel(zerolog.InfoLevel)
        zerolog.TimeFieldFormat = zerolog.TimeFormatUnix

        // Create a new logger
        logger := log.New()

        // Log a simple message
        logger.Info().Msg("This is an info message.")

        // Log a message with additional context
        logger.Info().Str("user", "john").Msg("User logged in.")

        // Log a warning message
        logger.Warn().Msg("This is a warning message.")

        // Log an error message
        logger.Error().Err(errors.New("something went wrong")).Msg("An error occurred.")
    }

sqlx

sqlx is an extension to the standard database/sql library. It provides additional functionality for working with SQL databases, including support for struct scanning, named parameters, and more.


    package main

    import (
        "database/sql"
        "fmt"
        "log"
        
        _ "github.com/lib/pq"
        "github.com/jmoiron/sqlx"
    )

    var schema = `
    CREATE TABLE person (
        first_name text,
        last_name text,
        email text
    );

    CREATE TABLE place (
        country text,
        city text NULL,
        telcode integer
    )`

    type Person struct {
        FirstName string `db:"first_name"`
        LastName  string `db:"last_name"`
        Email     string
    }

    type Place struct {
        Country string
        City    sql.NullString
        TelCode int
    }

    func main() {
        // this Pings the database trying to connect
        // use sqlx.Open() for sql.Open() semantics
        db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable")
        if err != nil {
            log.Fatalln(err)
        }

        // exec the schema or fail; multi-statement Exec behavior varies between
        // database drivers;  pq will exec them all, sqlite3 won't, ymmv
        db.MustExec(schema)
        
        tx := db.MustBegin()
        tx.MustExec("INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "Jason", "Moiron", "[email protected]")
        tx.MustExec("INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "John", "Doe", "[email protected]")
        tx.MustExec("INSERT INTO place (country, city, telcode) VALUES ($1, $2, $3)", "United States", "New York", "1")
        tx.MustExec("INSERT INTO place (country, telcode) VALUES ($1, $2)", "Hong Kong", "852")
        tx.MustExec("INSERT INTO place (country, telcode) VALUES ($1, $2)", "Singapore", "65")
        // Named queries can use structs, so if you have an existing struct (i.e. person := &Person{}) that you have populated, you can pass it in as &person
        tx.NamedExec("INSERT INTO person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)", &Person{"Jane", "Citizen", "[email protected]"})
        tx.Commit()

        // Query the database, storing results in a []Person (wrapped in []interface{})
        people := []Person{}
        db.Select(&people, "SELECT * FROM person ORDER BY first_name ASC")
        jason, john := people[0], people[1]

        fmt.Printf("%#v\n%#v", jason, john)
        // Person{FirstName:"Jason", LastName:"Moiron", Email:"[email protected]"}
        // Person{FirstName:"John", LastName:"Doe", Email:"[email protected]"}

        // You can also get a single result, a la QueryRow
        jason = Person{}
        err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason")
        fmt.Printf("%#v\n", jason)
        // Person{FirstName:"Jason", LastName:"Moiron", Email:"[email protected]"}

        // if you have null fields and use SELECT *, you must use sql.Null* in your struct
        places := []Place{}
        err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC")
        if err != nil {
            fmt.Println(err)
            return
        }
        usa, singsing, honkers := places[0], places[1], places[2]
        
        fmt.Printf("%#v\n%#v\n%#v\n", usa, singsing, honkers)
        // Place{Country:"United States", City:sql.NullString{String:"New York", Valid:true}, TelCode:1}
        // Place{Country:"Singapore", City:sql.NullString{String:"", Valid:false}, TelCode:65}
        // Place{Country:"Hong Kong", City:sql.NullString{String:"", Valid:false}, TelCode:852}

        // Loop through rows using only one struct
        place := Place{}
        rows, err := db.Queryx("SELECT * FROM place")
        for rows.Next() {
            err := rows.StructScan(&place)
            if err != nil {
                log.Fatalln(err)
            } 
            fmt.Printf("%#v\n", place)
        }
        // Place{Country:"United States", City:sql.NullString{String:"New York", Valid:true}, TelCode:1}
        // Place{Country:"Hong Kong", City:sql.NullString{String:"", Valid:false}, TelCode:852}
        // Place{Country:"Singapore", City:sql.NullString{String:"", Valid:false}, TelCode:65}

        // Named queries, using `:name` as the bindvar.  Automatic bindvar support
        // which takes into account the dbtype based on the driverName on sqlx.Open/Connect
        _, err = db.NamedExec(`INSERT INTO person (first_name,last_name,email) VALUES (:first,:last,:email)`, 
            map[string]interface{}{
                "first": "Bin",
                "last": "Smuth",
                "email": "[email protected]",
        })

        // Selects Mr. Smith from the database
        rows, err = db.NamedQuery(`SELECT * FROM person WHERE first_name=:fn`, map[string]interface{}{"fn": "Bin"})

        // Named queries can also use structs.  Their bind names follow the same rules
        // as the name -> db mapping, so struct fields are lowercased and the `db` tag
        // is taken into consideration.
        rows, err = db.NamedQuery(`SELECT * FROM person WHERE first_name=:first_name`, jason)
        
        
        // batch insert
        
        // batch insert with structs
        personStructs := []Person{
            {FirstName: "Ardie", LastName: "Savea", Email: "[email protected]"},
            {FirstName: "Sonny Bill", LastName: "Williams", Email: "[email protected]"},
            {FirstName: "Ngani", LastName: "Laumape", Email: "[email protected]"},
        }

        _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email)
            VALUES (:first_name, :last_name, :email)`, personStructs)

        // batch insert with maps
        personMaps := []map[string]interface{}{
            {"first_name": "Ardie", "last_name": "Savea", "email": "[email protected]"},
            {"first_name": "Sonny Bill", "last_name": "Williams", "email": "[email protected]"},
            {"first_name": "Ngani", "last_name": "Laumape", "email": "[email protected]"},
        }

        _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email)
            VALUES (:first_name, :last_name, :email)`, personMaps)
    }

gorm

gorm is a popular ORM library for Go. It provides a simple and powerful way to interact with databases using Go structs. GORM supports various databases, including PostgreSQL, MySQL, SQLite, and SQL Server.


    package main

    import (
      "gorm.io/gorm"
      "gorm.io/driver/sqlite"
    )

    type Product struct {
      gorm.Model
      Code  string
      Price uint
    }

    func main() {
      db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
      if err != nil {
        panic("failed to connect database")
      }

      // Migrate the schema
      db.AutoMigrate(&Product{})

      // Create
      db.Create(&Product{Code: "D42", Price: 100})

      // Read
      var product Product
      db.First(&product, 1) // find product with integer primary key
      db.First(&product, "code = ?", "D42") // find product with code D42

      // Update - update product's price to 200
      db.Model(&product).Update("Price", 200)
      // Update - update multiple fields
      db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
      db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

      // Delete - delete product
      db.Delete(&product, 1)
    }

chi

chi is a lightweight, idiomatic and composable router for building Go HTTP services. It is an alternative to the standard net/http mux and is designed to be simple and easy to use.


    package main

    import (
      "net/http"

      "github.com/go-chi/chi/v5"
      "github.com/go-chi/chi/v5/middleware"
    )

    func main() {
      r := chi.NewRouter()
      r.Use(middleware.Logger)
      r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("welcome"))
      })
      http.ListenAndServe(":3000", r)
    }

go-playground/validator

go-playground/validator is a popular validation library for Go. It provides a simple and powerful way to validate struct fields using tags. The library supports various validation rules and can be easily integrated into your applications.


  package main

  import (
      "errors"
      "fmt"

      "github.com/go-playground/validator/v10"
  )

  // User contains user information
  type User struct {
      FirstName      string     `validate:"required"`
      LastName       string     `validate:"required"`
      Age            uint8      `validate:"gte=0,lte=130"`
      Email          string     `validate:"required,email"`
      Gender         string     `validate:"oneof=male female prefer_not_to"`
      FavouriteColor string     `validate:"iscolor"`                // alias for 'hexcolor|rgb|rgba|hsl|hsla'
      Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
  }

  // Address houses a users address information
  type Address struct {
      Street string `validate:"required"`
      City   string `validate:"required"`
      Planet string `validate:"required"`
      Phone  string `validate:"required"`
  }

  // use a single instance of Validate, it caches struct info
  var validate *validator.Validate

  func main() {

      validate = validator.New(validator.WithRequiredStructEnabled())

      validateStruct()
      validateVariable()
  }

  func validateStruct() {
      address := &Address{
          Street: "Eavesdown Docks",
          Planet: "Persphone",
          Phone:  "none",
      }

      user := &User{
          FirstName:      "Badger",
          LastName:       "Smith",
          Age:            135,
          Gender:         "male",
          Email:          "[email protected]",
          FavouriteColor: "#000-",
          Addresses:      []*Address{address},
      }

      // returns nil or ValidationErrors ( []FieldError )
      err := validate.Struct(user)
      if err != nil {

          // this check is only needed when your code could produce
          // an invalid value for validation such as interface with nil
          // value most including myself do not usually have code like this.
          var invalidValidationError *validator.InvalidValidationError
          if errors.As(err, &invalidValidationError) {
              fmt.Println(err)
              return
          }

          var validateErrs validator.ValidationErrors
          if errors.As(err, &validateErrs) {
              for _, e := range validateErrs {
                  fmt.Println(e.Namespace())
                  fmt.Println(e.Field())
                  fmt.Println(e.StructNamespace())
                  fmt.Println(e.StructField())
                  fmt.Println(e.Tag())
                  fmt.Println(e.ActualTag())
                  fmt.Println(e.Kind())
                  fmt.Println(e.Type())
                  fmt.Println(e.Value())
                  fmt.Println(e.Param())
                  fmt.Println()
              }
          }

          // from here you can create your own error messages in whatever language you wish
          return
      }

      // save user to database
  }

  func validateVariable() {

      myEmail := "joeybloggs.gmail.com"

      errs := validate.Var(myEmail, "required,email")

      if errs != nil {
          fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "email" tag
          return
      }

      // email ok, move on
  }

viper

viper is a comprehensive configuration solution for Go applications. It supports various configuration file formats (JSON, TOML, YAML, HCL, and Java properties) and can read from environment variables, command-line flags, and remote key-value stores. Viper is particularly useful for managing application settings and can be easily integrated into your projects.


    package main

    import (
        "fmt"
        "log"

        "github.com/spf13/viper"
    )

    func main() {
        // Set the name of the config file (without extension)
        viper.SetConfigName("config")
        // Set the path to look for the config file
        viper.AddConfigPath(".")
        // Set the config type (json, toml, yaml, hcl, envfile)
        viper.SetConfigType("yaml")

        // Read the config file
        err := viper.ReadInConfig()
        if err != nil {
            log.Fatalf("Error reading config file: %s", err)
        }

        // Get a value from the config
        port := viper.GetInt("server.port")
        fmt.Printf("Server port: %d\n", port)

        // Get a nested value from the config
        dbHost := viper.GetString("database.host")
        fmt.Printf("Database host: %s\n", dbHost)

        // Get all keys and values from the config
        allKeys := viper.AllKeys()
        fmt.Println("All keys:", allKeys)

        // Get a value with a default fallback
        timeout := viper.GetInt("server.timeout")
        if timeout == 0 {
            timeout = 30 // default value
        }
        fmt.Printf("Server timeout: %d\n", timeout)
    }

lo

lo is a library that provides a set of functional programming utilities for Go. It includes functions for working with slices, maps, and other data structures, making it easier to perform common operations like filtering, mapping, and reducing.


    package main

    import (
        "fmt"
        "github.com/samber/lo"
    )

    func main() {
        // Create a slice of integers
        numbers := []int{1, 2, 3, 4, 5}

        // Map: square each number
        squares := lo.Map(numbers, func(n int) int {
            return n * n
        })
        fmt.Println("Squares:", squares)

        // Filter: keep only even numbers
        evens := lo.Filter(numbers, func(n int) bool {
            return n%2 == 0
        })
        fmt.Println("Evens:", evens)

        // Reduce: sum all numbers
        sum := lo.Reduce(numbers, func(acc int, n int) int {
            return acc + n
        }, 0)
        fmt.Println("Sum:", sum)
    }

Wrap up

In this article, we explored some of the most common libraries in Go, including the standard library and popular third-party libraries. We also discussed some best practices for using libraries in Go, such as avoiding reinventing the wheel and keeping your code simple and focused. By following these guidelines, you can write better Go code and take advantage of the rich ecosystem of libraries available to you.

💡 Quiz
Should we always use third-party libraries in our Go projects?

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: