添加企业微信
咨询电话: 4008-753-365
投诉电话: 18021097332

咨询时间与投诉时间为9:00-18:00
资源中心 /Go 调用 SUBMAIL SMS API 发送国际短信
          
2026-05-09 02:00:58
  65
  2

如何用 Go 调用 SUBMAIL SMS API 发送国际短信


一、为什么选择 Go + SUBMAIL?

Go 语言凭借其出色的并发模型、极低的内存占用和接近 C 的执行性能,已成为微服务和云原生场景的首选语言。结合 SUBMAIL SMS API(赛邮短信接口),Go 开发者可以:

  • 零第三方依赖接入:完全使用 Go 标准库(net/httpcrypto/md5mime/multipart),无需任何外部包
  • goroutine 天然支持高并发:利用 goroutine + sync.WaitGroup 实现高并发批量发送,性能远超同步方案
  • 覆盖全球 200+ 国家和地区,国内 + 国际短信一站式覆盖
  • 编译为单一二进制文件,部署到 Docker / Kubernetes 极其简便


二、前期准备

1. 注册账号并创建应用

前往 imgmysubmail.com 注册,进入控制台「应用集成」→「国际短信」,创建应用,获取:

  • AppID:应用唯一标识
  • AppKey:应用密钥(切勿提交至代码仓库)

2. 确认 Go 版本

go version   # 需要 Go 1.18 及以上


三、核心 API 说明

  • 接口地址https://api-v4.mysubmail.com/internationalsms/send
  • 请求方式:HTTP POST
  • Content-Typemultipart/form-dataapplication/x-www-form-urlencoded

主要请求参数:

  • appid(必需):国际短信应用 AppID
  • signature(必需):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 文件
}


十、小结

  • 注册账号imgmysubmail.com
  • Go 版本要求:Go 1.18 及以上
  • 零外部依赖:完全使用 Go 标准库(net/httpcrypto/md5mime/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 示例文档:imgglobal.mysubmail.com

社区 Go SDK:imggithub.com

官方 API 文档:imgen.mysubmail.com


了解更多:

SUBMAIL SMS API 国内

SUBMAIL SMS API 国际

申请测试



2
转化率
营销
EDM
短信文案
 推荐文章

Hello Friend!一触即发,开启云通信之旅!

欢迎使用 SUBMAIL 赛邮,点击下方进行注册,开启您的云通信之旅!

免费注册
一触即发,开启云通信之旅!
电话咨询
语音
上海赛邮云计算有限公司
致电:4008-753-365
您可以添加赛邮微信公众号、微博关注或联系我们
语音 微信公众号
语音 赛邮微博
SUBMAIL 赛邮·云通信    ©️ 2021 ALL RIGHTS RESERVED.
保留所有权利 |    开发者公约 | 使用协议 沪 ICP 备 16035411 号 -1