如何用 Go 调用 SUBMAIL SMS API 发送国际短信
一、为什么选择 Go + SUBMAIL?
Go 语言凭借其出色的并发模型、极低的内存占用和接近 C 的执行性能,已成为微服务和云原生场景的首选语言。结合 SUBMAIL SMS API,Go 开发者可以:
- 零第三方依赖接入:完全使用 Go 标准库(
net/http、crypto/md5、mime/multipart),无需任何外部包 - goroutine 天然支持高并发:利用
goroutine + sync.WaitGroup实现高并发批量发送,性能远超同步方案 - 覆盖全球 200+ 国家和地区,国内 + 国际短信一站式覆盖
- 编译为单一二进制文件,部署到 Docker / Kubernetes 极其简便
二、前期准备
1. 注册账号并创建应用
前往 SUBMAIL 注册,进入控制台「应用集成」→「国际短信」,创建应用,获取:
AppID:应用唯一标识AppKey:应用密钥(切勿提交至代码仓库)
2. 确认 Go 版本
go version # 需要 Go 1.18 及以上
三、核心 API 说明
- 接口地址:
https://api-v4.mysubmail.com/internationalsms/send - 请求方式:HTTP POST
- Content-Type:
multipart/form-data或application/x-www-form-urlencoded
主要请求参数:
appid(必需):国际短信应用 AppIDsignature(必需):normal模式填 AppKey 明文;md5模式填签名字符串to(必需):收件人手机号,必须携带国际区号,如+1xxxxxxxxxx(美国)、+44xxxxxxxxxx(英国)content(必需):短信正文内容sign_type(可选):normal(测试用)或md5(生产推荐)
四、完整 Go 示例代码
方式一:multipart/form-data + normal 认证(官方推荐方式)
// main.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
const (
API_URL = "https://api-v4.mysubmail.com/internationalsms/send"
)
// SMSResponse SUBMAIL API 响应结构
type SMSResponse struct {
Status string `json:"status"`
SendID string `json:"send_id"`
Fee int `json:"fee"`
SMSCredits int `json:"sms_credits"`
Code string `json:"code"`
Msg string `json:"msg"`
}
// sendInternationalSMS 发送国际短信(multipart/form-data + normal 明文认证)
//
// 参数:
// - to: 收件人号码,需带国际区号,如 +1xxxxxxxxxx
// - content: 短信正文
//
// 返回:SMSResponse 和 error
func sendInternationalSMS(to, content string) (*SMSResponse, error) {
appid := os.Getenv("SUBMAIL_APPID")
appkey := os.Getenv("SUBMAIL_APPKEY")
// 构建 postdata
postdata := map[string]string{
"appid": appid,
"signature": appkey, // normal 模式直接传 AppKey
"to": to,
"content": content,
"sign_type": "normal",
}
// 使用 mime/multipart 构建请求体(官方推荐方式)
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for key, val := range postdata {
if err := writer.WriteField(key, val); err != nil {
return nil, fmt.Errorf("写入字段失败 [%s]: %w", key, err)
}
}
contentType := writer.FormDataContentType()
writer.Close()
// 发起 HTTP POST 请求
resp, err := http.Post(API_URL, contentType, body)
if err != nil {
return nil, fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
// 读取并解析响应
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
var smsResp SMSResponse
if err := json.Unmarshal(result, &smsResp); err != nil {
return nil, fmt.Errorf("JSON 解析失败: %w", err)
}
return &smsResp, nil
}
// handleResponse 统一处理 API 响应
func handleResponse(resp *SMSResponse, context string) {
if resp.Status == "success" {
fmt.Printf("✅ %s 成功!\n", context)
fmt.Printf(" send_id: %s\n", resp.SendID)
fmt.Printf(" 消耗额度: %d\n", resp.Fee)
fmt.Printf(" 剩余额度: %d\n", resp.SMSCredits)
} else {
fmt.Printf("❌ %s 失败!\n", context)
fmt.Printf(" 错误码: %s\n", resp.Code)
fmt.Printf(" 原因: %s\n", resp.Msg)
}
}
func main() {
resp, err := sendInternationalSMS(
"+11234567890",
"Your verification code is 8866. Valid for 5 minutes. --SUBMAIL",
)
if err != nil {
fmt.Println("发送失败:", err)
return
}
handleResponse(resp, "发送国际短信")
}
方式二:x-www-form-urlencoded + normal 认证
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
)
// sendSMSFormURLEncoded 使用 x-www-form-urlencoded 发送国际短信
func sendSMSFormURLEncoded(to, content string) (*SMSResponse, error) {
appid := os.Getenv("SUBMAIL_APPID")
appkey := os.Getenv("SUBMAIL_APPKEY")
// 构建表单参数
formData := url.Values{}
formData.Set("appid", appid)
formData.Set("signature", appkey)
formData.Set("to", to)
formData.Set("content", content)
formData.Set("sign_type", "normal")
// 发起请求
req, err := http.NewRequest(
"POST",
"https://api-v4.mysubmail.com/internationalsms/send",
strings.NewReader(formData.Encode()),
)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var smsResp SMSResponse
if err := json.Unmarshal(body, &smsResp); err != nil {
return nil, fmt.Errorf("JSON 解析失败: %w", err)
}
return &smsResp, nil
}
方式三:MD5 签名认证(生产环境推荐)
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"sort"
"strings"
)
// buildMD5Signature 构建 SUBMAIL MD5 数字签名
//
// 签名规则:appid + appkey + 排序后参数字符串 + appid + appkey,取 MD5
func buildMD5Signature(params map[string]string, appid, appkey string) string {
// 过滤掉 sign_type 和 signature,收集所有 key
keys := make([]string, 0, len(params))
for k := range params {
if k != "sign_type" && k != "signature" {
keys = append(keys, k)
}
}
// 按字典序排序
sort.Strings(keys)
// 拼接参数字符串 key=value&key=value
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, k+"="+params[k])
}
paramStr := strings.Join(parts, "&")
// 拼接签名原文
raw := appid + appkey + paramStr + appid + appkey
// 计算 MD5
h := md5.New()
h.Write([]byte(raw))
return hex.EncodeToString(h.Sum(nil))
}
// sendSMSWithMD5 使用 MD5 签名认证发送国际短信(生产环境推荐)
func sendSMSWithMD5(to, content string) (*SMSResponse, error) {
appid := os.Getenv("SUBMAIL_APPID")
appkey := os.Getenv("SUBMAIL_APPKEY")
// 先组装核心参数(不含 signature 和 sign_type)
params := map[string]string{
"appid": appid,
"to": to,
"content": content,
}
// 生成 MD5 签名
signature := buildMD5Signature(params, appid, appkey)
// 加入签名和认证类型
params["signature"] = signature
params["sign_type"] = "md5"
// 构建 multipart 请求体
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for key, val := range params {
_ = writer.WriteField(key, val)
}
contentType := writer.FormDataContentType()
writer.Close()
resp, err := http.Post(
"https://api-v4.mysubmail.com/internationalsms/send",
contentType,
body,
)
if err != nil {
return nil, fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
result, _ := io.ReadAll(resp.Body)
var smsResp SMSResponse
if err := json.Unmarshal(result, &smsResp); err != nil {
return nil, fmt.Errorf("JSON 解析失败: %w", err)
}
return &smsResp, nil
}
func main() {
resp, err := sendSMSWithMD5(
"+447911123456", // 英国号码
"Hello! Your order has been shipped. --SUBMAIL",
)
if err != nil {
fmt.Println("发送失败:", err)
return
}
handleResponse(resp, "MD5 签名发送")
}
方式四:模板发送(internationalsms/xsend)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
// sendWithTemplate 使用模板发送国际短信(internationalsms/xsend)
//
// 参数:
// - to: 收件人号码(带国际区号)
// - projectID: 模板 ID(控制台中的 project 字段)
// - variables: 模板变量,如 map[string]string{"code": "8866", "minutes": "5"}
func sendWithTemplate(to, projectID string, variables map[string]string) (*SMSResponse, error) {
appid := os.Getenv("SUBMAIL_APPID")
appkey := os.Getenv("SUBMAIL_APPKEY")
// 将模板变量序列化为 JSON 字符串
varsJSON, err := json.Marshal(variables)
if err != nil {
return nil, fmt.Errorf("序列化模板变量失败: %w", err)
}
postdata := map[string]string{
"appid": appid,
"signature": appkey,
"to": to,
"project": projectID,
"vars": string(varsJSON),
"sign_type": "normal",
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for key, val := range postdata {
_ = writer.WriteField(key, val)
}
contentType := writer.FormDataContentType()
writer.Close()
resp, err := http.Post(
"https://api-v4.mysubmail.com/internationalsms/xsend",
contentType,
body,
)
if err != nil {
return nil, fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
result, _ := io.ReadAll(resp.Body)
var smsResp SMSResponse
_ = json.Unmarshal(result, &smsResp)
return &smsResp, nil
}
func main() {
// 模板示例:【SUBMAIL】你好 @var(name),你的验证码是 @var(code),@var(minutes) 分钟内有效
resp, err := sendWithTemplate(
"+8613812345678",
"your_template_id",
map[string]string{
"name": "张三",
"code": "9527",
"minutes": "5",
},
)
if err != nil {
fmt.Println("发送失败:", err)
return
}
handleResponse(resp, "模板发送")
}
五、封装为可复用的 SubMailClient
将所有功能封装为结构体,方便在大型项目中统一管理:
// submail/client.go
package submail
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"sort"
"strings"
)
const (
sendURL = "https://api-v4.mysubmail.com/internationalsms/send"
xsendURL = "https://api-v4.mysubmail.com/internationalsms/xsend"
)
// Client SUBMAIL 国际短信客户端
type Client struct {
AppID string
AppKey string
SignType string // "normal" 或 "md5"
client *http.Client
}
// SMSResponse API 响应结构
type SMSResponse struct {
Status string `json:"status"`
SendID string `json:"send_id"`
Fee int `json:"fee"`
SMSCredits int `json:"sms_credits"`
Code string `json:"code"`
Msg string `json:"msg"`
}
// New 创建新的 SUBMAIL 客户端
func New(appID, appKey, signType string) *Client {
return &Client{
AppID: appID,
AppKey: appKey,
SignType: signType,
client: &http.Client{},
}
}
// buildSignature 生成签名
func (c *Client) buildSignature(params map[string]string) string {
if c.SignType == "normal" {
return c.AppKey
}
// MD5 签名
keys := make([]string, 0)
for k := range params {
if k != "sign_type" && k != "signature" {
keys = append(keys, k)
}
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, k+"="+params[k])
}
raw := c.AppID + c.AppKey + strings.Join(parts, "&") + c.AppID + c.AppKey
h := md5.New()
h.Write([]byte(raw))
return hex.EncodeToString(h.Sum(nil))
}
// post 发起 multipart POST 请求
func (c *Client) post(apiURL string, params map[string]string) (*SMSResponse, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for key, val := range params {
_ = writer.WriteField(key, val)
}
contentType := writer.FormDataContentType()
writer.Close()
resp, err := c.client.Post(apiURL, contentType, body)
if err != nil {
return nil, fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
result, _ := io.ReadAll(resp.Body)
var smsResp SMSResponse
if err := json.Unmarshal(result, &smsResp); err != nil {
return nil, fmt.Errorf("JSON 解析失败: %w", err)
}
return &smsResp, nil
}
// Send 发送国际短信
func (c *Client) Send(to, content string) (*SMSResponse, error) {
params := map[string]string{
"appid": c.AppID,
"to": to,
"content": content,
}
params["signature"] = c.buildSignature(params)
params["sign_type"] = c.SignType
return c.post(sendURL, params)
}
// SendWithTemplate 使用模板发送国际短信
func (c *Client) SendWithTemplate(to, projectID string,
variables map[string]string) (*SMSResponse, error) {
varsJSON, err := json.Marshal(variables)
if err != nil {
return nil, fmt.Errorf("序列化变量失败: %w", err)
}
params := map[string]string{
"appid": c.AppID,
"to": to,
"project": projectID,
"vars": string(varsJSON),
}
params["signature"] = c.buildSignature(params)
params["sign_type"] = c.SignType
return c.post(xsendURL, params)
}
使用方式:
package main
import (
"fmt"
"os"
"your_project/submail"
)
func main() {
client := submail.New(
os.Getenv("SUBMAIL_APPID"),
os.Getenv("SUBMAIL_APPKEY"),
"md5", // 生产环境使用 MD5 签名
)
// 直接发送
resp, err := client.Send("+11234567890", "Your OTP is 8866. --SUBMAIL")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("状态: %s,send_id: %s\n", resp.Status, resp.SendID)
// 模板发送
resp2, _ := client.SendWithTemplate(
"+8613812345678",
"your_template_id",
map[string]string{"code": "9527", "minutes": "5"},
)
fmt.Printf("模板发送状态: %s\n", resp2.Status)
}
六、批量发送:goroutine 并发控制
利用 Go 的并发优势,高效批量发送短信:
package main
import (
"fmt"
"sync"
)
type SMSTask struct {
To string
Content string
}
// sendBulk 并发批量发送短信
// concurrency 控制最大并发数,避免超出 SUBMAIL QPS 限制
func sendBulk(client *SubMailClient, tasks []SMSTask, concurrency int) {
sem := make(chan struct{}, concurrency) // 信号量控制并发数
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t SMSTask) {
defer wg.Done()
sem <- struct{}{} // 占用一个并发槽
defer func() { <-sem }() // 释放并发槽
resp, err := client.Send(t.To, t.Content)
if err != nil {
fmt.Printf("❌ 发送失败 [%s]: %v\n", t.To, err)
return
}
if resp.Status == "success" {
fmt.Printf("✅ 发送成功 [%s],send_id: %s\n", t.To, resp.SendID)
} else {
fmt.Printf("❌ 发送失败 [%s],错误码: %s,原因: %s\n",
t.To, resp.Code, resp.Msg)
}
}(task)
}
wg.Wait()
fmt.Println("所有短信发送完毕!")
}
// 示例调用
func main() {
client := submail.New(
os.Getenv("SUBMAIL_APPID"),
os.Getenv("SUBMAIL_APPKEY"),
"md5",
)
tasks := []SMSTask{
{To: "+11234567890", Content: "Your code is 1111. --SUBMAIL"},
{To: "+447911123456", Content: "Your code is 2222. --SUBMAIL"},
{To: "+819012345678", Content: "Your code is 3333. --SUBMAIL"},
{To: "+8613812345678", Content: "你的验证码是 4444。--SUBMAIL"},
}
sendBulk(client, tasks, 5) // 最大 5 个并发
}
超过 100 条时,建议改用
internationalsms/multisend接口,单次请求支持最多 10,000 个号码,性能更优。
七、在 Gin 框架中集成
package main
import (
"fmt"
"math/rand"
"net/http"
"os"
"regexp"
"github.com/gin-gonic/gin"
"your_project/submail"
)
var smsClient *submail.Client
func init() {
smsClient = submail.New(
os.Getenv("SUBMAIL_APPID"),
os.Getenv("SUBMAIL_APPKEY"),
"md5",
)
}
// SendOTPRequest 发送 OTP 请求体
type SendOTPRequest struct {
Phone string `json:"phone" binding:"required"`
}
// sendOTPHandler 发送语音/短信验证码 Handler
func sendOTPHandler(c *gin.Context) {
var req SendOTPRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 验证手机号格式(需带国际区号)
matched, _ := regexp.MatchString(`^+\d{7,15}$`, req.Phone)
if !matched {
c.JSON(http.StatusBadRequest, gin.H{
"error": "手机号格式错误,需带国际区号,如 +1xxxxxxxxxx",
})
return
}
// 生成 6 位 OTP
otp := fmt.Sprintf("%06d", rand.Intn(1000000))
content := fmt.Sprintf(
"Your OTP is %s. Valid for 5 minutes. --SUBMAIL", otp,
)
resp, err := smsClient.Send(req.Phone, content)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if resp.Status == "success" {
// 实际项目中将 otp 存入 Redis,设置 5 分钟过期
// rdb.Set(ctx, "otp:"+req.Phone, otp, 5*time.Minute)
c.JSON(http.StatusOK, gin.H{"success": true, "send_id": resp.SendID})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": resp.Msg})
}
}
func main() {
r := gin.Default()
r.POST("/api/auth/send-otp", sendOTPHandler)
r.Run(":8080")
}
八、错误处理与常见错误码
成功响应示例:
{
"status": "success",
"send_id": "abcdef1234567890",
"fee": 1,
"sms_credits": 99
}
失败响应示例:
{
"status": "error",
"code": "103",
"msg": "Unauthorized"
}
常见错误码及解决方案:
103:AppID 或 AppKey 错误 → 检查环境变量是否正确设置104:签名验证失败 → 检查 MD5 签名,注意sort.Strings()字典序排序108:短信额度不足 → 前往控制台充值401:收件人号码格式错误 → 国际号码必须以+开头加区号402:内容违规 → 检查短信正文是否含敏感词
九、安全建议
🔐 绝对不要将 AppKey 硬编码在代码中,Go 项目推荐以下方式:
使用环境变量(推荐):
# Linux / macOS
export SUBMAIL_APPID="your_appid"
export SUBMAIL_APPKEY="your_appkey"
// Go 中安全读取
appid := os.Getenv("SUBMAIL_APPID")
appkey := os.Getenv("SUBMAIL_APPKEY")
if appid == "" || appkey == "" {
log.Fatal("SUBMAIL_APPID 和 SUBMAIL_APPKEY 环境变量未设置")
}
生产环境推荐方案:
- Docker / Kubernetes → 通过 Secret 注入环境变量
- 阿里云 → 使用 KMS 密钥管理服务
- AWS → 使用 AWS Secrets Manager
- 本地开发 → 使用
.env文件 +godotenv包
go get github.com/joho/godotenv
import "github.com/joho/godotenv"
func init() {
_ = godotenv.Load() // 加载 .env 文件
}
十、小结
- 注册账号:
mysubmail.com
- Go 版本要求:Go 1.18 及以上
- 零外部依赖:完全使用 Go 标准库(
net/http、crypto/md5、mime/multipart) - 发送接口:
https://api-v4.mysubmail.com/internationalsms/send - 模板接口:
https://api-v4.mysubmail.com/internationalsms/xsend - 号码格式:必须带
+国际区号 - 认证方式:测试用
normal,生产用md5 - 并发方案:
goroutine + sync.WaitGroup + 信号量实现高性能批量发送
Go 标准库的强大让接入 SUBMAIL SMS API 无需任何第三方依赖,核心发送逻辑不超过 40 行代码,从注册到发出第一条国际短信通常只需 20 分钟。
SUBMAIL 的 REST API 设计简洁,Go 接入门槛极低,通常 20 分钟内即可完成第一条国际短信的发送。结合 OTP 验证码、订单通知、营销触达等场景,可以大幅提升用户触达效率。
官方 Go 示例文档:global.mysubmail.com
社区 Go SDK:github.com
官方 API 文档:)en.mysubmail.com