Let's write config for your Golang web app on right way — YAML 👌
Tags: go, beginners, tutorial, productivity
Published: 2020-02-13 20:48:49

## Introduction

Hello, everyone! 😉 Today, I would like to discuss about configuration for web application on Golang. And not just talk, but show a simple example of a **YAML-based** configuration for Go web app.

> It will be quite a short article, because I don't want to _obstruct_ your information field on purpose! ☝️

### 📝 Table of contents

- [Project structure](#ch-1)
- [What's YAML?](#ch-2)
- [Closer look at config file](#ch-3)
- [Copy-paste repository](#ch-4)
- [Let's code!](#ch-5)

<a name="ch-1"></a>
## Project structure

As you know, I always use [Go Modules](https://blog.golang.org/using-go-modules) for my projects (_even for the smallest_). This demo project is **no** exception.

```console
$ tree .
.
├── Makefile
├── config.yml
├── go.mod
├── go.sum
└── main.go
```

- `Makefile` — put all frequently used commands in there.
- `config.yml` — config on YAML format.
- `main.go` — main file with web app code.

<a name="ch-2"></a>
## What's YAML?

Follow Wiki page:

> YAML (a recursive acronym for "_YAML Ain't Markup Language_") is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted.

And it's truth! YAML is awesome format to write **small** or **complex** configs with understandable structure. Many services and tools, like _Docker compose_ and _Kubernetes_, uses YAML as main format to describe its configurations.

### Golang and YAML

There are many Go packages to work with YAML files. I mostly use [go-yaml/yaml](https://github.com/go-yaml/yaml) (_version 2_), because it's stable and have nice API.

> But you can use any other package you're used to. The essence of it will not change! 😎

![config file](https://dev-to-uploads.s3.amazonaws.com/i/nijultb38be3ding5be7.jpg)

<a name="ch-3"></a>
## Closer look at config file 👀

Let's take a look our (_dummy_) config file for web app:

```yaml
# config.yml

server:
  host: 127.0.0.1
  port: 8080
  timeout:
    server: 30
    read: 15
    write: 10
    idle: 5
```

`server` — it's root layer of config.
`host`, `port` and `timeout` — options, which we will use later.

<a name="ch-4"></a>
## ✅ Copy-paste repository

_Especially for you_, I created repository with full code example on my GitHub:

{% github koddr/example-go-config-yaml no-readme %}

Just `git clone` and read instructions from `README`.

<a name="ch-5"></a>
## Let's code!

> I built web application's code in an **intuitive form**. If something is still unclear, please ask questions in comments! 💻

**EDIT @ 19 Feb 2020:** Many thanks to [Jordan Gregory](https://dev.to/j4ng5y) (_aka j4ng5y_) for huge fixes for my earlier code example. It's really awesome work and I'd like to recommend to follow these new example for all newbie (and not so) gophers! 👍

```go
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"gopkg.in/yaml.v2"
)

// Config struct for webapp config
type Config struct {
	Server struct {
		// Host is the local machine IP Address to bind the HTTP Server to
		Host string `yaml:"host"`

		// Port is the local machine TCP Port to bind the HTTP Server to
		Port    string `yaml:"port"`
		Timeout struct {
			// Server is the general server timeout to use
			// for graceful shutdowns
			Server time.Duration `yaml:"server"`

			// Write is the amount of time to wait until an HTTP server
			// write opperation is cancelled
			Write time.Duration `yaml:"write"`

			// Read is the amount of time to wait until an HTTP server
			// read operation is cancelled
			Read time.Duration `yaml:"read"`

			// Read is the amount of time to wait
			// until an IDLE HTTP session is closed
			Idle time.Duration `yaml:"idle"`
		} `yaml:"timeout"`
	} `yaml:"server"`
}

// NewConfig returns a new decoded Config struct
func NewConfig(configPath string) (*Config, error) {
	// Create config structure
	config := &Config{}

	// Open config file
	file, err := os.Open(configPath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	// Init new YAML decode
	d := yaml.NewDecoder(file)

	// Start YAML decoding from file
	if err := d.Decode(&config); err != nil {
		return nil, err
	}

	return config, nil
}

// ValidateConfigPath just makes sure, that the path provided is a file,
// that can be read
func ValidateConfigPath(path string) error {
	s, err := os.Stat(path)
	if err != nil {
		return err
	}
	if s.IsDir() {
		return fmt.Errorf("'%s' is a directory, not a normal file", path)
	}
	return nil
}

// ParseFlags will create and parse the CLI flags
// and return the path to be used elsewhere
func ParseFlags() (string, error) {
	// String that contains the configured configuration path
	var configPath string

	// Set up a CLI flag called "-config" to allow users
	// to supply the configuration file
	flag.StringVar(&configPath, "config", "./config.yml", "path to config file")

	// Actually parse the flags
	flag.Parse()

	// Validate the path first
	if err := ValidateConfigPath(configPath); err != nil {
		return "", err
	}

	// Return the configuration path
	return configPath, nil
}

// NewRouter generates the router used in the HTTP Server
func NewRouter() *http.ServeMux {
	// Create router and define routes and return that router
	router := http.NewServeMux()

	router.HandleFunc("/welcome", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
	})

	return router
}

// Run will run the HTTP Server
func (config Config) Run() {
	// Set up a channel to listen to for interrupt signals
	var runChan = make(chan os.Signal, 1)

	// Set up a context to allow for graceful server shutdowns in the event
	// of an OS interrupt (defers the cancel just in case)
	ctx, cancel := context.WithTimeout(
		context.Background(),
		config.Server.Timeout.Server,
	)
	defer cancel()

	// Define server options
	server := &http.Server{
		Addr:         config.Server.Host + ":" + config.Server.Port,
		Handler:      NewRouter(),
		ReadTimeout:  config.Server.Timeout.Read * time.Second,
		WriteTimeout: config.Server.Timeout.Write * time.Second,
		IdleTimeout:  config.Server.Timeout.Idle * time.Second,
	}

	// Handle ctrl+c/ctrl+x interrupt
	signal.Notify(runChan, os.Interrupt, syscall.SIGTSTP)

	// Alert the user that the server is starting
	log.Printf("Server is starting on %s\n", server.Addr)

	// Run the server on a new goroutine
	go func() {
		if err := server.ListenAndServe(); err != nil {
			if err == http.ErrServerClosed {
				// Normal interrupt operation, ignore
			} else {
				log.Fatalf("Server failed to start due to err: %v", err)
			}
		}
	}()

	// Block on this channel listeninf for those previously defined syscalls assign
	// to variable so we can let the user know why the server is shutting down
	interrupt := <-runChan

	// If we get one of the pre-prescribed syscalls, gracefully terminate the server
	// while alerting the user
	log.Printf("Server is shutting down due to %+v\n", interrupt)
	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("Server was unable to gracefully shutdown due to err: %+v", err)
	}
}

// Func main should be as small as possible and do as little as possible by convention
func main() {
	// Generate our config based on the config supplied
	// by the user in the flags
	cfgPath, err := ParseFlags()
	if err != nil {
		log.Fatal(err)
	}
	cfg, err := NewConfig(cfgPath)
	if err != nil {
		log.Fatal(err)
	}

	// Run the server
	cfg.Run()
}
```

OK! Run it:

```console
$ go run ./...

# OR with different config file

$ go run ./... -config ./static/my-other-config.yml
```

And _finally_, go to `http://127.0.0.1:8080/welcome` and see message:

```console
Hello, you've requested: /welcome
```

All done! 🎉

## Photo by

[Title] Fabian Grohs https://unsplash.com/photos/dC6Pb2JdAqs
[1] Alfred Rowe https://unsplash.com/photos/FVWTUOIUZd8

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv