Learning Go by examples: part 8 - Automatically cross-compile & release your Go app
Tags: go, beginners, github, tutorial
Published: 2021-09-01 07:31:03

---
title: Learning Go by examples: part 8 - Automatically cross-compile & release your Go app
published: true
description: Serie of article in order to learn Golang language by concrete applications as example
series: Learning Go by examples
tags: Golang, beginners, GitHub, tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gkv1nf89weztfvqfx85v.png
---

In previous articles we created an [HTTP REST API server](https://dev.to/aurelievache/learning-go-by-examples-part-2-create-an-http-rest-api-server-in-go-1cdm), a [CLI](https://dev.to/aurelievache/learning-go-by-examples-part-3-create-a-cli-app-in-go-1h43), a [Bot for Discord](https://dev.to/aurelievache/learning-go-by-examples-part-4-create-a-bot-for-discord-in-go-43cf), a [gRPC app](https://dev.to/aurelievache/learning-go-by-examples-part-6-create-a-grpc-app-in-go-2ja3) ... and even a [game for Nintendo Game Boy Advance](https://dev.to/aurelievache/learning-go-by-examples-part-5-create-a-game-boy-advance-gba-game-in-go-5944). 

As we have already seen, Golang can be used for several of type of applications, but you know, I love creating CLI apps & tools, I love DevOps philosophy and I love Gophers. So in this article we'll create a little tool, with few Go best practices, and automatically generate cross-platform executable binaries and create GitHub releases through GitHub actions.

Ready?

## Initialization

First, create our new repository in GitHub (in order to share and open-source it).

For that, I logged in [GitHub website](https://github.com/), clicked on the **repositories** link, click on "New" green button and then I created a new repository called [gophersay](https://github.com/scraly/gophersay).

Now, in your local computer, git clone this new repository where you want:

```
$ git clone https://github.com/scraly/gophersay.git
$ cd gophersay
```
Now, we have to initialize Go modules (dependency management):

```
$ go mod init github.com/scraly/gophersay
go: creating new go.mod: module github.com/scraly/gophersay
```

This will create a `go.mod` file like this:

```
module github.com/scraly/gophersay

go 1.16
```

Before starting to code our Desktop application, as good practices, we will create a simple code organization.

Create the following folders organization:

```
.
├── bin
├── README.md
└── go.mod
```

That's it? Yes, the rest of our code organization will be created shortly ;-).

## Let's create our app

![Fox Gopher](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yrsfks6mf5eexmcqwex0.png)

What do we want?

Do you know "[cowsay](https://fr.wikipedia.org/wiki/Cowsay)" application? It's a simple app that display your text said by a cow.

![cowsay](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0gvdm2adkdv0uln4yy2y.png)

I love cow, but I love more Gophers, so we want to create a "cowsay" version with a Gopher instead of a cow.
The program will display a text written by the user and a Gopher, in ASCII, that say the text.

This app may seem useless but you will see, it will allow us to see some good practices ;-).

The first thing to do is to retrieve [Gophers in ASCII format](https://github.com/scraly/gophersay/tree/main/gophers) I created and put them in a new `gophers` folder. You need to have a folder like this:

```
├── gophers
│   ├── gopher0.txt
│   ├── gopher1.txt
│   ├── gopher2.txt
│   └── gopher3.txt
```

Create a `main.go` file.

First, we initialize the package, called `main`, and all dependencies/librairies we need to import:

```golang
package main

import (
	"fmt"
	"log"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"

	"embed"
)
```

Then, we initialize our variable:

```golang
// Hey, I want to embed "gophers" folder in the executable binary
// Use embed go 1.16 new feature (for embed gophers static files)
//go:embed gophers
var embedGopherFiles embed.FS
```

Thanks to awesome **embed** feature included since [Go 1.16](https://golang.org/doc/go1.16#library-embed) version, we tell that we embed `gophers/` folder in the variable `embedGopherFiles`.

Let's create the `main()` function:

```golang
func main() {

	// Display usage/help message
	if len(os.Args) == 1 || (len(os.Args) == 2 && os.Args[1] == "-h") || (len(os.Args) == 2 && os.Args[1] == "--help") {
		usage := "GopherSay is inspired by Cowsay program.\nGopherSay allow you to display a message said by a cute random Gopher.\n\nUsage:\n   gophersay MESSAGE\n\nExample:\n   gophersay hello Gopher lovers"

		fmt.Println(usage)
		return
	} else if len(os.Args) > 1 {

		message := strings.Join(os.Args[1:], " ")
		nbChar := len(message)

		line := " "
		for i := 0; i <= nbChar; i++ {
			line += "-"
		}

		fmt.Println(line)
		fmt.Println("< " + message + " >")
		fmt.Println(line)
		fmt.Println("        \\")
		fmt.Println("         \\")

		// Generate a random integer depending on get the number of ascii files
		rand.Seed(time.Now().UnixNano())
		randInt := rand.Intn(getNbOfGopherFiles() - 1)

		// Display random gopher ASCII embed files
		fileData, err := embedGopherFiles.ReadFile("gophers/gopher" + strconv.Itoa(randInt) + ".txt")
		if err != nil {
			log.Fatal("Error during read gopher ascii file", err)
		}
		fmt.Println(string(fileData))
	}
}
```

It's time to explain the `main()` function step by step.

First, if the user execute our app/tool without argument, or with "-h" option and or "--help" option, we display an usage/a help message:

```golang
	// Display usage/help message
	if len(os.Args) == 1 || (len(os.Args) == 2 && os.Args[1] == "-h") || (len(os.Args) == 2 && os.Args[1] == "--help") {
		usage := "GopherSay is inspired by Cowsay program.\nGopherSay allow you to display a message said by a cute random Gopher.\n\nUsage:\n   gophersay MESSAGE\n\nExample:\n   gophersay hello Gopher lovers"

		fmt.Println(usage)
		return
	}
```

Then, if user execute the `gophersay` app with an argument, a text, we define a variable message that retrieve all arguments and a variable with the number of characters of the message.
We print out this message surrounded by "bubble", like "cowsay" program does: 

```golang
else if len(os.Args) > 1 {

		message := strings.Join(os.Args[1:], " ")
		nbChar := len(message)

		line := " "
		for i := 0; i <= nbChar; i++ {
			line += "-"
		}

		fmt.Println(line)
		fmt.Println("< " + message + " >")
		fmt.Println(line)
		fmt.Println("        \\")
		fmt.Println("         \\")
```

After that, we generate a random integer between 0 and the number of gopher files we have -1 (4-1 at this time but I plan to add more):
```golang
		// Generate a random integer depending on get the number of ascii files
		rand.Seed(time.Now().UnixNano())
		randInt := rand.Intn(getNbOfGopherFiles() - 1)
```

Wait... why do we execute `rand.Seed()` function?

`rand.Intn(int)` returns a non negative pseudo-random number in [0,n]. It's cool, but … it produces a deterministic sequence of values!
So the solution, in order to have "real" random number is to use `rand.Seed()` in order to initialize the default source.

![crypto/rand](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a385vkldksuvwfqov1p2.png)

Let's go back to our code, we then want to display our cute ASCII Gopher:

```golang
		// Display random gopher ASCII embed files
		fileData, err := embedGopherFiles.ReadFile("gophers/gopher" + strconv.Itoa(randInt) + ".txt")
		if err != nil {
			log.Fatal("Error during read gopher ascii file", err)
		}
		fmt.Println(string(fileData))
```

And finally, create the function that return the number of ASCII Gopher image files:

```golang
func getNbOfGopherFiles() int {

	files, err := embedGopherFiles.ReadDir("gophers")
	if err != nil {
		log.Fatal("Error during reading gophers folder", err)
	}

	nbOfFiles := 0
	for _, _ = range files {
		nbOfFiles++
	}

	return nbOfFiles
}
```

### OK, but what is this famous embed??

If we package only our `main.go` file in an executable binary, when we will execute it, we'll have a problem because "gophers/" folder not exists in your computer.

Before Go version 1.16, there were several solutions but not as easy as the new [`embed` package](https://golang.org/doc/go1.16#library-embed).

> The new `embed` package provides access to files embedded in the program during compilation using the new `//go:embed directive`. 

The new `//go:embed` directive allow to embed static files and folders into application binary at compile-time without using an external tool.

In order to use it, first we have to declare a variable for the embedded content. In our example we embed our `gophers/` folder:

```golang
//go:embed gophers
var embedGopherFiles embed.FS
```

Then, we can read one file in this folder:

```golang
fileData, err := embedGopherFiles.ReadFile("gophers/gopher" + strconv.Itoa(randInt) + ".txt")
```

And retrieve a list of the files in this folder:
```golang
files, err := embedGopherFiles.ReadDir("gophers")
```

You can also embed a file directly:
```golang
//go:embed gophers/gopher0.txt
var myFile string
```

/!\ If the embed pattern names a folder, all files are embedded (recursively), except the files with names beginning with "." or "_". 
If you want to embed them, you need to specify the folder like this: `myfolder/*`

Awesome!

## Test it!

After code explanation, it's time to test our little app!

```
$ go run main.go
GopherSay is inspired by Cowsay program.
GopherSay allow you to display a message said by a cute random Gopher.

Usage:
   gophersay MESSAGE

Example:
   gophersay hello Gopher lovers
```

```bash
$ go run main.go --help
GopherSay is inspired by Cowsay program.
GopherSay allow you to display a message said by a cute random Gopher.

Usage:
   gophersay MESSAGE

Example:
   gophersay hello Gopher lovers
```

Cool, we have our usage message.

```bash
$ go run main.go Hello Gopher lovers!
```

![gophersay](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sv0q9ffp82llncebyzkc.png)
 
Yeah! Our text is said by one of our cute ASCII Gophers!

## Build it!

Your application is now ready, you can build it.

In previous articles, we used [Taskfile](https://taskfile.dev/#/) in order to automate our common tasks.

I created a `Taskfile.yaml` file:

```yaml
version: "3"
    
tasks:
    run: 
        desc: Run the app
        cmds:
        - GOFLAGS=-mod=mod go run main.go

    build:
        desc: Build the app
        cmds:
        - GOFLAGS=-mod=mod go build -o bin/gophersay main.go 

    clean:
        desc: Build the app
        cmds:
        - rm -rf dist 
```

So, now we can build our app:

```
$ task build
task: [build] GOFLAGS=-mod=mod go build -o bin/gophersay main.go
```

But... the executable binary is only for our environment, our OS, our platform, and I want to share my `gophersay` worldwide so it's time to find a way to easily cross-compile our app!

## GoReleaser

![GoReleaser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nb7x7cs422mu5d886xrt.png)

With [GoReleaser](https://goreleaser.com/), it's possible to:

* Cross-compile Go project
* Release to GitHub, GitLab and Gitea
* Create Docker images and manifests
* Create Linux packages and Homebrew taps
* ...

Oh, come on, it's exactly what do we want!

First, we need to [install GoReleaser](https://goreleaser.com/install/).

For MacOS:
```
$ brew install goreleaser/tap/goreleaser
```

Run the init command to create a `.goreleaser.yml` configuration file:

```
$ goreleaser init
   • Generating .goreleaser.yml file
   • config created; please edit accordingly to your needs file=.goreleaser.yml
```

Let's watch this new generated file:
```yaml
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
  hooks:
    # You may remove this if you don't use go modules.
    - go mod tidy
    # you may remove this if you don't need go generate
    - go generate ./...
builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
archives:
  - replacements:
      darwin: Darwin
      linux: Linux
      windows: Windows
      386: i386
      amd64: x86_64
checksum:
  name_template: 'checksums.txt'
snapshot:
  name_template: "{{ incpatch .Version }}-next"
changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
```

It's pretty cool. Because we don't use `go generate` in our application, we can remove the `- go generate ./...` line ;-).

Let's run a "local-only" release to generate a release of our Go app locally:
```
$ goreleaser release --snapshot --skip-publish --rm-dist
```

/!\ Don't forget to call this `goreleaser release` command with `--rm-dist` option or you can execute `task clean` target in order to remove **dist/** folder. If not, you'll have an issue because this folder need to be empty ^^.

If we take a look into new `dist/` generated folder, we can see that GoReleaser generate, for us, cross-platform executable binaries, and checksum:
```
dist
├── checksums.txt
├── config.yaml
├── gophersay_0.0.1-next_Darwin_arm64.tar.gz
├── gophersay_0.0.1-next_Darwin_x86_64.tar.gz
├── gophersay_0.0.1-next_Linux_arm64.tar.gz
├── gophersay_0.0.1-next_Linux_i386.tar.gz
├── gophersay_0.0.1-next_Linux_x86_64.tar.gz
├── gophersay_0.0.1-next_Windows_i386.tar.gz
├── gophersay_0.0.1-next_Windows_x86_64.tar.gz
├── gophersay_darwin_amd64
│   └── gophersay
├── gophersay_darwin_arm64
│   └── gophersay
├── gophersay_linux_386
│   └── gophersay
├── gophersay_linux_amd64
│   └── gophersay
├── gophersay_linux_arm64
│   └── gophersay
├── gophersay_windows_386
│   └── gophersay.exe
└── gophersay_windows_amd64
    └── gophersay.exe
```

It's perfect!

## When GitHub Action meet GoReleaser...

And what about generate a new release automatically?

Now, the next step is to execute GoReleaser, and publish a new associated Release in GitHub everytime we tag a new version of our application in our Git repository.

Let's do this!

Our Git repository is hosted in GitHub so we will use [GitHub Actions](https://docs.github.com/en/actions) for our CI (Continuous Integration) pipeline.

Create our workflow:

```
$ mkdir .github/workflows
$ cd .github/workflows
```

Inside it, create `goreleaser.yml` file with this content:

```yaml
name: goreleaser

on:
  push:
    tags:
      - '*'

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.16
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v2
        with:
          distribution: goreleaser
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

This workflow contains one job that we'll checkout the repository, package our app with GoReleaser and generate a GitHub release.

/!\ In order to release to GitHub, GoReleaser need a valid GitHub token with the `repo` scope. Fortunately, GitHub automatically [creates a GITHUB_TOKEN secret](https://docs.github.com/en/actions/reference/authentication-in-a-workflow#about-the-github_token-secret) to use in a workflow. 

After pushed your modification in the Git repository, now we can create a Git tag and push it:

```
$ git tag -a v1.0.0 -m "First release"

$ git push --tags
Énumération des objets: 1, fait.
Décompte des objets: 100% (1/1), fait.
Écriture des objets: 100% (1/1), 157 octets | 157.00 Kio/s, fait.
Total 1 (delta 0), réutilisés 0 (delta 0), réutilisés du pack 0
To https://github.com/scraly/gophersay.git
 * [new tag]         v1.0.0 -> v1.0.0
```

Let's go in our GitHub repository, and click on ["Actions"](https://github.com/scraly/gophersay/actions) tab in order to watch running, failed and successfull workflows:

![Gh actions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8gr8445ycbpm1ep9ij4d.png)
 
Perfect, our workflow successfully runned.

A new GitHub release have been automatically created:
![GH release](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xnzgk0uv3t5ogpfk9wg4.png)
 
![GH release details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k8600bo86sg2omork79k.png)
 
So now, each time I will update my app and create a Git tag and push it, automatically a new (GitHub) GH release will be created with cross-platform binaries :-).

## Thanks

If you like this article/tutorial and the cute `gophersay` app, don't hesitate to add a star on GitHub :-)

{% github scraly/gophersay %}

## Conclusion

As you have seen in this article and previous articles, it's possible to create multiple different applications in Go... and to automatize build and cross-compilation.
This time we didn't use Cobra, Viper and other frameworks because I wanted to show you we ca do a very small CLI without it :-).

All the code of our **GopherSay** app in Go is available in: https://github.com/scraly/gophersay

In the following articles we will create others kind/types of applications in Go.

Hope you'll like it.