Learning Go by examples: part 4 - Create a Bot for Discord in Go
Tags: go, beginners, discord, tutorial
Published: 2021-08-04 05:52:09

---
title: Learning Go by examples: part 4 - Create a Bot for Discord in Go
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, Discord, tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwgjfwyax0icenru9tg1.png
---

After 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) and our [first CLI (Command Line Interface) application in Go](https://dev.to/aurelievache/learning-go-by-examples-part-3-create-a-cli-app-in-go-1h43), what can we do now?
What if we create our own bot for Discord? ^^

I use Discord more and more everyday with my communities, so why not creating a bot that display our favorites Gophers to my friends? :-) 

## Discord prerequisites 

If you don't know Discord yet, it's like Slack, Teams or Mattermost, a good free alternative.

First, if you don't have a [Discord account](https://discord.com/), you need to create one ;-).


### Enable developer mode

In order to have sufficient rights, you need to enable developer mode in your Discord account. For that, in your Discord application, click on **User Settings** button:

![developer mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ki9s5z0vmum6ug0nric.png)
 
Then, click on **Advanced** and then enable **Developer Mode**:
![enable developer mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5f6md8fm6ry7mnfhggh9.png)
 
### Create a Discord application

Go to [https://discord.com/developers/applications/](https://discord.com/developers/applications/), then on **New Application** button, and then name your app and click on **Create** button: 

![create app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vsshua3gmicpno3zl4z7.png)
 
![don't be afraid](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g87uzrkc6xiqu1nx1txw.png)

You can now fill the description and add an icon to your freshly created app.

![bot description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eg3n7pj0a3aay834ohtp.png)

### Create a Bot

Click on **Bot** menu and then on **Add Bot** button.

![create a bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ncnar4pob1fscmsm320i.png)
 
This action allows to make visible your app/your bot on Discord.

The message "A wild bot has appeared!" should be appear in your interface:

![A wild bot has appeared](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/quimqm4v13mgn9oh2j0q.png)

Now go in **OAuth2** menu and click on **Copy** button in order to get `Client ID` information:

![OAuth2 menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ul3c47wi2nsaw5rg7ws9.png)
 
### Generate the Bot invite link

In order to link our Bot to one of our Discord server, we need to generate an invite link, with the **CLIENT ID** we copied:

```
https://discord.com/api/oauth2/authorize?client_id=<CLIENT-ID>&permissions=8&scope=bot
```

When we go to this URL, a connection window appears:

![Bot invite](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dn1hvsgs3gyv5blsul1a.png) 

Select the Discord server (that you created or have admin rights) and then click on **Continue** button and then confirm that you allow permissions to your Bot.

Your app is now authorized to do "things" on your Discord server :-).

You should now see your Bot (like others members) in your Discord server:

![A new Bot in our Discord server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3zsy2q1dznwfyldp2sm.png)

### Save the token

There is one last thing to do so that our Go application can connect to the Discord server: we need a **token**.

For that, go back in the Discord application developers website, then click on **Bot** menu and then click on **Copy** button in order to copy the token (and save it somewhere):

![save our token](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vu16isg8g337t05vmknf.png)
  
You will have to paste this token further in this article ;-).
 
## It's time to create our awesome Bot!

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

Ok, everything have been configured in Discord, you know I love concrete things, so it's time to play with Go now and code our simple (but so cute) Bot! :-D

### Initialization

