User:Giuseppe Lavagetto/Golang for SRE
This page is supposed to be a draft of a simple set of general rules for writing applications in golang for SRE usage.
This is not:
- A style guide
- A resource on how to write software in go
Consider it an attempt at standardizing how we structure our projects.
In the reminder, we will refer to an example application, xkcli - a cli tool (and library!) to find XKCD comic strips about a topic
Application scaffolding using cobra
- Use cobra to generate your application:
$ go get github.com/spf13/cobra/cobra
$ cobra init --pkg-name github.com/lavagetto/xkcli --author "Giuseppe Lavagetto" --license Apache
This will generate a basic directory tree for you:
xkcli/ ├── cmd │ └── root.go ├── LICENSE └── main.go
You can now add subcommands, if you like. We want to support three commands:
- search [--lucky] [--idOnly] <search_term> to search the archive,
- refresh to refresh the archive,
- print [--url|--image|--text] <xkcd n> to print the desired part of the comic strip to stdout
We thus run cobra add <cmd>:
$ cobra add search
search created at /home/joe/Code/xkcli
...
This creates the subcommands:
xkcli/ ├── cmd │ ├── print.go │ ├── refresh.go │ ├── root.go │ └── search.go ├── LICENSE └── main.go
Now we'll need to add the appropriate flags to each command. Please refer to the cobra readme for an introduction.
Note that an application setup with cobra also comes with the awesome configuration manager called viper, that allows to read configurations in various formats, and from different sources (including etcd and consul), and to handle watching the configuration for changes.
Setup logging
Use Uber's zap which combines good structured and unstructured logs support, and performs very well. It should be conventional to use --debug as a command-line parameter for all commands, and make logging work in the same way.
So we will have to add a flag, and a simple helper function to our main.go
var debugLog bool
// Helper function
func setupLogging(isDebug bool) *zap.Logger {
var log *zap.Logger
var err error
if isDebug {
// level = debug, format human-readable
log, err = zap.NewDevelopment()
} else {
// level = info, json
log, err = zap.NewProduction()
}
if err != nil {
panic(err)
}
return log
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.xkcli.yaml)")
rootCmd.PersistentFlags().BoolVar(&debugLog, "debug", false, "Show more logs.")
}
And then call it from the subcommands' Run function:
var refreshCmd = &cobra.Command{
Use: "refresh",
Short: "Download the info about the missing strips.",
Long: `xkcli refresh will refresh the local database of strips,
fetching all the relative metadata.`,
Run: func(cmd *cobra.Command, args []string) {
logger := setupLogging(debugLog)
defer logger.Sync()
log := logger.Sugar()
log.Debug("Showing logs at debug level")
},
}
go mod configuration
Once we've added our flags to one command, it's time to add the go mod configuration and vendorize your dependencies:
xkcli$ go mod init github.com/lavagetto/xkcli
xkcli$ go mod vendor
go: finding module for package github.com/spf13/viper
...
xkcli$
Please note: having go.mod configured in your project is useful even if you decide to build the application using the dreaded Debian conventions in production.
In that case:
- Add the vendor directory to .gitignore
- Pin the packages versions to what is available in Debian
- Package all your missing dependencies (ahah)
Testing
The importance of testing early in your development process (or - just do TDD) cannot be stressed enough in go. If you don't do that, you might end up with mostly untestable code pretty quickly, in particular if you're not an experienced go programmer (or at least - it happened over and over to me!).
Use the builtin testing library, although the additions from testify, like assert, require and mock are nice, and something you're probably used to in other languages.
CI
TODO
Makefile
TODO