After created an HTTP REST API server and our first CLI (Command Line Interface) application in Go, 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, 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:
Then, click on Advanced and then enable Developer Mode:
Create a Discord application
Go to https://discord.com/developers/applications/, then on New Application button, and then name your app and click on Create button:
You can now fill the description and add an icon to your freshly created app.
Create a Bot
Click on Bot menu and then on Add Bot button.
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:
Now go in OAuth2 menu and click on Copy button in order to get Client ID
information:
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:
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:
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):
You will have to paste this token further in this article ;-).
It's time to create our awesome Bot!
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 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 library.
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:
$ 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:
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:
// 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:
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.
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:
// 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.
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 created by my friend Gaëlle Acas. This API plays with my Gophers GitHub repository and is hosted in a private Google Cloud Run.
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!
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.
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
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:
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:
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
:
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:
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 in order to automate our common tasks.
So, for this app too, I created a Taskfile.yml
file with this content:
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.
Awesome, the !gophers
command works too, we now know all the existings Gophers :-).
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
If you are interested by creating your own Bot for Discord in Go, several examples with DiscordGo are available.
In the following articles we will create others kind/types of applications in Go.
Hope you'll like it.
Top comments (9)
I got empty string when getting m.Content. Maybe if someone follows this post and has the same problem as me, You could check the solution provided in the following issue. Thank you, this post really help me understanding Go
Thanks a ton!
This is much better than going through hundreds of pages of authoritative books on golang, currently going through the book The Go Programming language. I'm an example-oriented learner, so this will help a lot. I hope you have more time in your schedule to write tutorials on go for beginners.
Wonderful tutorial! Thank you so much! I am trying to learn Go through coding a Discord Bot and this provides me with a very basic outline of what I have to do. Worked like a charm too. :)
One tiny issue in the code:
json: "name"
in the Gopher struct should have no space after json: or you'll get a warning like "struct field tagjson: "name"
not compatible with reflect.StructTag.Get: bad syntax for struct tag value".Was a bit worrisome to me as a total newbie before I googled the fix. :)
Thanks Jan
You use which version of Go? :)
I use 1.17.3.
Just went through this tutorial. Very straight forward and helpful as I am currently learning Go.
One thing that I do want to point out is when I ran the bot and started sending messages I wasn't getting a response back from the bot. It turns out that in the bot settings in the Discord Developer Portal you need to go to Bot, then "turn on" the following setting. This might be something new after this tutorial was written:
How to read an attachment sent in a slash command in discordgo?
the bot can't receive message contents from guild, but direct message works properly. not sure if it's the bug from the Github discordgo repo