We created our [Git repository](https://github.com/scraly/learning-go-by-examples) in the previous article, so now we just have to retrieve it locally:

```
$ git clone https://github.com/scraly/learning-go-by-examples.git
$ cd learning-go-by-examples
```

We will create a folder `go-gopher-bot-discord` for our CLI application and go into it:

```
$ mkdir go-gopher-bot-discord
$ cd go-gopher-bot-discord
```

Now, we have to initialize Go modules (dependency management):

```
$ go mod init github.com/scraly/learning-go-by-examples/go-gopher-bot-discord
go: creating new go.mod: module github.com/scraly/learning-go-by-examples/go-gopher-bot-discord
```

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

```
module github.com/scraly/learning-go-by-examples/go-gopher-bot-discord

go 1.16
```

Before to start our super CLI application, as good practices, we will create a simple code organization.

Create the following folders organization:

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

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

### What do we want?

Wait a minute, what do we want for our Bot?

We want a bot for Discord which will:
* Display a cute Gopher, when we will enter `!gopher` in our favorite Discord server(s)
* Display the list of available Gophers, when we will enter `!gophers`
* Display a random Gopher, when we will enter `!random`

### DiscordGo

In order to do that, we need a Client that interact with Go servers. Don't forget that a lot of useful and awesome libraries exists in Go, we don't have to reinvent the wheel, so we will use [DiscordGo](https://github.com/bwmarrin/discordgo) library.

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

DiscordGo is a Go package that provides low level bindings to the Discord chat client API. DiscordGo has nearly complete support for all of the Discord API endpoints, websocket interface, and voice interface. 
Cool!

Let's install **DiscordGo** in order to use it in our code:

```shell
$ go get github.com/bwmarrin/discordgo
```

Good, now we can create a **main.go** file and copy/paste the following code into it.

Go code is organized into packages. So, first, we initialize the package, called main, and all dependencies/librairies we need to import and use in our main file:

```golang
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"github.com/bwmarrin/discordgo"
)
```

Then, we init the `Token` variable which will be a needed parameter for our Bot app, the KuteGo API URL and the `init()` function that define we need the token:

```golang
// Variables used for command line parameters
var (
	Token string
)

const KuteGoAPIURL = "https://kutego-api-xxxxx-ew.a.run.app"

func init() {
	flag.StringVar(&Token, "t", "", "Bot Token")
	flag.Parse()
}
```

And the `main()` function that create a Discord session, register to MessageCreate events and run our Bot:

```golang
func main() {

	// Create a new Discord session using the provided bot token.
	dg, err := discordgo.New("Bot " + Token)
	if err != nil {
		fmt.Println("error creating Discord session,", err)
		return
	}

	// Register the messageCreate func as a callback for MessageCreate events.
	dg.AddHandler(messageCreate)

	// In this example, we only care about receiving message events.
	dg.Identify.Intents = discordgo.IntentsGuildMessages

	// Open a websocket connection to Discord and begin listening.
	err = dg.Open()
	if err != nil {
		fmt.Println("error opening connection,", err)
		return
	}

	// Wait here until CTRL-C or other term signal is received.
	fmt.Println("Bot is now running. Press CTRL-C to exit.")
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
	<-sc

	// Cleanly close down the Discord session.
	dg.Close()
}
```

Next, we need to define and implement a `Gopher` struct and the `messageCreate` function that will be called each time a message will be send in our Discord server.

```golang
type Gopher struct {
	Name string `json: "name"`
}

// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the authenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {

	// Ignore all messages created by the bot itself
	// This isn't required in this specific example but it's a good practice.
	if m.Author.ID == s.State.User.ID {
		return
	}

	if m.Content == "!gopher" {

		//Call the KuteGo API and retrieve our cute Dr Who Gopher
		response, err := http.Get(KuteGoAPIURL + "/gopher/" + "dr-who")
		if err != nil {
			fmt.Println(err)
		}
		defer response.Body.Close()

		if response.StatusCode == 200 {
			_, err = s.ChannelFileSend(m.ChannelID, "dr-who.png", response.Body)
			if err != nil {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Error: Can't get dr-who Gopher! :-(")
		}
	}

	if m.Content == "!random" {

		//Call the KuteGo API and retrieve a random Gopher
		response, err := http.Get(KuteGoAPIURL + "/gopher/random/")
		if err != nil {
			fmt.Println(err)
		}
		defer response.Body.Close()

		if response.StatusCode == 200 {
			_, err = s.ChannelFileSend(m.ChannelID, "random-gopher.png", response.Body)
			if err != nil {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Error: Can't get random Gopher! :-(")
		}
	}

	if m.Content == "!gophers" {

		//Call the KuteGo API and display the list of available Gophers
		response, err := http.Get(KuteGoAPIURL + "/gophers/")
		if err != nil {
			fmt.Println(err)
		}
		defer response.Body.Close()

		if response.StatusCode == 200 {
			// Transform our response to a []byte
			body, err := ioutil.ReadAll(response.Body)
			if err != nil {
				fmt.Println(err)
			}

			// Put only needed informations of the JSON document in our array of Gopher
			var data []Gopher
			err = json.Unmarshal(body, &data)
			if err != nil {
				fmt.Println(err)
			}

			// Create a string with all of the Gopher's name and a blank line as separator
			var gophers strings.Builder
			for _, gopher := range data {
				gophers.WriteString(gopher.Name + "\n")
			}

			// Send a text message with the list of Gophers
			_, err = s.ChannelMessageSend(m.ChannelID, gophers.String())
			if err != nil {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Error: Can't get list of Gophers! :-(")
		}
	}
}
```

## Let's dig in the code, step by step 

I know, the last code block is huge and there are little tips and mechanisms to know, so let's dig in code blocks, step by step and slowly :-).

### Ignore all messages created by the Bot

Everytime a message is sent in the Discord server, our function is executed, so the first thing is to tell that we ignore all messages created by the Bot itself:

```golang
	// Ignore all messages created by the bot itself
	// This isn't required in this specific example but it's a good practice.
	if m.Author.ID == s.State.User.ID {
		return
	}
```

### If message sent is equals to !gopher

If `!gopher` text message is sent in the Discord server, we ask **dr-who** Gopher to the KuteGo API, we close the response body and then if everything is OK, we send a message with embedded our cute Doctor Who Gopher.

```golang
	if m.Content == "!gopher" {

		//Call the KuteGo API and retrieve our cute Dr Who Gopher
		response, err := http.Get(KuteGoAPIURL + "/gopher/" + "dr-who")
		if err != nil {
			fmt.Println(err)
		}
		defer response.Body.Close()

		if response.StatusCode == 200 {
			_, err = s.ChannelFileSend(m.ChannelID, "dr-who.png", response.Body)
			if err != nil {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Error: Can't get dr-who Gopher! :-(")
		}
	}
```

### KuteGo API

As you have maybe seen, we call a URL started with "https://kutego-api-" in our application, but what is it?

In reality, it's a REST API named [KuteGo API](https://github.com/gaelleacas/kutego-api) created by my friend [Gaëlle Acas](twitter.com/gaelleacas/). This API plays with my [Gophers GitHub](github.com/scraly/gophers) repository and is hosted in a private Google Cloud Run.

```golang
const KuteGoAPIURL = "https://kutego-api-xxxxxx-ew.a.run.app"
```

So if you want to use it, you can install it locally (or wherever you want) and change `kutego-api` URL to `localhost:8080` ;-).

### If message sent is equals to !random

If `!random` text message is sent in the Discord server, we ask to the KuteGo API a random Gopher, we close the response body and then if everything is OK, we send a message with embedded our cute random Gopher. Surprise!

```golang
	if m.Content == "!random" {

		//Call the KuteGo API and retrieve a random Gopher
		response, err := http.Get(KuteGoAPIURL + "/gopher/random/")
		if err != nil {
			fmt.Println(err)
		}
		defer response.Body.Close()

		if response.StatusCode == 200 {
			_, err = s.ChannelFileSend(m.ChannelID, "random-gopher.png", response.Body)
			if err != nil {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Error: Can't get random Gopher! :-(")
		}
	}
```

### If message sent is equals to !gophers 

Let's attack to JSON parsing :-D.

In Golang, when you need to display informations contained in a JSON object, several new words appear: **marshal** and **unmarshal**.

![Go Marshal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vj69p4a2wux39vmlr0u2.png)

**Unmarshal** is the way that turn a JSON document into a Go struct.
**Marshal** is the opposite: we turn on Go struct to JSON document.

So when we unmarshal a JSON document, we transform it in a structured data that we can access easily. If a document doesn't fit into the structure it will throw an error.

So, in the following code block:
* we initialize a struct named Gopher that contains **Name** and will match with the word **name** in a JSON document
* we call **/gophers** route from KuteGo API
* we read the response body (in order to get an array of byte)
* we close the response body (a good practice seen in the previous article ;-))
* we create an array of Gopher (our Go struct with only information we want to display/handle)
* we put the JSON document in our array of Gopher
* we create a list of gophers with only the name of all existing gophers
* we send a message in the Discord server with this list of gophers


