一、为什么用 Go 写微信机器人

做微信机器人时,最常见的语言选择是 Python 或 Java,但 Go 在这类场景里其实有明显优势:

  • 单二进制部署:编译出一个可执行文件,扔到服务器上就能跑,不用装运行时
  • goroutine 天然适合消息并发:Webhook 回调接收和消息发送天然分成两条协程,靠 channel 解耦
  • 内存占用低:常驻服务挂一天,内存也就几十 MB

这篇文章不聊原理,直接从扫码登录讲到回调接收、再到消息发送,所有接口以实际可调用为准(接口细节以官方文档 https://weiti.apifox.cn 为准)。

二、整体架构

一个最小可用的 Go 版微信机器人,只需要三个模块:

Webhook接收器(HTTP Server) ──► 消息队列(channel) ──► 发送消费者(goroutine)
        │                                                    │
        └── 收到消息立刻返回 {"ret": 200}          按队列串行调用发送接口

关键点只有一个:回调处理和消息发送必须分开。回调接口要快速响应,发送逻辑放进独立的消费者协程里慢慢跑。

三、获取登录二维码

首次登录时 appId 传空,服务端会分配一个新的应用 ID;regionId 填账号常用的省份代码(比如 440000 是广东)。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

const (
	baseURL = "https://wx.chuapi.com"
	token   = "你的X-finder-TOKEN"
)

// 通用 POST 请求
func post(path string, body map[string]interface{}) (map[string]interface{}, error) {
	jsonBytes, _ := json.Marshal(body)
	req, err := http.NewRequest("POST", baseURL+path, bytes.NewBuffer(jsonBytes))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-finder-TOKEN", token)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}
	return result, nil
}

// 获取登录二维码
func getLoginQrCode(appId, regionId string) map[string]interface{} {
	body := map[string]interface{}{
		"appId":    appId, // 首次登录传空
		"regionId": regionId,
	}
	return post("/finder/v2/api/login/getLoginQrCode", body)
}

func main() {
	result := getLoginQrCode("", "440000")
	data := result["data"].(map[string]interface{})
	fmt.Println("appId:", data["appId"])   // 记下来,之后所有请求都要用
	fmt.Println("uuid:", data["uuid"])
	fmt.Println("二维码地址:", data["qrImgUrl"])
}

返回的 data 里会带上 appId 和 uuid,用手机扫码即可。appId 一定要持久化保存,下次重登必须传同一个,否则会被当成新设备。

四、轮询确认登录

扫码后轮询 checkLogin,status 为 2 表示登录成功:

// 确认登录状态
func checkLogin(appId, uuid string) map[string]interface{} {
	body := map[string]interface{}{
		"appId":       appId,
		"uuid":        uuid,
		"autoSliding": true, // 遇到滑块验证自动处理
	}
	return post("/finder/v2/api/login/checkLogin", body)
}

ret 为 200、status 为 2 时登录完成,可以开始接收消息了。

五、接收 Webhook 回调

登录成功后配置回调地址,消息会以 POST 方式推送过来。回调体里常用的字段有 appId、msgType、fromUser、nickName、content、createTime、chatRoomId。

// 消息结构体,只解析需要的字段
type WxMessage struct {
	AppId      string `json:"appId"`
	MsgType    int    `json:"msgType"`
	FromUser   string `json:"fromUser"`
	NickName   string `json:"nickName"`
	Content    string `json:"content"`
	ChatRoomId string `json:"chatRoomId"`
	CreateTime int64  `json:"createTime"`
}

// 回调入口
func callbackHandler(msgCh chan<- WxMessage) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var msg WxMessage
		if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
			w.Write([]byte(`{"ret": 200}`))
			return
		}
		// 群消息时 chatRoomId 有值,私聊时 fromUser 就是发送人
		select {
		case msgCh <- msg:
		default:
			// 队列满了就丢弃,绝不阻塞回调响应
		}
		// 必须返回这个格式,否则平台会重推
		w.Write([]byte(`{"ret": 200}`))
	}
}

注意两点:

  1. 必须返回 {"ret": 200},返回别的格式平台会认为推送失败并重发
  2. 回调里不做任何耗时操作,消息塞进 channel 立刻返回

六、发送消息的消费者

用 goroutine 从 channel 里取消息,串行调用发送接口,中间加随机间隔模拟真人的打字节奏:

// 发送文本消息
func sendText(appId, toWxid, content string) map[string]interface{} {
	body := map[string]interface{}{
		"appId":   appId,
		"toWxid":  toWxid,
		"content": content,
	}
	return post("/finder/v2/api/message/postText", body)
}

// 消费者:串行发送,控制节奏
func sender(appId string, msgCh <-chan WxMessage) {
	for msg := range msgCh {
		toWxid := msg.FromUser
		if msg.ChatRoomId != "" {
			toWxid = msg.ChatRoomId // 群消息回复到群
		}

		reply := "收到,正在处理:" + msg.Content
		result := sendText(appId, toWxid, reply)
		if result["ret"].(float64) == 200 {
			fmt.Printf("已回复 %s\n", toWxid)
		}

		// 随机休眠 1~5 秒,避免高频触发风控
		time.Sleep(time.Duration(1000+rand.Intn(4000)) * time.Millisecond)
	}
}

ret 为 200 表示发送成功。接口调用一旦返回非 200,别立刻重试,先记日志排查。

七、组装启动

把三个模块串起来:

func main() {
	rand.Seed(time.Now().UnixNano())

	appId := "从数据库/文件读取的appId"

	msgCh := make(chan WxMessage, 1000)

	// 后台消费者
	go sender(appId, msgCh)

	http.HandleFunc("/callback", callbackHandler(msgCh))
	fmt.Println("服务启动,监听 :8080")
	http.ListenAndServe(":8080", nil)
}

本地联调时回调地址需要公网可达,用 ngrok 做内网穿透即可:

ngrok http 8080
# 把生成的 https 地址 + /callback 配到平台后台

八、几个必须知道的坑

  1. appId 是账号身份:掉线重登必须传同一个 appId,换了就会被识别为新设备登录
  2. 新号第一晚容易掉线:这是正常现象,重新走一遍 getLoginQrCode → checkLogin 即可,稳定期一般在 15 天之后
  3. 频率控制:一分钟内发送不要超过 40 条,用上面消费者里的随机休眠就能压住
  4. regionId 选真实地区:填账号所在地对应的省份代码,异地登录会增加风控风险
  5. channel 缓冲要给够:消息高峰期回调会瞬间涌进来,缓冲太小会被 default 分支丢掉

九、Go 版的优势总结

环节实现方式
回调接收net/http 原生 Server,无第三方依赖
解耦带 1000 缓冲的 channel
发送节流单消费者 + 随机休眠
部署编译单文件,交叉编译一行命令

整套代码没有引入任何重框架,标准库就够用,这也是 Go 接这类 API 的典型做法。如果你不想自己处理协议层,直接对接现成的 HTTP API(比如 WTAPI 这类基于 HTTP + Webhook 的方案)再套上面的模板,半天内就能跑通一个稳定的自动回复机器人。

参考资料

接口定义与参数说明文档:weiti.apifox.cn


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