How to Use the Slack API in Go
You need a bot to post a message to a Slack channel. You could craft a JSON payload, sign the request, handle rate limits, and parse the response manually. Or you can use the official slack-go/slack library, which handles the boilerplate so you focus on the logic. The library wraps the Slack API in idiomatic Go types and methods, turning HTTP calls into simple function calls.
The library handles the boilerplate
The slack-go/slack package is a client library. It speaks the Slack API protocol for you. You provide a token, and the library manages authentication headers, JSON serialization, and retry logic. You interact with structs like slack.Client instead of raw http.Request objects. The library maps Slack's API endpoints to Go methods. PostMessage becomes a function call. GetUsers returns a slice of user structs. This reduces errors and keeps your code readable.
Install the library with the standard tooling. Run go get github.com/slack-go/slack in your terminal. The command downloads the package and adds it to your go.mod file. Your editor should run gofmt on save. The library follows standard Go formatting. Trust the tool. Argue logic, not indentation.
Post a message
Here's the simplest way to post a message: create a client, call PostMessage, and handle the error.
package main
import (
"log"
"github.com/slack-go/slack"
)
func main() {
// Token authenticates the bot. Keep this secret.
token := "xoxb-your-bot-token-here"
// New creates a client configured with the token.
api := slack.New(token)
// Channel ID identifies the target destination.
channelID := "C012AB3CD"
// MsgOptionText sets the message body.
text := "Hello from Go!"
_, _, _, err := api.PostMessage(channelID, slack.MsgOptionText(text, false))
// Check error immediately. Slack returns errors for invalid channels or tokens.
if err != nil {
log.Fatalf("Failed to post message: %v", err)
}
}
When you call slack.New(token), the library stores the token and sets up an HTTP client. It doesn't connect to Slack yet. The connection happens on the first request. PostMessage builds a JSON payload, adds the Authorization: Bearer header, and sends a POST request to api.slack.com/chat.postMessage. The library unmarshals the JSON response into Go structs. If Slack returns an error code, the library wraps it in a Go error. You check err != nil to catch issues like missing permissions or rate limits. The library also handles exponential backoff automatically if Slack returns a 429 Too Many Requests response.
The PostMessage function uses a variadic options pattern. You pass the channel ID, followed by any number of slack.MsgOption functions. Each option modifies the request payload. slack.MsgOptionText(text, false) sets the plain text message. The second argument controls whether to escape characters. slack.MsgOptionBlocks(blocks...) adds Block Kit elements like buttons or sections. This pattern keeps the API flexible. You can combine options in any order. The library applies them sequentially to build the final JSON.
Post a message, check the error, move on.
Handle interactions with webhooks
Interactive components like buttons or modals require a webhook listener. Slack sends a POST request to your server when a user clicks a button. You must verify the request signature to ensure it actually came from Slack. Then you parse the payload and respond.
Here's the handler that validates the request and routes the interaction:
package main
import (
"encoding/json"
"net/http"
"github.com/slack-go/slack"
)
// handleInteractions validates and decodes Slack webhook requests.
func handleInteractions(w http.ResponseWriter, r *http.Request) {
// VerifyRequest checks the signature against your signing secret.
if !slack.VerifyRequest(r, "your-signing-secret") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Decode the payload into a struct.
var interaction slack.InteractionCallback
if err := json.NewDecoder(r.Body).Decode(&interaction); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Route based on interaction type.
switch interaction.Type {
case slack.InteractionTypeBlockActions:
handleBlockAction(w, interaction)
default:
return
}
}
Slack enforces a strict 3-second response window for webhooks. If your handler takes longer, Slack assumes the request failed and retries. This causes duplicate actions. Always write the HTTP response status before performing slow operations. In the example, w.WriteHeader(http.StatusOK) runs first. The API call happens after. If the API call fails, you can't change the status code, but you can log the error. For long-running tasks, spawn a goroutine to handle the work and return 200 immediately. Use channels or a database to track state.
Here's the helper that responds to the button click:
// handleBlockAction responds to a button click in a message.
func handleBlockAction(w http.ResponseWriter, interaction slack.InteractionCallback) {
// Create a client to make API calls.
// In production, reuse a single client instance.
api := slack.New("xoxb-your-bot-token-here")
// Write status 200 immediately.
// Slack expects a response within 3 seconds or it retries.
w.WriteHeader(http.StatusOK)
// Post a message to the channel where the button was clicked.
_, _, _, err := api.PostMessage(
interaction.Channel.ID,
slack.MsgOptionText("Action received!", false),
)
if err != nil {
// Log the error but don't crash the handler.
log.Printf("PostMessage failed: %v", err)
}
}
When calling PostMessage from a handler, pass a context derived from the request. This cancels the API call if the client disconnects. Use r.Context() or context.WithTimeout. The library methods accept context in newer versions or you can wrap the client. Always respect cancellation. Name your receiver variables with one or two letters matching the type, like (c *Client) PostMessage(...). Don't use this or self.
Verify the signature first. Trust nothing from the internet.
Pitfalls and errors
Common issues include token scope mismatches and signature verification failures. If your bot token lacks the chat:write scope, the API returns an error like missing_scope. The library surfaces this as a standard Go error. You can check the error message or type-assert to *slack.APIError for details. The compiler rejects this with undefined: slack if you forget to import the package. If the token is expired, the runtime returns invalid_auth from the API.
Signature verification fails if your signing secret is wrong or the request body is modified. The library returns false from VerifyRequest. Always return 401 Unauthorized in that case. Rate limiting is handled automatically, but heavy usage can still trigger delays. The library retries with exponential backoff. If you need to detect rate limits explicitly, check the error for rate_limited. Goroutine leaks can happen if you spawn a goroutine per request and don't manage context. Use context.WithTimeout for API calls in handlers.
Go convention dictates checking errors immediately. Write if err != nil { return err } or log and return. The community accepts this verbosity because it makes failure paths explicit. Don't swallow errors. Log them or return them. The worst goroutine bug is the one that never logs.
Scopes are permissions. Check them before debugging code.
When to use what
Use slack-go/slack when you need full API access, including posting messages, managing channels, and handling interactive components. Use raw http.Client when you only need to hit a specific webhook URL that doesn't require authentication or complex payloads. Use the slack-go/slack event subscriber when you need to listen for real-time events like messages or reactions via the Events API. Use a third-party library like bot when you want a higher-level framework with command parsing and middleware, though the official library is usually sufficient for most bots. Use plain sequential code for simple scripts that run once and exit. Use a long-running HTTP server for bots that respond to interactions or events.
The official library is the standard. Reach for it unless you have a specific reason not to.