```golang
type Gopher struct {
	Name string `json: "name"`
}

...

	if m.Content == "!gophers" {

		//Call the KuteGo API and display the list of available Gophers
		response, err := http.Get(KuteGoAPIURL + "/gophers/")
		if err != nil {
			fmt.Println(err)
		}
		defer response.Body.Close()

		if response.StatusCode == 200 {
			// Transform our response to a []byte
			body, err := ioutil.ReadAll(response.Body)
			if err != nil {
				fmt.Println(err)
			}

			// Put only needed informations of the JSON document in our array of Gopher
			var data []Gopher
			err = json.Unmarshal(body, &data)
			if err != nil {
				fmt.Println(err)
			}

			// Create a string with all of the Gopher's name and a blank line as separator
			var gophers strings.Builder
			for _, gopher := range data {
				gophers.WriteString(gopher.Name + "\n")
			}

			// Send a text message with the list of Gophers
			_, err = s.ChannelMessageSend(m.ChannelID, gophers.String())
			if err != nil {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Error: Can't get list of Gophers! :-(")
		}
	}
```

### Wait... `strings.Builder` what is it?

In Go, like others languages, when you want to concatenate and build strings, several ways to do that exists.

The easiest way is to simply concatenate strings with the **+** operator like this:

```golang
package main

import (
	"fmt"
	"strconv"
)

func main() {
	str := "my string"
	str += " and numbers: "

	for i:=0;i<10;i++ {
	  // convert int to string
	  str += strconv.Itoa(i)
	}
	
	fmt.Println(str)
}
```

