Go SDK
Go SDK 讓你的業務服務端用 Go 呼叫服務端 REST API(下文簡稱 OpenAPI)、接收事件回撥和同步回撥。本頁介紹安裝、建立客戶端和全部配置項、第一個呼叫,以及 SDK 怎樣獲取和快取 App Token、怎樣用 context 控制每次呼叫的時限。
這裡的 SDK 包名為 drim,使用應用身份。以使用者身份登入、通過長連線收發訊息的 Go 客戶端 SDK(包名 im),見客戶端文件。
| 要做什麼 | 看哪裡 |
|---|---|
| 安裝 SDK | 下載與安裝 |
| 建立客戶端,設定超時、代理和自簽名證書 | 建立客戶端、配置項 |
| 為 App 簽發登入憑證 | 簽發登入憑證 |
| 以通知賬號或系統身份發訊息 | 傳送訊息 |
| 多個例項共用 App Token、輪換 Secret | App Token 的獲取與快取 |
| 控制每次呼叫的時限、帶上追蹤 ID | 上下文與超時 |
下載與安裝
當前釋出版本為 v1.0.3(MIT 許可證)。在業務 Go 工程中執行:
go get github.com/DeepRespond-AI/deeprespond-im-server-go-sdk@v1.0.3需要 Demo 或離線原始碼時:下載 Go 服務端 SDK + Demo(1.0.3) · SHA256 校驗檔案。 解壓後進入 deeprespond-im-server-go-sdk-1.0.3,該根目錄即 Go 模組,包含 SDK、Demo、MIT 許可證及完整測試資料。直接體驗見啟動步驟。
離線開發可選本地替換,路徑指向解壓根目錄:
go mod edit -replace=github.com/DeepRespond-AI/deeprespond-im-server-go-sdk=/绝对路径/deeprespond-im-server-go-sdk-1.0.3儲存下面的 Go 示例後執行 go mod tidy。升級已有專案時,移除舊的本地 replace,將匯入路徑更新為下表中的公開模組路徑;callback 與 drimtest 子包也需一起更新。
- 需要 Go 1.23 以上:遍歷方法(如
client.Users.All)返回 Go 1.23 的迭代器iter.Seq2,用for ... range遍歷。 - 只依賴標準庫,不引入第三方模組。
- 模組中有三個包:
| 包 | 匯入路徑 | 用途 |
|---|---|---|
drim | github.com/DeepRespond-AI/deeprespond-im-server-go-sdk | 呼叫 OpenAPI:客戶端、各服務的方法、引數和結果型別、錯誤 |
callback | github.com/DeepRespond-AI/deeprespond-im-server-go-sdk/callback | 接收回調:校驗簽名、防重放、去重、分派事件和同步回撥 |
drimtest | github.com/DeepRespond-AI/deeprespond-im-server-go-sdk/drimtest | 測試輔助:假的 OpenAPI 服務、帶簽名的回撥請求、簽名測試向量 |
模組路徑對應公開的獨立 GitHub 倉庫,包名仍為 drim,匯入時顯式指定包名:
import drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"只在服務端使用
Client Secret 和 App Token 等同應用管理員的密碼:可以建立和刪除使用者、以任何使用者的身份發訊息。不要把它們寫進程式碼倉庫、App 或網頁,客戶端請使用 Web SDK 等客戶端 SDK。
建立客戶端
package main
import (
"log"
"os"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func main() {
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
})
if err != nil {
log.Fatal(err) // 配置不合规,如 BaseURL 不是 https 地址
}
defer client.Close()
// 把 client 交给你的业务代码使用……
}- 一個客戶端對應一個應用:
OrgName和AppName即 AppKey(org_name#app_name)中#前後的兩部分,在控制台的應用詳情中檢視;Client ID 和 Client Secret 見鑑權與 App Token。同一程序管理多個應用時,為每個應用各建一個客戶端。 - 在程序中複用:客戶端可以被多個協程同時使用,內部共享 App Token、連線池和限流等待。請在程序啟動時建立一個,不要每個請求新建。
- 建立時不發請求:
New只檢查配置,第一次呼叫介面時才換取 App Token。配置不合規時返回普通的error(不是*drim.Error)。 Close:停止後臺的令牌換取,關閉 SDK 自己建立的 HTTP 客戶端的空閒連線。之後的呼叫返回client_unavailable錯誤。
New 檢查的內容:
BaseURL必須是http或https的絕對地址,不能帶查詢引數、片段、使用者名稱和密碼;末尾的/會去掉。可以帶路徑字首,如經過閘道器轉發的https://gw.example.com/im。http://只允許本機(localhost、127.0.0.0/8、::1)和內網地址(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16),其他地址必須用https://,避免憑據以明文發出。OrgName、AppName、ClientID、ClientSecret不能為空;時長和次數不能為負數;設定了RateLimit時PerSecond必須大於 0。
配置項
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
BaseURL | string | 是 | IM 服務的地址,如 https://im.example.com |
OrgName | string | 是 | 租戶的唯一標識,即 AppKey 中 # 之前的部分 |
AppName | string | 是 | 應用名稱,即 AppKey 中 # 之後的部分 |
ClientID | string | 是 | 應用的 Client ID |
ClientSecret | string | 是 | 應用的 Client Secret |
HTTPClient | *http.Client | 否 | 發請求用的 HTTP 客戶端,見下文 |
Timeout | time.Duration | 否 | 每次嘗試的時限(從發出請求到讀完響應),預設 30 秒。完成上傳固定為 150 秒,不受它影響 |
Retry | drim.RetryPolicy | 否 | 自動重試的設定,見下文 |
MaxRetryAfter | time.Duration | 否 | 被限流時自動等待的 Retry-After 上限,預設 60 秒;超過的不等待,直接返回 rate_limited |
RateLimit | *drim.RateLimit | 否 | 本地限速,預設關閉,見限流 |
TokenStore | drim.TokenStore | 否 | 多個服務例項共用一個 App Token 時的共享儲存,預設只在程序記憶體中,見多個例項共用 App Token |
Logger | *slog.Logger | 否 | 日誌,預設不輸出,見日誌與排查 |
Hooks | drim.Hooks | 否 | 請求前後的鉤子,用於注入追蹤頭、記錄指標,見日誌與排查 |
UserAgentSuffix | string | 否 | 附加到 User-Agent 之後,如 order-svc/2.3。SDK 的 User-Agent 為 deeprespond-im-server-sdk/go/{版本} |
HTTP 客戶端
不設定 HTTPClient 時,SDK 自己建立一個:每個主機最多 100 個空閒連線,空閒 90 秒關閉,連線超時和 TLS 握手各 10 秒,使用環境變數中的代理(HTTPS_PROXY 等),不跟隨重定向(OpenAPI 不會重定向,收到 3xx 按 invalid_response 處理)。Close 時關閉它的空閒連線。
需要自定義時(如私有化部署使用自簽名證書、不走代理),傳入自己的 *http.Client。SDK 不修改也不關閉它;不要設定 http.Client.Timeout,每次嘗試的時限由 SDK 按 Timeout 控制(完成上傳需要 150 秒)。
package main
import (
"crypto/tls"
"crypto/x509"
"log"
"net/http"
"os"
"time"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func newClient() (*drim.Client, error) {
// 私有化部署:信任自己的 CA 证书(SDK 没有跳过证书校验的选项)
pem, err := os.ReadFile("/etc/im/ca.pem")
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pem)
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil // 内网直连,不走代理
transport.TLSClientConfig = &tls.Config{RootCAs: pool}
return drim.New(drim.Config{
BaseURL: "https://im.internal.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
HTTPClient: &http.Client{Transport: transport},
Timeout: 10 * time.Second,
})
}
func main() {
client, err := newClient()
if err != nil {
log.Fatal(err)
}
defer client.Close()
}重試設定
Retry 的欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
MaxAttempts | int | 網路錯誤、超時、5xx 時的最多嘗試次數(含第一次),預設 4,即最多重試 3 次;1 表示不重試 |
MaxRateLimited | int | 收到 rate_limited 後等待重發的次數,預設 3 |
Disabled | bool | 為 true 時兩種都不重試。App Token 失效後換取新令牌再重發一次不受影響 |
哪些請求會自動重試、怎樣重試,見重試與冪等。
第一個呼叫
簽發登入憑證
推薦的登入方式是登入憑證:使用者登入你的業務系統後,業務服務端為他簽發一個 IM 登入憑證,App 用客戶端 SDK 以憑證登入(5 分鐘內有效、只能用一次),見簽發登入憑證。
下面是業務服務端提供給 App 的“取 IM 登入憑證”介面。AutoCreate: true 時使用者不存在會自動建立,業務系統的使用者 ID 直接作為 IM 的使用者名稱:
package main
import (
"encoding/json"
"log"
"net/http"
"os"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func main() {
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// App 登录业务系统后调用这个接口取 IM 的登录凭证;凭证无效时 App 会再取一次,这里要允许重复调用
http.HandleFunc("POST /api/im-ticket", func(w http.ResponseWriter, r *http.Request) {
userID := currentUser(r) // 由业务系统的登录态得出
ticket, err := client.Users.IssueLoginTicket(r.Context(), userID, drim.IssueLoginTicketParams{AutoCreate: true})
if drim.IsCode(err, drim.CodeUserDisabled) {
http.Error(w, "账号已被封禁", http.StatusForbidden)
return
}
if err != nil {
log.Printf("签发 IM 登录凭证失败:%v", err) // 错误信息中不含凭证和令牌
http.Error(w, "暂时不可用", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"ticket": ticket.Ticket, "expires_in": ticket.ExpiresIn})
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func currentUser(r *http.Request) string { return r.Header.Get("X-User-ID") }- 每次簽發得到一個新憑證(
ult_開頭),之前簽發的在有效期內仍可使用,所以 SDK 會自動重試這個請求;只把最後拿到的交給 App。 ticket.UserCreated表示這次是否建立了使用者。- 憑證相當於臨時密碼,只經 HTTPS 返回給這個使用者的 App,不要寫入日誌。
傳送訊息
以業務的通知賬號給使用者發一條單聊訊息:
package main
import (
"context"
"log"
"os"
"time"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func main() {
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 整次调用(含重试和限流等待)最多 10 秒
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
body, err := drim.Marshal(map[string]any{"card": "order", "order_id": "1001", "title": "订单已支付"})
if err != nil {
log.Fatal(err)
}
result, err := client.Messages.Send(ctx, drim.SendMessageParams{
From: "notice",
ToUser: "zhangsan",
ClientMsgID: "order-1001-paid", // 业务单号作为去重 ID:业务层重试也不会重复发送
Type: "custom",
Body: body,
Push: &drim.MessagePushOptions{Title: "订单通知", Body: "您的订单已支付"},
})
if err != nil {
log.Fatal(err)
}
if result.Duplicate {
log.Printf("重复提交,返回的是第一次发送的消息 %s", result.MessageID)
}
log.Printf("已发送:会话 %s,序号 %d", result.ConversationID, result.Seq)
// 群聊:不给 From 时以系统身份发送
_, err = client.Messages.Send(ctx, drim.SendMessageParams{
ToGroup: "1840012345678901",
Type: "text",
Body: drim.JSON(`{"text":"今晚 8 点系统维护"}`),
})
if err != nil {
log.Print(err)
}
}- 訊息內容
Body是json.RawMessage,格式見訊息格式。用drim.JSON直接寫 JSON 文本,或用drim.Marshal編碼 Go 的值(它不把<、>、&轉義為<等,訊息大小按發出的位元組計算)。 - 傳送的引數和返回值見傳送訊息;
ClientMsgID的作用見重試與冪等。
App Token 的獲取與快取
呼叫 OpenAPI 需要 App Token(見鑑權與 App Token)。SDK 用 Client ID 和 Client Secret 自動換取、快取和續期,你不需要自己呼叫換取令牌的介面:
- 第一次呼叫時換取:令牌儲存在客戶端的記憶體中,之後的請求都帶上它。
- 同時只換取一次:多個協程同時需要新令牌時,SDK 只發一個換取請求,其他請求等待它的結果(換取請求按來源 IP 限流,也計入應用的 OpenAPI 額度)。
- 提前續期:令牌的剩餘有效期不到 10%(最少提前 5 分鐘)時,下一次請求在後臺換取新令牌,換取期間仍用舊令牌,請求不必等待。剩餘不足 1 分鐘時,請求等待換取完成再發出。長時間沒有請求時不會主動續期。
- 到期時刻按本機時間計算:取“發出換取請求的時刻 +
expires_in”。本機時鐘與服務端相差較大時,令牌可能提前失效,由下面的“令牌失效”兜底。 - 令牌失效:OpenAPI 返回
unauthenticated且沒有details.reason時(Secret 被吊銷或過期、應用停用等),SDK 丟棄這個令牌,換取一次新令牌後把這個請求重發一次;換取失敗或重發仍是unauthenticated時,把錯誤返回給你。
換取失敗
| 換取的結果 | SDK 的處理 |
|---|---|
invalid_client(憑據錯誤或應用不存在)、ip_not_allowed(來源 IP 不在白名單) | 返回這個錯誤並記錯誤日誌;之後 1 分鐘內的請求直接返回 client_unavailable,不再換取,避免錯誤的配置耗盡來源 IP 的換取額度 |
tenant_unavailable、app_unavailable(服務暫停) | 返回這個錯誤;之後隨機 2 到 5 分鐘內的請求直接返回 client_unavailable,不再換取 |
rate_limited | 按 Retry-After 等待後再換取,最多 3 次 |
網路錯誤、5xx | 按 1、2、4 秒重試,最多 3 次 |
冷卻期內返回的 client_unavailable 錯誤中,Details["cause"] 為最初的錯誤碼,Details["until"] 為冷卻結束的時刻。判斷方法見錯誤型別與判斷。
換取令牌的錯誤中 Op 為 app.token,而不是這次呼叫的介面名。
應用刪除後客戶端永久失效
OpenAPI 返回 unauthenticated 且 details.reason 為 app_deleted 或 tenant_closed 時,應用已被刪除或租戶已登出,不會恢復。SDK 把客戶端標為永久失效:之後的呼叫都直接返回 client_unavailable,不再換取和重試,並記錯誤日誌。用 drim.IsPermanentlyUnavailable(err) 判斷,這時應清除儲存的憑據。
輪換 Client Secret
在控制台輪換 Secret 後,舊 Secret 有 24 小時的寬限期(見鑑權與 App Token)。把服務的配置更新為新 Secret 後,呼叫 UpdateCredentials,或用新配置重新建立客戶端:
// 配置中心推送了新的 Secret 之后
client.UpdateCredentials(os.Getenv("IM_CLIENT_ID"), os.Getenv("IM_CLIENT_SECRET"))UpdateCredentials 丟棄當前的令牌、結束換取失敗後的冷卻期,下一次請求用新憑據換取。它可以在客戶端被其他協程使用時呼叫。
多個例項共用 App Token
多個服務例項各自換取令牌是允許的,同時有多個有效的 App Token 互不影響。例項很多、希望減少換取請求時,可以用 TokenStore 讓它們共用一個令牌:
type TokenStore interface {
Get(ctx context.Context, key string) (token string, expiresAt time.Time, ok bool, err error)
Set(ctx context.Context, key string, token string, expiresAt time.Time) error
// CompareAndDelete 只在存储中的令牌等于 token 时删除(避免删掉别的实例刚换来的新令牌)
CompareAndDelete(ctx context.Context, key string, token string) error
}- 記憶體中沒有令牌時,SDK 先從儲存中讀取(
Get返回ok為false時由本例項換取,換取後Set)。從儲存中取到的令牌同樣按“剩餘不到 10% 時換取”續期,換取後寫回。 - 令牌失效時,只有儲存中的令牌仍是失效的那個才刪除(
CompareAndDelete)。 - 儲存的鍵形如
drim:token:加 16 個十六進位制字元,由服務地址、org_name、app_name和 Client ID 算出,不含 Secret。 - 儲存出錯(或 5 秒內沒有返回)時,SDK 退回本程序換取,不影響請求,並記警告日誌。
SDK 只依賴標準庫,沒有內建 Redis 的實現。用 go-redis 的寫法:
package tokenstore
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
// RedisTokenStore 在 Redis 中保存 App Token,值为 "{到期的毫秒}:{令牌}"。
type RedisTokenStore struct {
R *redis.Client
Prefix string
}
var _ drim.TokenStore = RedisTokenStore{}
func (s RedisTokenStore) Get(ctx context.Context, key string) (string, time.Time, bool, error) {
v, err := s.R.Get(ctx, s.Prefix+key).Result()
if errors.Is(err, redis.Nil) {
return "", time.Time{}, false, nil
}
if err != nil {
return "", time.Time{}, false, err
}
ms, token, ok := strings.Cut(v, ":")
n, err := strconv.ParseInt(ms, 10, 64)
if !ok || err != nil {
return "", time.Time{}, false, nil
}
return token, time.UnixMilli(n), true, nil
}
func (s RedisTokenStore) Set(ctx context.Context, key, token string, expiresAt time.Time) error {
value := fmt.Sprintf("%d:%s", expiresAt.UnixMilli(), token)
return s.R.Set(ctx, s.Prefix+key, value, time.Until(expiresAt)).Err()
}
var compareAndDelete = redis.NewScript(`
local v = redis.call('GET', KEYS[1])
if v then
local i = string.find(v, ':', 1, true)
if i and string.sub(v, i + 1) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
end
return 0`)
func (s RedisTokenStore) CompareAndDelete(ctx context.Context, key, token string) error {
return compareAndDelete.Run(ctx, s.R, []string{s.Prefix + key}, token).Err()
}建立客戶端時傳入 TokenStore: RedisTokenStore{R: rdb, Prefix: "order-svc:"}。
取得當前的令牌
client.Token(ctx) 返回當前有效的 App Token(需要時換取),只用於排查,例如用 curl 手工呼叫一個介面。不要把它寫入日誌或交給客戶端。client.AppID() 返回換取令牌時得到的應用 ID,還沒換取過時為空字串。
上下文與超時
每個方法的第一個引數是 context.Context:
ctx的截止時間是整次呼叫的總時限,包括自動重試、限流等待和等待換取令牌。等待會超過截止時間時,SDK 不再等待,直接返回錯誤。Config.Timeout是每次嘗試的時限(預設 30 秒),從發出請求到讀完響應。ctx被取消時返回錯誤碼canceled,errors.Is(err, context.Canceled)為真;截止時間已到或某次嘗試超時返回timeout,errors.Is(err, context.DeadlineExceeded)為真。- 在 HTTP 處理函式中,直接把
r.Context()傳給 SDK:使用者斷開連線時,SDK 的呼叫隨之取消。
單次呼叫的選項通過 ctx 傳入,不改變方法的簽名,隻影響用這個 ctx 發出的呼叫(遍歷方法的每一頁都沿用):
| 函式 | 說明 |
|---|---|
drim.WithRequestID(ctx, id) | 作為請求頭 X-Request-ID 傳送你的追蹤 ID,服務端原樣沿用並寫入日誌,同一次呼叫的重試沿用同一個 ID。必須是 1 到 64 個字母、數字或 -、_、.,不合規的在本地以 invalid_params 拒絕 |
drim.WithAttemptTimeout(ctx, d) | 這次呼叫中每次嘗試的時限,覆蓋 Config.Timeout(也覆蓋完成上傳的 150 秒) |
drim.WithMaxAttempts(ctx, n) | 這次呼叫中網路錯誤、超時、5xx 的最多嘗試次數(含第一次),1 表示不自動重試 |
drim.WithMaxRetryAfter(ctx, d) | 這次呼叫自動等待的 Retry-After 上限;0 表示被限流時不等待,直接返回 rate_limited |
func notifyPaid(ctx context.Context, client *drim.Client, orderID, username string) error {
// 整次调用最多 3 秒;带上业务的追踪 ID,服务端的日志中能按它查到这次调用
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
ctx = drim.WithRequestID(ctx, "order."+orderID)
// 线上请求被限流时不排队等待,交给业务的消息队列稍后再发
ctx = drim.WithMaxRetryAfter(ctx, 0)
_, err := client.Messages.Send(ctx, drim.SendMessageParams{
From: "notice",
ToUser: username,
ClientMsgID: "order-" + orderID + "-paid",
Type: "text",
Body: drim.JSON(`{"text":"您的订单已支付"}`),
})
return err
}頻道版本
本頁提供的原始碼包(1.0.2)包含獨立 RTC 頻道的服務(client.Channels)和 9 種頻道回撥的型別,更早的版本可能沒有。接入方法見頻道方法和使用獨立頻道。
