Project Overview
DisGo is a Go library for building Discord bots and integrations. It provides full REST and Gateway support, strongly typed data structures, and extensible middleware for high-performance, production-ready applications.
High-Level Overview
DisGo exposes a single, intuitive API to:
- Authenticate and manage bot clients
- Interact with Discord’s REST endpoints (channels, messages, guilds, etc.)
- Connect to Discord’s Gateway for real-time events
- Register event listeners and middleware
- Inspect and override library version metadata at build time
Key Features
- Full REST API coverage
- Gateway connection with shard management and intent filtering
- Strongly typed events and request/response structs
- Built-in rate limit handling and retry logic
- Middleware and plugin support for custom behavior
- Version and metadata exposed via
disgo.Version
anddisgo.SemVersion
- Minimal external dependencies for lightweight deployments
Why DisGo?
- Type Safety
Eliminate JSON parsing errors with Go structs that mirror Discord’s schema. - Performance
Optimized for low latency and efficient resource usage. - Flexibility
Configure gateway options (intents, identify payload), swap out middleware, or extend core behavior. - Discoverability
Comprehensive documentation, examples, and community-driven plugins.
Minimal Example
This example shows how to instantiate a DisGo client and log in:
package main
import (
"context"
"log"
"os"
"github.com/disgoorg/disgo"
"github.com/disgoorg/disgo/bot"
)
func main() {
token := os.Getenv("DISGO_BOT_TOKEN")
// Create a new DisGo client with default gateway options
client, err := disgo.New(token,
bot.WithGatewayConfigOpts(), // uses default intents
bot.WithEventListenerFunc(func(e *disgo.MessageCreate) {
if e.Message.Content == "hello" {
_, _ = e.Client().Rest().CreateMessage(e.ChannelID, disgo.NewMessageCreateRequest().SetContent("Hi there!"))
}
}),
)
if err != nil {
log.Fatalf("failed to create client: %v", err)
}
// Connect to Discord’s Gateway
if err := client.OpenGateway(context.Background()); err != nil {
log.Fatalf("failed to open gateway: %v", err)
}
}
Value Proposition
By using DisGo, developers gain:
- A single library to handle all Discord interactions
- Strongly typed code to reduce runtime errors
- Built-in best practices for rate limits and gateway management
- Easy extensibility for custom commands, middleware, and plugins
Explore the full documentation and examples on GitHub to get started with advanced features like slash commands, voice support, and custom middleware.
Getting Started
Get up and running with DisGo: install the library, create your first bot, run it, and respond to messages.
Prerequisites
- Go 1.24 or later
- A Discord application with a bot token
DISGO_TOKEN
environment variable set to your bot token
Adding DisGo as a Dependency
- Initialize your module (if needed):
go mod init github.com/yourusername/yourbot
- Add DisGo and tidy up:
go get github.com/disgoorg/disgo@latest go mod tidy
Writing Your First Bot
Create a file named main.go
with the following content:
package main
import (
"context"
"log"
"os"
"github.com/disgoorg/disgo"
"github.com/disgoorg/disgo/bot"
"github.com/disgoorg/disgo/events"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/gateway"
)
func main() {
token := os.Getenv("DISGO_TOKEN")
if token == "" {
log.Fatal("DISGO_TOKEN not set")
}
client, err := disgo.New(token,
// Enable message intents to receive message content
bot.WithGatewayConfigOpts(
gateway.WithIntents(
gateway.IntentGuildMessages,
gateway.IntentMessageContent,
),
),
// Register event handlers
bot.WithEventListeners(&events.ListenerAdapter{
OnReady: func(event *events.Ready) {
log.Printf("Logged in as %s#%s", event.User.Username, event.User.Discriminator)
},
OnMessageCreate: func(event *events.MessageCreate) {
// Ignore bots to prevent loops
if event.Message.Author.Bot {
return
}
// Simple ping-pong response
if event.Message.Content == "ping" {
_, err := event.Client().Rest.CreateMessage(event.ChannelID, discord.MessageCreate{
Content: "pong",
})
if err != nil {
log.Println("failed to send message:", err)
}
}
},
}),
)
if err != nil {
log.Fatal("failed to create client:", err)
}
// Connect to Discord and block until shutdown
if err = client.Open(context.Background()); err != nil {
log.Fatal("failed to open connection:", err)
}
<-client.Done()
}
Running the Bot
- Export your bot token:
export DISGO_TOKEN=your_bot_token
- Start the bot:
go run main.go
- In any guild channel where your bot is present, send
ping
to see it reply withpong
.
Next Steps
- Browse the full API reference on pkg.go.dev:
https://pkg.go.dev/github.com/disgoorg/disgo - Explore advanced examples in the
examples/
directory - Consult the
docs/
folder for guides on embeds, components, slash commands, and more
Core Concepts: Subsection Selection
Please specify which subsection you’d like detailed documentation for:
- SyncCommands utility
- Mux routing methods (Use, Route, Mount, etc.)
- handlerHolder matching logic
- Middleware package (e.g. Defer, Go, Logger)
- Router interface and handler types
Or let me know any other core concept area in disgoorg/disgo you’d like covered.
Advanced Usage
Dive into Disgo’s power-user features to fine-tune sharding, voice audio handling, HTTP servers, webhook clients, and proxy configurations.
Configuring the ShardManager
Customize shard behavior via functional options passed to sharding.New
.
import (
"context"
"log"
"github.com/disgoorg/disgo/sharding"
"github.com/disgoorg/disgo/gateway"
)
// eventHandler receives gateway events
func eventHandler(evt gateway.Event) { /* … */ }
func main() {
token := "YOUR_BOT_TOKEN"
mgr := sharding.New(token, eventHandler,
// 1. Manage shards 0–2 out of 5 total
sharding.WithShardIDs(0, 1, 2),
sharding.WithShardCount(5),
// 2. Enable auto-resharding: split large shards into 3
sharding.WithAutoScaling(true),
sharding.WithShardSplitCount(3),
// 3. Pass gateway options: set intents, proxy URL
sharding.WithGatewayConfigOpts(
gateway.WithIntents(gateway.IntentGuildMessages|gateway.IntentGuildMembers),
gateway.WithProxyURL("http://proxy:8080"),
),
// 4. Increase login concurrency
sharding.WithRateLimiterConfigOpt(
sharding.WithMaxConcurrency(2),
sharding.WithIdentifyWait(5*1e9), // 5s
),
)
ctx := context.Background()
if err := mgr.Open(ctx); err != nil {
log.Fatalf("failed to open sharding manager: %v", err)
}
defer mgr.Close(ctx)
}
Integrating AudioReceiver into Your Voice Connection
Receive per-user Opus frames by implementing voice.OpusFrameReceiver
.
1. Implement OpusFrameReceiver
import (
"log"
"github.com/disgoorg/disgo/voice"
"github.com/disgoorg/snowflake/v2"
)
type MyOpusReceiver struct{}
func NewMyOpusReceiver() *MyOpusReceiver { return &MyOpusReceiver{} }
func (r *MyOpusReceiver) ReceiveOpusFrame(userID snowflake.ID, packet *voice.Packet) error {
log.Printf("Frame from %d: %d bytes", userID, len(packet.Data))
// decode or buffer packet.Data as needed
return nil
}
func (r *MyOpusReceiver) CleanupUser(userID snowflake.ID) {
log.Printf("cleanup resources for user %d", userID)
}
func (r *MyOpusReceiver) Close() {
log.Println("closing MyOpusReceiver")
}
2. Register and Open Connection
import (
"context"
"log"
"github.com/disgoorg/disgo/voice"
"github.com/disgoorg/disgo/voice/manager"
)
func main() {
ctx := context.Background()
guildID := Snowflake(123)
channelID := Snowflake(456)
// Create voice connection
conn := manager.New().CreateConn(guildID)
// Set OpusFrameReceiver
receiver := NewMyOpusReceiver()
conn.SetOpusFrameReceiver(receiver)
if err := conn.Open(ctx, channelID, false, false); err != nil {
log.Fatalf("failed to open voice conn: %v", err)
}
defer conn.Close(ctx)
}
3. Practical Tips
- Frames arrive every 20 ms; aggregate for PCM decoding.
CleanupUser
triggers on user leave/disconnect.- Calling
conn.Close
or replacing the receiver invokes itsClose()
.
Configuring the HTTP Server
Expose an interaction callback endpoint with httpserver.New
and override defaults via options.
import (
"context"
"log"
"net/http"
"github.com/disgoorg/disgo/httpserver"
"github.com/disgoorg/disgo/discord"
)
func main() {
// Your public key from Discord Developer Portal
publicKey := "YOUR_PUBLIC_KEY_HEX"
// Define interaction handler
handler := func(respond httpserver.RespondFunc, event httpserver.EventInteractionCreate) {
respond(discord.InteractionResponse{
Type: discord.InteractionResponseTypeChannelMessageWithSource,
Data: &discord.InteractionResponseData{
Content: "Hello from Disgo!",
},
})
}
srv, err := httpserver.New(
publicKey, handler,
httpserver.WithAddress(":8080"),
httpserver.WithURL("/discord/interactions"),
httpserver.WithTLS("cert.pem", "key.pem"),
)
if err != nil {
log.Fatalf("failed to create server: %v", err)
}
go func() {
if err := srv.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Shutdown handling omitted for brevity
}
Creating a Webhook Client
Initialize webhook.Client
by ID/token or by parsing a URL, and apply client or logger customizations.
import (
"context"
"fmt"
"log"
"github.com/disgoorg/disgo/rest/webhook"
"github.com/disgoorg/snowflake/v2"
)
func exampleByID() {
id, _ := snowflake.Parse("123456789012345678")
token := "your-webhook-token"
client := webhook.New(id, token,
webhook.WithLogger(log.Default()),
webhook.WithRestClientConfigOpts(
webhook.WithBaseURL("https://custom.api/"),
),
)
fmt.Println("Webhook URL:", client.URL())
client.Close(context.Background())
}
func exampleByURL() {
rawURL := "https://discord.com/api/webhooks/123456789012345678/your-webhook-token"
client, err := webhook.NewWithURL(rawURL,
webhook.WithDefaultAllowedMentions(webhook.AllowedMentions{
Parse: []webhook.AllowedMentionType{webhook.AllowedMentionTypeUsers},
}),
)
if err != nil {
log.Fatalf("could not create webhook client: %v", err)
}
// Use client...
client.Close(context.Background())
}
Configuring Disgo with Gateway & REST Proxies
Point Disgo at custom gateway/rest proxies and disable internal rate limiters.
import (
"log"
"os"
"github.com/disgoorg/disgo"
"github.com/disgoorg/disgo/bot"
"github.com/disgoorg/disgo/gateway"
"github.com/disgoorg/disgo/rest"
"github.com/disgoorg/disgo/sharding"
)
func main() {
token := os.Getenv("disgo_token")
gwURL := os.Getenv("disgo_gateway_url")
restURL := os.Getenv("disgo_rest_url")
client, err := disgo.New(token,
bot.WithShardManagerConfigOpts(
sharding.WithGatewayConfigOpts(
gateway.WithURL(gwURL),
gateway.WithCompress(false),
),
sharding.WithRateLimiter(sharding.NewNoopRateLimiter()),
),
bot.WithRestClientConfigOpts(
rest.WithURL(restURL),
rest.WithRateLimiter(rest.NewNoopRateLimiter()),
),
// add your event handlers…
)
if err != nil {
log.Fatal("failed to create Disgo client:", err)
}
client.Open() // or client.OpenWithContext(ctx)
}
Practical guidance:
- Use
NewNoopRateLimiter
when your proxy enforces limits. - Disable compression behind low-latency internal proxies.
- Intents and sharding typically are handled by the proxy.
Examples Cookbook
Curated, runnable examples demonstrating common patterns and specialized features in Disgo.
Slash Commands via Gateway
Register and handle slash commands over the Discord Gateway.
client, err := disgo.New(token,
bot.WithDefaultGateway(),
bot.WithEventListenerFunc(func(event *events.ApplicationCommandInteractionCreate) {
data := event.SlashCommandInteractionData()
if data.CommandName() == "say" {
err := event.CreateMessage(discord.NewMessageCreateBuilder().
SetContent(data.String("message")).
SetEphemeral(data.Bool("ephemeral")).
Build(),
)
if err != nil {
log.Println("failed to send message:", err)
}
}
}),
)
if err != nil {
log.Fatal(err)
}
defer client.Close(context.Background())
// Register slash commands in a guild
_, err = client.Rest.SetGuildCommands(client.ApplicationID, guildID, []discord.ApplicationCommandCreate{
discord.SlashCommandCreate{
Name: "say",
Description: "says what you say",
Options: []discord.ApplicationCommandOption{
discord.ApplicationCommandOptionString{
Name: "message",
Description: "What to say",
Required: true,
},
discord.ApplicationCommandOptionBool{
Name: "ephemeral",
Description: "Visible only to you",
Required: true,
},
},
},
})
if err != nil {
log.Fatal(err)
}
// Connect to Discord
if err := client.OpenGateway(context.Background()); err != nil {
log.Fatal(err)
}
Practical tips:
- Use
signal.Notify
and block on a channel to gracefully shut down. - Log errors returned by
event.CreateMessage
to catch delivery issues.
Slash Commands via HTTP Server
Receive interactions via an HTTP callback endpoint—ideal for serverless setups.
// Custom ed25519 verifier
type customVerifier struct{}
func (customVerifier) Verify(pk httpserver.PublicKey, msg, sig []byte) bool {
return ed25519.Verify(pk, msg, sig)
}
func (customVerifier) SignatureSize() int {
return ed25519.SignatureSize
}
client, err := disgo.New(token,
bot.WithHTTPServerConfigOpts(
publicKey,
httpserver.WithURL("/interactions/callback"),
httpserver.WithAddress(":8080"),
httpserver.WithVerifier(customVerifier{}),
),
bot.WithEventListenerFunc(commandListener),
)
if err != nil {
log.Fatal(err)
}
defer client.Close(context.Background())
// Register commands as in the gateway example
client.Rest.SetGuildCommands(client.ApplicationID, guildID, commands)
// Start the HTTP server
if err := client.OpenHTTPServer(); err != nil {
log.Fatal(err)
}
Practical tips:
- Expose Discord’s public key via
WithVerifier
. - Ensure your service is reachable by Discord (reverse proxy or public ingress).
Localizing Slash Commands
Define per-locale names and descriptions to improve UX for multilingual communities.
discord.SlashCommandCreate{
Name: "say",
NameLocalizations: map[discord.Locale]string{
discord.LocaleEnglishGB: "say",
discord.LocaleGerman: "sagen",
},
Description: "says what you say",
DescriptionLocalizations: map[discord.Locale]string{
discord.LocaleEnglishGB: "says what you say",
discord.LocaleGerman: "sagt was du sagst",
},
Options: []discord.ApplicationCommandOption{
discord.ApplicationCommandOptionString{
Name: "message",
NameLocalizations: map[discord.Locale]string{
discord.LocaleGerman: "nachricht",
},
Description: "What to say",
DescriptionLocalizations: map[discord.Locale]string{
discord.LocaleGerman: "Was soll ich sagen?",
},
Required: true,
},
},
}
Practical tips:
- Localizations apply at registration time—update via REST when changed.
- Omit a locale to fall back on the base
Name
orDescription
.
Using Components V2 for Rich Message Layouts
Build and send a Discord message using Components V2 (v0.38+).
flags := discord.MessageFlagIsComponentsV2
if ephemeral, _ := data.OptBool("ephemeral"); ephemeral {
flags = flags.Add(discord.MessageFlagEphemeral)
}
msg := discord.MessageCreate{
Flags: flags,
Components: []discord.LayoutComponent{
discord.NewContainer(
discord.NewSection(
discord.NewTextDisplay("**Name: [Seeing Red](https://open.spotify.com/track/65qBr6ToDUjTD1RiE1H4Gl)**"),
discord.NewTextDisplay("**Artist: [Architects](https://open.spotify.com/artist/3ZztVuWxHzNpl0THurTFCv)**"),
discord.NewTextDisplay("**Album: [The Sky, The Earth & All Between](https://open.spotify.com/album/2W82VyyIFAXigJEiLm5TT1)**"),
).WithAccessory(discord.NewThumbnail("attachment://thumbnail.png")),
discord.NewTextDisplay("`0:08` / `3:40`"),
discord.NewTextDisplay("[🔘▬▬▬▬▬▬▬▬▬]"),
discord.NewSmallSeparator(),
discord.NewActionRow(
discord.NewPrimaryButton("", "/player/previous").WithEmoji(discord.ComponentEmoji{Name: "⏮"}),
discord.NewPrimaryButton("", "/player/pause_play").WithEmoji(discord.ComponentEmoji{Name: "⏯"}),
discord.NewPrimaryButton("", "/player/next").WithEmoji(discord.ComponentEmoji{Name: "⏭"}),
discord.NewDangerButton("", "/player/stop").WithEmoji(discord.ComponentEmoji{Name: "⏹"}),
discord.NewPrimaryButton("", "/player/like").WithEmoji(discord.ComponentEmoji{Name: "❤️"}),
),
).WithAccentColor(0x5c5fea),
},
Files: []*discord.File{
discord.NewFile("thumbnail.png", "", bytes.NewReader(thumbnail)),
},
}
// Send in response to an interaction
if err := e.CreateMessage(msg); err != nil {
slog.Error("error sending Components V2 message", slog.Any("err", err))
}
Practical tips:
- Always include
MessageFlagIsComponentsV2
. - Reference files with
attachment://<filename>
. - Group buttons in
NewActionRow
(max 5 per row). - Use
NewSmallSeparator
and multipleNewTextDisplay
calls for fine-grained spacing.
AutoModeration Rule Management
Create, update, and delete AutoModeration rules via the REST client.
// Create a rule
rule, err := client.Rest.CreateAutoModerationRule(guildID, discord.AutoModerationRuleCreate{
Name: "test-rule",
EventType: discord.AutoModerationEventTypeMessageSend,
TriggerType: discord.AutoModerationTriggerTypeKeyword,
TriggerMetadata: &discord.AutoModerationTriggerMetadata{
KeywordFilter: []string{"*test*"},
},
Actions: []discord.AutoModerationAction{
{
Type: discord.AutoModerationActionTypeSendAlertMessage,
Metadata: &discord.AutoModerationActionMetadata{ChannelID: channelID},
},
{Type: discord.AutoModerationActionTypeBlockMessage},
},
Enabled: omit.Ptr(true),
})
if err != nil {
log.Fatal("create rule:", err)
}
fmt.Println("Created rule ID:", rule.ID)
// Update the rule
updated, err := client.Rest.UpdateAutoModerationRule(guildID, rule.ID, discord.AutoModerationRuleUpdate{
Name: omit.Ptr("test-rule-updated"),
TriggerMetadata: &discord.AutoModerationTriggerMetadata{
KeywordFilter: []string{"*test2*"},
},
Actions: &[]discord.AutoModerationAction{
{
Type: discord.AutoModerationActionTypeSendAlertMessage,
Metadata: &discord.AutoModerationActionMetadata{ChannelID: channelID},
},
},
})
if err != nil {
log.Fatal("update rule:", err)
}
fmt.Println("Updated rule name to:", *updated.Name)
// Delete the rule
if err := client.Rest.DeleteAutoModerationRule(guildID, rule.ID); err != nil {
log.Fatal("delete rule:", err)
}
fmt.Println("Deleted rule:", rule.ID)
Practical tips:
- Use
omit.Ptr(...)
for optional fields. - Updating
Actions
replaces existing actions; passnil
or omit to keep unchanged. - Listen to
events.AutoModerationRuleCreate/Update/Delete
to track propagation.
Playing a DCA Audio File
Stream a pre-encoded Opus “.dca” file into a voice channel with 20 ms frame timing.
func play(client *bot.Client, closeChan chan os.Signal) {
conn := client.VoiceManager.CreateConn(guildID)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := conn.Open(ctx, channelID, false, false); err != nil {
log.Fatal("voice connect:", err)
}
defer func() {
closeCtx, closeCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer closeCancel()
conn.Close(closeCtx)
}()
if err := conn.SetSpeaking(ctx, voice.SpeakingFlagMicrophone); err != nil {
log.Fatal("set speaking:", err)
}
writeOpus(conn.UDP())
closeChan <- syscall.SIGTERM
}
func writeOpus(w io.Writer) {
file, err := os.Open("nico.dca")
if err != nil {
log.Fatal("open dca:", err)
}
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
var lenBuf [4]byte
for range ticker.C {
if _, err := io.ReadFull(file, lenBuf[:]); err != nil {
if err == io.EOF {
file.Close()
return
}
log.Fatal("read length:", err)
}
frameLen := int64(binary.LittleEndian.Uint32(lenBuf[:]))
if _, err := io.CopyN(w, file, frameLen); err != nil {
file.Close()
return
}
}
}
Practical tips:
- Generate DCA with the
dca
tool or custom script. - Place
nico.dca
next to your binary or adjust the path. - Enable
IntentGuildVoiceStates
and supplyguildID
+channelID
.
OAuth2 Client Configuration and Authorization Flow
Set up an OAuth2 client, generate auth URLs, handle callbacks, and fetch user data.
1. Initialize the OAuth2 Client
import (
"net/http"
"os"
"github.com/disgoorg/disgo/oauth2"
"github.com/disgoorg/disgo/rest"
"github.com/disgoorg/snowflake/v2"
)
var (
clientID = snowflake.GetEnv("client_id")
clientSecret = os.Getenv("client_secret")
httpClient = http.DefaultClient
oauthClient *oauth2.Client
)
func initOAuth2() {
oauthClient = oauth2.New(
clientID,
clientSecret,
oauth2.WithRestClientConfigOpts(
rest.WithHTTPClient(httpClient),
),
)
}
2. Generate the Authorization URL
import "github.com/disgoorg/disgo/discord"
func handleLogin(w http.ResponseWriter, r *http.Request) {
params := oauth2.AuthorizationURLParams{
RedirectURI: baseURL + "/callback",
Scopes: []discord.OAuth2Scope{
discord.OAuth2ScopeIdentify,
discord.OAuth2ScopeEmail,
discord.OAuth2ScopeConnections,
},
}
url := oauthClient.GenerateAuthorizationURL(params)
http.Redirect(w, r, url, http.StatusSeeOther)
}
3. Handle the OAuth2 Callback
var sessions = make(map[string]oauth2.Session)
func handleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if code == "" || state == "" {
http.Error(w, "missing code or state", http.StatusBadRequest)
return
}
session, _, err := oauthClient.StartSession(code, state)
if err != nil {
http.Error(w, "start session: "+err.Error(), http.StatusInternalServerError)
return
}
id := randStr(32)
sessions[id] = session
http.SetCookie(w, &http.Cookie{Name: "token", Value: id, Path: "/"})
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func randStr(n int) string {
letters := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
4. Fetch User Data with the Session
import "github.com/disgoorg/disgo/json/v2"
func handleRoot(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("token")
if err != nil {
showLogin(w); return
}
session, ok := sessions[cookie.Value]
if !ok {
showLogin(w); return
}
user, err := oauthClient.GetUser(session)
if err != nil {
http.Error(w, "GetUser: "+err.Error(), http.StatusInternalServerError)
return
}
connections, err := oauthClient.GetConnections(session)
if err != nil {
http.Error(w, "GetConnections: "+err.Error(), http.StatusInternalServerError)
return
}
userJSON, _ := json.MarshalIndent(user, "", " ")
connJSON, _ := json.MarshalIndent(connections, "", " ")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(
"User:<br><pre>" + string(userJSON) + "</pre>" +
"Connections:<br><pre>" + string(connJSON) + "</pre>",
))
}
Practical tips:
- Match
RedirectURI
exactly with your Discord application setting. - Store sessions securely (database or cache) in production.
- Validate
state
to protect against CSRF. - Request only necessary scopes.
- Implement token refresh for long-lived sessions.
Message Collector Example
Collect a fixed number of user messages with a timeout using bot.NewEventCollector
.
func onMessageCreate(event *events.MessageCreate) {
if event.Message.Author.Bot || event.Message.Author.System {
return
}
if event.Message.Content != "start" {
return
}
go func() {
ch, closeCollector := bot.NewEventCollector(event.Client(), func(e *events.MessageCreate) bool {
return e.ChannelID == event.ChannelID &&
e.Message.Author.ID == event.Message.Author.ID &&
e.Message.Content != ""
})
defer closeCollector()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
result := ">>> "
count := 1
for {
select {
case <-ctx.Done():
event.Client().Rest.CreateMessage(event.ChannelID, discord.MessageCreate{
Content: "cancelled",
})
return
case msgEvt := <-ch:
result += fmt.Sprintf("%d. %s\n\n", count, msgEvt.Message.Content)
if count == 3 {
event.Client().Rest.CreateMessage(msgEvt.ChannelID, discord.MessageCreate{
Content: result,
})
return
}
count++
}
}
}()
}
Practical tips:
- Defer the collector’s close function to free resources.
- Use
select
onctx.Done()
to handle timeouts. - Tailor the filter function for exact collection criteria.
- Run the collector in its own goroutine to avoid blocking other handlers.
Architecture Overview
This section describes how DisGo’s core packages interact to provide a high-level, extensible Discord bot framework. You’ll learn how the root, bot, gateway, REST, HTTP server, and cache modules fit together and where to extend functionality.
Package Layout
• disgo
– Entry point: version metadata and New
.
• bot
– Client
: orchestrates gateway, REST, HTTP server, cache, logging.
– ConfigOpt
: options for intents, shards, caching, rate-limits, HTTP server.
• gateway
– WebSocket connection, shard manager, event dispatch, lifecycle hooks.
• rest
– Typed wrappers around Discord REST API, built-in rate-limiter, middleware support.
• httpserver
– HTTP listener for slash-command interactions, signature verification, router integration.
• cache
– Abstract cache interfaces, default in-memory providers, adapters (Redis, BoltDB).
• util
– Logging, bitwise flag helpers, backoff, UUID, timing utilities.
Client Initialization
Use disgo.New
to wire all subsystems with sensible defaults. You supply your bot token and any number of bot.ConfigOpt
to tweak behavior.
import (
"log"
"os"
"github.com/disgoorg/disgo"
"github.com/disgoorg/disgo/bot"
)
func main() {
token := os.Getenv("DISCORD_TOKEN")
if token == "" {
log.Fatal("missing DISCORD_TOKEN")
}
client, err := disgo.New(token,
// Core gateway intents
bot.WithIntents(bot.IntentGuilds, bot.IntentGuildMessages),
// Use default in-memory cache
bot.WithCacheProvider(bot.CacheWithDefault[bot.UserCache]()),
// Enable built-in HTTP server on :8080
bot.WithHTTPServerAddr(":8080"),
// Debug-level logging
bot.WithLogLevel(bot.LogDebug),
)
if err != nil {
log.Fatalf("failed to create client: %v", err)
}
…
}
Gateway Subsystem
The gateway module manages WebSocket connections and shards.
• ShardManager spins up one or more shards based on config.
• EventDispatcher invokes AddEventHandler
callbacks on typed event structs.
• Lifecycle Hooks allow OnConnect
, OnDisconnect
, OnReconnect
handlers.
// Register a message handler
client.Gateway().AddEventHandler(func(e bot.MessageCreate) {
if e.Message.Content == "!hello" {
_, _ = e.Sender().SendMessage(e.Message.ChannelID, "Hello, world!")
}
})
// Start the connection loop (blocks)
if err := client.OpenGateway(); err != nil {
log.Fatalf("gateway failed: %v", err)
}
defer client.CloseGateway()
REST Module
All Discord REST endpoints live under disgo/rest
.
• Typed Methods return Go structs for channels, guilds, emojis, etc.
• RateLimiter prevents HTTP 429s; you can supply a custom one with bot.WithRESTRateLimiter
.
• Middleware hooks let you inject headers, logging, or backoff policies.
import "github.com/disgoorg/disgo/rest"
// Replace default rate-limiter with in-memory bucket
client, _ := disgo.New(token,
bot.WithRESTRateLimiter(rest.NewMemoryRateLimiter()),
)
HTTP Server (Interactions)
DisGo includes a built-in HTTP server for slash command interactions:
• Verifies Discord signatures.
• Dispatches InteractionCreate
events via the gateway dispatcher.
• Exposes WithHTTPServerAddr
and WithHTTPServerRouter
for custom routing.
client, _ := disgo.New(token,
bot.WithHTTPServerAddr(":3000"),
)
// Interaction handlers use the same AddEventHandler API
client.Gateway().AddEventHandler(func(i bot.InteractionCreate) {
// respond to slash command
})
go client.OpenHTTPServer()
Caching
bot.CacheProvider
abstracts storage of users, channels, guilds, roles, etc.
• In‐memory: bot.CacheWithDefault[...]()
• Redis: cache.NewRedisProvider(addr, options...)
• Custom: implement cache.Provider
for your backend.
// Use Redis for user and channel caches
client, _ := disgo.New(token,
bot.WithCacheProvider(
bot.CacheWithProvider(cache.NewRedis("localhost:6379"), bot.UserCache, bot.ChannelCache),
),
)
Extension Points
DisGo design encourages customization via:
• bot.ConfigOpt: add or override any subsystem during New
.
• Event Handlers: Gateway().AddEventHandler
for low-latency callbacks.
• REST Middleware: custom auth, metrics, or retry logic.
• HTTP Router: mount additional routes (health checks, metrics).
• Custom Cache Providers: optimize memory or persistence.
By understanding these layers and their interactions, you can reason about internals and extend DisGo to meet advanced use cases.
Contribution & Development Guide
This guide outlines coding style, branching, issue/PR workflow, CI, and local testing for DisGo.
Coding Style & Formatting
Enforce consistent formatting via .editorconfig
at the repo root. Editors with EditorConfig support will apply these rules automatically.
Key settings:
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
tab_width = 4
[*.md]
trim_trailing_whitespace = false
[*.{yaml,yml}]
indent_style = space
indent_size = 2
Before each commit:
go fmt ./...
goimports -w .
Branching & Commit Conventions
Use descriptive prefixes to categorize work:
- feat/ – new features
- fix/ or bugfix/ – bug fixes
- docs/ – documentation
- refactor/ – structural changes
Example:
git checkout -b feat/add-reaction-caching
Write commit messages in the form:
<type>(<scope>): <short description>
Reference issues:
fix(cache): prevent TTL overflow
Closes #123
Issue Templates
Bug Report
Use .github/ISSUE_TEMPLATE/bug_report.md
. Include:
- Describe the bug – What’s not working?
- Error – Paste logs or stack trace.
- To Reproduce – Minimal Go snippet.
- Expected behavior – Desired outcome.
- DisGo Version – e.g.
v0.8.3
orgo.mod
go
directive.
Feature Request
Use .github/ISSUE_TEMPLATE/feature_request.md
. Include:
- Problem – Current limitation.
- Solution – Proposed behavior.
- Alternatives – Other approaches considered.
- Additional context – Diagrams or mockups.
Pull Request Labeling
Automated by .github/workflows/labeler.yml
using actions/labeler@v5
:
Branch prefixes
- feat/ →
type:enhancement
- fix/, bugfix/, hotfix/ →
type:bugfix
- docs/, documentation/ →
type:docs
- refactor/ →
type:refactor
- feat/ →
File-pattern rules
cache/*
→t:caching
gateway/*
→t:gateway
handler/*
→t:handler
oauth2/*
→t:oauth2
rest/*
→t:rest
sharding/*
→t:sharding
voice/*
→t:voice
*_rate_limiter_*
→t:ratelimits
Example:
git checkout -b fix/cache-ttl-expiry
# On PR open → labels: "type:bugfix", "t:caching"
CI Workflows
Go Pipeline (.github/workflows/go.yml
)
Runs on pushes and PRs affecting Go files, go.mod
, go.sum
, or the workflow itself. Jobs: build, test, lint.
name: Go
on:
push:
paths:
- '**/*.go'
- 'go.mod'
- 'go.sum'
- '.github/workflows/go.yml'
pull_request:
paths:
- '**/*.go'
- 'go.mod'
- 'go.sum'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.24
- uses: actions/checkout@v4
- name: Build
run: go build -v ./...
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.24
- uses: actions/checkout@v4
- name: Test
run: go test -v ./...
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.24
- uses: actions/checkout@v4
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
Tips:
- Cache Go modules:
- name: Cache Go modules uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- Align
go.mod
’sgo
directive withgo-version
. - Customize linting via
.golangci.yml
.
Labeler Workflow (.github/workflows/labeler.yml
)
Triggers on pull request events and applies labels based on rules in .github/labeler.yml
.
Local Testing & Linting
Run before every push:
# Format and imports
go fmt ./...
goimports -w .
# Run all tests
go test ./... -cover
# Lint
golangci-lint run
Submitting a Pull Request
- Fork and clone the repo.
- Create a branch with an appropriate prefix.
- Implement changes; update or add tests.
- Run formatting, tests, linting.
- Push and open a PR against
main
, linking relevant issues. - Ensure CI passes; address review feedback.
- Squash or rebase commits as requested.
Practical Tips
- Keep PRs focused on a single concern.
- Write clear, conventional commit messages.
- Update documentation when adding or changing behavior.
- Rebase frequently against
main
to minimize conflicts. - Use
make ci
(if available) to replicate CI steps locally.