Easy but not very efficient when we concatenate a lot of strings together ;-).

Another (but old) solution is to use `bytes.Buffer` and then convert it to a string once you have concatenated everything:

```golang
package main

import (
	"fmt"
	"bytes"
	"strconv"
)

func main() {

	var buffer bytes.Buffer

	buffer.WriteString("my string")
	buffer.WriteString(" and numbers: ")

    for i:=0;i<10;i++ {
        buffer.WriteString(strconv.Itoa(i))
    }

    fmt.Println(buffer.String())
}
```

And, yes, our choosen solution, recommanded and new since Go 1.10 is to use `strings.Builder`:

```golang
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {

	var sb strings.Builder

	sb.WriteString("my string")
	sb.WriteString(" and numbers: ")

    for i:=0;i<10;i++ {
        sb.WriteString(strconv.Itoa(i))
    }

    fmt.Println(sb.String())
}
```

> "A Builder is used to efficiently build a string using Write methods. It minimizes memory copying. The zero value is ready to use."

Perfect! :-)

## Test it! 

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

First, you can export the token:

```
$ export BOT_TOKEN=<your bot token>
```

Let's run locally our Bot right now:

```
$ go run main.go -t $BOT_TOKEN
Bot is now running.  Press CTRL-C to exit.
```

Your Bot is running in your local machine and is now connected to Discord.

Let's enter several messages in our Discord server:

![!gopher command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/77cc6wko8lvuwzknmw9n.png)
 
Awesome, when we enter the command `!gopher`, our Who Gopher appear!

## Build it! 

Your application is now ready, you can build it.
For that, like the previous articles, we will use [Taskfile](https://taskfile.dev/#/) in order to automate our common tasks.

So, for this app too, I created a `Taskfile.yml` file with this content:

```yaml
version: "3"
    
tasks:
    build:
        desc: Build the app
        cmds:
        - GOFLAGS=-mod=mod go build -o bin/gopher-bot-discord main.go 

    run: 
        desc: Run the app
        cmds:
        - GOFLAGS=-mod=mod go run main.go -t $BOT_TOKEN

    bot:
        desc: Execute the bot
        cmds:
        - ./bin/gopher-bot-discord -t $BOT_TOKEN
```

Thanks to this, we can build our app easily:

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

Let's test it again with our fresh executable binary:

```
$ ./bin/gopher-bot-discord -t $BOT_TOKEN
Bot is now running.  Press CTRL-C to exit.
```

or through task:
```
$ task bot
task: [bot] ./bin/gopher-bot-discord -t $BOT_TOKEN
Bot is now running.  Press CTRL-C to exit.
```

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

Awesome, the `!gophers` command works too, we now know all the existings Gophers :-).

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

And, we've got a random Gopher! 

## Conclusion

As you have seen in this article and previous articles, it's possible to create applications in Go: CLI, REST API... but also fun apps like a Discord Bot! :-) 

All the code of our Bot in Go is available in: [https://github.com/scraly/learning-go-by-examples/tree/main/go-gopher-bot-discord](https://github.com/scraly/learning-go-by-examples/tree/main/go-gopher-bot-discord)

If you are interested by creating your own Bot for Discord in Go, several [examples with DiscordGo](https://github.com/bwmarrin/discordgo/tree/master/examples) are available.

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

Hope you'll like it.