Go(Golang)调用 Twitter API:可运行的代码
在 Go 里获取 Twitter/X 数据,用 net/http 发送 HTTPS 请求,把 JSON 解码成类型化结构体,再按游标逐页翻下去。X 没有官方 Go SDK,而官方 X API 每读取一条帖子收费 $0.005。
简短回答: X 只为 Python 和 TypeScript 发布官方 SDK,所以 Go 代码要通过 net/http 或 gotwi 这样的社区库调用官方 X API,还需要开发者账号和预付额度:每条帖子 $0.005。读取公开数据,从 net/http 调用 Sorsa API 即可:一个密钥,每次搜索请求 20 条推文,每 1,000 条 $0.07 起;100 次免费请求能读取 2,000 条。
- 接口
POST /search-tweets- 每页条数
- 每次请求 20 条推文
- 价格
- 每 1,000 条推文 $0.07 起
#X 有官方的 Go SDK 吗?
没有。X 的开发者文档只列出了 Python 和 TypeScript 的官方 SDK,Go 被归入 X 不维护的社区库(见 X 的工具和库页面(在新标签页打开))。X 唯一的官方 Go 项目是 xurl,它是在终端里运行的命令行工具,不是可以 import 的包。
这样就只剩社区客户端,而且其中只有一个还在发布新版本。下面的日期来自各项目的 GitHub 历史:
| Go 客户端 | 最新版本 | 2026 年 11 月 2 日的状态 |
|---|---|---|
| gotwi | v0.18.5,2026 年 8 月 21 日 | 仍在维护;覆盖大部分 v2 接口,支持 OAuth 1.0a、bearer 令牌或 PKCE |
| go-twitter v2(g8rswimmer) | v2.1.5,2022 年 12 月 29 日 | 近四年没有发布新版本 |
| go-twitter(dghubble) | 2022 年已归档 | 为已下线的 v1.1 API 编写 |
gotwi
- 最新版本
- v0.18.5,2026 年 8 月 21 日
- 2026 年 11 月 2 日的状态
- 仍在维护;覆盖大部分 v2 接口,支持 OAuth 1.0a、bearer 令牌或 PKCE
go-twitter v2(g8rswimmer)
- 最新版本
- v2.1.5,2022 年 12 月 29 日
- 2026 年 11 月 2 日的状态
- 近四年没有发布新版本
go-twitter(dghubble)
- 最新版本
- 2022 年已归档
- 2026 年 11 月 2 日的状态
- 为已下线的 v1.1 API 编写
库只负责组装请求。每次调用仍然会消耗 X 的预付额度:每条帖子 $0.005,每个用户 $0.010,每月最多读取 300 万条帖子(见 X 的价目表(在新标签页打开))。从 2026 年 11 月 22 日起,通过 expansions 返回的作者和媒体按同样的费率计费(见 X 在 10 月 23 日的通知(在新标签页打开)),所以大多数 Go 代码发出的那种搜索,费用最高是帖子价格的三倍:
q := url.Values{
"query": {"golang lang:en"},
"max_results": {"100"},
"expansions": {"author_id"},
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.x.com/2/tweets/search/recent?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("X_BEARER_TOKEN"))
// 100 posts: $0.50. From Nov 22, 2026, with authors: up to $1.50
走官方路线,第一次调用之前需要什么
- 在 console.x.com 用一个 X 账号开通开发者账号,并接受 X 的《开发者协议》。
- 一个应用,以及它的 bearer 令牌,X 只会显示一次。
- 提前购买的额度,响应返回的每条帖子和每个用户都会消耗它。
- 凡是发帖、点赞或关注,都需要 OAuth 用户令牌。
读取公开的帖子、用户资料和粉丝,这些都不需要。Tweepy 和 xdk 的同样取舍,写在 Python 调用 Twitter API 指南里;本页其余部分用 Go 来完成这项工作。
#用一个 API 密钥在 Go 里获取推文
要在 Go 里读取 X 数据,用 Sorsa API,一款 Twitter(X)API 与爬虫:用一个 ApiKey 请求头取代 OAuth,所有套餐(包括免费账号)统一每秒 20 次请求,搜索可回溯到 2006 年,搜索结果每 1,000 条推文 $0.07 起,每条推文里都带有作者的完整资料。标准库就是全部客户端:net/http 发送请求,encoding/json 读取回复,所以 go.mod 不会多出任何依赖。Sorsa API 从不要求你的 X 登录信息,所以你的 X 账号没有风险。
-
获取密钥。 注册后,从控制台复制 API 密钥。前 100 次请求免费,无需绑卡,永不过期。
-
创建模块并保存密钥。 把密钥放在环境变量里,只在服务端代码中调用 API,不要放在浏览器或交给用户的应用里。本指南的代码需要 Go 1.25 或更新的版本。
Terminal go mod init example.com/xdata export SORSA_API_KEY="your_key" # PowerShell: $env:SORSA_API_KEY="your_key" -
搜索推文。 把 X 搜索语法的查询发给
POST /search-tweets(在新标签页打开),密钥放在ApiKey请求头里,然后运行go run .。package main import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os" "time" ) func main() { key := os.Getenv("SORSA_API_KEY") // or "YOUR_API_KEY" to try it body, _ := json.Marshal(map[string]string{ "query": "golang lang:en", "order": "latest", }) req, err := http.NewRequest(http.MethodPost, "https://api.sorsa.io/v3/search-tweets", bytes.NewReader(body)) if err != nil { log.Fatal(err) } req.Header.Set("ApiKey", key) req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Fatalf("sorsa: %s", resp.Status) } var page struct { Tweets []struct { ID string `json:"id"` FullText string `json:"full_text"` User struct { Username string `json:"username"` } `json:"user"` } `json:"tweets"` NextCursor string `json:"next_cursor"` } if err := json.NewDecoder(resp.Body).Decode(&page); err != nil { log.Fatal(err) } for _, t := range page.Tweets { fmt.Println(t.User.Username, t.FullText) } } -
读取这一页。 每个响应最多含 20 条推文,以及用于下一页的
next_cursor。下面的值是按文档结构编写的演示数据。Response200 OK { "tweets": [ { "id": "1985611093417201664", "full_text": "Rewrote our ingest worker with range-over-func iterators.", "created_at": "2026-11-01T21:14:52Z", "lang": "en", "likes_count": 57, "retweet_count": 9, "reply_count": 4, "quote_count": 2, "view_count": 4120, "bookmark_count": 11, "is_reply": false, "quoted_status": null, "user": { "id": "1601739518260654080", "username": "gopher_ingest_notes", "display_name": "Gopher Ingest Notes", "created_at": "2022-12-11T09:03:18Z", "followers_count": 2310 } } ], "next_cursor": "DAABCgABF7Zq0xLwAAA" }
每条推文都带着互动数据和作者一起返回,所以请求里不用设置 expansions 或字段列表,而且无论返回什么,每次请求的价格都一样。
#用类型化结构体建模响应
为代码要读的字段定义一个 Tweet 和一个 User 结构体,凡是返回推文或用户的 Sorsa API 接口,都能解码到它们里面,因为 API 在所有地方都使用这两种结构。把它们放进一个小包里(这里叫 sorsa),和下一节的客户端放在一起。
package sorsa
import "time"
type Tweet struct {
ID string `json:"id"`
FullText string `json:"full_text"`
CreatedAt time.Time `json:"created_at"`
Lang string `json:"lang"`
LikesCount int `json:"likes_count"`
RetweetCount int `json:"retweet_count"`
ReplyCount int `json:"reply_count"`
QuoteCount int `json:"quote_count"`
ViewCount int `json:"view_count"`
IsReply bool `json:"is_reply"`
User User `json:"user"`
QuotedStatus *Tweet `json:"quoted_status"`
RetweetedStatus *Tweet `json:"retweeted_status"`
}
type User struct {
ID string `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
FollowersCount int `json:"followers_count"`
FollowingsCount int `json:"followings_count"`
TweetsCount int `json:"tweets_count"`
Verified bool `json:"verified"`
}
// TweetsPage and UsersPage are the two list shapes. NextCursor
// is empty on the last page.
type TweetsPage struct {
Tweets []Tweet `json:"tweets"`
NextCursor string `json:"next_cursor"`
}
type UsersPage struct {
Users []User `json:"users"`
NextCursor string `json:"next_cursor"`
}
这些结构体里有三个选择比其他的更重要。ID 有 19 位数字,以 JSON 字符串形式返回,所以要解码成 string:如果用 int64 字段,解码器会报 "cannot unmarshal string into Go struct field" 而中断。日期以 ISO 8601 UTC 格式返回,time.Time 不需要自定义代码就能读取。被引用或被转发的推文,是推文里面的推文,所以需要用指针:Go 不允许结构体按值包含它自己,而当这条帖子没有引用任何内容时,指针保持为 nil。
在 Go 里,标签对不上任何 JSON 键不算错误:字段只是保持零值。从官方 X API 迁移过来的代码最先暴露这一点,因为 X 把计数命名为 public_metrics 里的 like_count,而 Sorsa API 直接在推文上返回 likes_count。在下面选择你需要的字段,复制一个标签与 API 参考一致的结构体。
import "time"
type Tweet struct {
ID string `json:"id"`
FullText string `json:"full_text"`
CreatedAt time.Time `json:"created_at"`
LikesCount int `json:"likes_count"`
ViewCount int `json:"view_count"`
User User `json:"user"`
}
type User struct {
ID string `json:"id"`
Username string `json:"username"`
FollowersCount int `json:"followers_count"`
}
标签和类型与 Sorsa API 在线 OpenAPI 参考文档一致;未勾选的字段会被 encoding/json 直接跳过。
#构建带重试和限速器的客户端
把密钥、HTTP 客户端、重试规则和速率限速器放进同一个 Client,并在所有 goroutine 之间共享这一个值:http.Client 并发使用是安全的,控制起始节奏的 ticker(定时器)也是。后面的每个例子都调用它的 Do 方法。
package sorsa
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const baseURL = "https://api.sorsa.io/v3"
// Client is safe for concurrent use: share one per API key.
type Client struct {
key string
http *http.Client
pace *time.Ticker
}
// New paces requests at 19 starts a second, one under the limit.
// Pass the key from os.Getenv("SORSA_API_KEY"), or "YOUR_API_KEY".
func New(key string) *Client {
return &Client{
key: key,
http: &http.Client{Timeout: 30 * time.Second},
pace: time.NewTicker(time.Second / 19),
}
}
// APIError is a reply other than 200, with the API's message.
type APIError struct {
Status int `json:"-"`
Message string `json:"message"`
}
func (e *APIError) Error() string {
return fmt.Sprintf("sorsa: %d %s", e.Status, e.Message)
}
// Do sends one request and decodes the reply into out. It retries
// 429 and 5xx up to 4 times, after 1, 2, 4 and 8 seconds.
func (c *Client) Do(ctx context.Context, method, path string,
query url.Values, body, out any) error {
var payload []byte
if body != nil {
var err error
if payload, err = json.Marshal(body); err != nil {
return err
}
}
u := baseURL + path
if len(query) > 0 {
u += "?" + query.Encode()
}
for attempt := 0; ; attempt++ {
select { // wait for a start slot
case <-c.pace.C:
case <-ctx.Done():
return ctx.Err()
}
req, err := http.NewRequestWithContext(ctx, method, u,
bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("ApiKey", c.key)
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusOK {
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(out)
}
apiErr := &APIError{Status: resp.StatusCode}
_ = json.NewDecoder(io.LimitReader(resp.Body, 4096)).Decode(apiErr)
resp.Body.Close()
retry := resp.StatusCode == http.StatusTooManyRequests ||
resp.StatusCode >= 500
if !retry || attempt == 4 {
return apiErr
}
select {
case <-time.After(time.Second << attempt):
case <-ctx.Done():
return ctx.Err()
}
}
}
429 表示你的密钥在一秒内发起了超过 20 次请求,5xx 是服务端的临时故障,两者都值得重试。401 或 403 则不值得:说明密钥错误或缺失,或者请求次数已用完,Message 会告诉你是哪一种。失败的请求从不计费,所以重试不花钱;而且 Sorsa API 的每个接口都只读,所以重试 /search-tweets 这样的 POST 请求,不会创建出重复的东西。
ticker 每 1/19 秒放出一次起始机会,每次尝试(包括重试)都要等一次。按每秒 19 次,限速器留出了一次的余量,所以程序空闲时攒下的 tick,永远不会让某一秒超过 Sorsa API 每秒 20 次的上限。context 会同时结束这两处等待,所以被取消的任务不会再发出排队中的请求。调用方用 errors.As 区分错误:
var apiErr *sorsa.APIError
if errors.As(err, &apiErr) && apiErr.Status == http.StatusForbidden {
log.Fatal("check the key or its remaining requests: ", apiErr.Message)
}
#用迭代器遍历翻页结果
游标是指向下一页结果的不透明键:把 next_cursor 原样发回去,当响应不再带它时停止。搜索和时间线每页最多返回 20 条推文,一页的条数可能更少而后面仍有结果,所以条数永远无法告诉你是否已经到末尾。自 Go 1.23 起,函数可以成为 for range 循环遍历的对象,这样整个游标循环就能放进一个方法里:
package sorsa
import (
"context"
"iter"
"net/http"
"net/url"
)
// SearchPages yields pages of tweets until the cursor runs out
// or maxPages pages have been read.
func (c *Client) SearchPages(ctx context.Context, query string,
maxPages int) iter.Seq2[[]Tweet, error] {
return func(yield func([]Tweet, error) bool) {
body := map[string]string{"query": query, "order": "latest"}
for range maxPages {
var page TweetsPage
err := c.Do(ctx, http.MethodPost, "/search-tweets",
nil, body, &page)
if !yield(page.Tweets, err) || err != nil {
return
}
next := page.NextCursor
if next == "" || next == body["next_cursor"] {
return
}
body["next_cursor"] = next
}
}
}
// Followers yields an account's followers, up to 200 a page.
func (c *Client) Followers(ctx context.Context, username string,
maxPages int) iter.Seq2[[]User, error] {
return func(yield func([]User, error) bool) {
q := url.Values{"username": {username}}
for range maxPages {
var page UsersPage
err := c.Do(ctx, http.MethodGet, "/followers",
q, nil, &page)
if !yield(page.Users, err) || err != nil {
return
}
next := page.NextCursor
if next == "" || next == q.Get("next_cursor") {
return
}
q.Set("next_cursor", next)
}
}
}
调用方读取页面的方式,和读取任何切片一样,而 break 会立刻停止请求:
client := sorsa.New(os.Getenv("SORSA_API_KEY"))
ctx := context.Background()
for tweets, err := range client.SearchPages(ctx, "golang lang:en", 50) {
if err != nil {
log.Fatal(err)
}
for _, t := range tweets {
fmt.Println(t.ID, t.CreatedAt.Format(time.DateOnly), t.User.Username)
}
}
POST 接口把游标放在 JSON 请求体里,GET 接口把它作为查询参数,这是两种方法唯一的区别。GET /followers(在新标签页打开)每页返回最多 200 份用户资料,所以 100 万个粉丝的列表大约需要 5,000 次请求;指南导出 Twitter 粉丝演示了如何把这样的列表写入 CSV。把 maxPages 当作预算,这样范围宽泛的查询花的钱永远不会超出计划;沿着同一个游标翻页时,保持查询不变。检查游标是否重复,是一道成本很低的保险,可以结束那种本来会把整个预算耗在同一页上的循环。
#每次请求获取 100 条推文或用户资料
当你已经有 ID 或用户名时,就分批发送。POST /tweet-info-bulk(在新标签页打开)每次请求最多接收 100 个推文 ID 或链接,GET /info-batch(在新标签页打开)每次请求最多接收 100 个用户名。slices.Chunk 会帮你切分列表:
var tweets []sorsa.Tweet
for batch := range slices.Chunk(tweetIDs, 100) {
var page sorsa.TweetsPage
body := map[string][]string{"tweet_links": batch}
err := client.Do(ctx, http.MethodPost, "/tweet-info-bulk",
nil, body, &page)
if err != nil {
return err
}
tweets = append(tweets, page.Tweets...)
}
var users []sorsa.User
for batch := range slices.Chunk(usernames, 100) {
var page sorsa.UsersPage
q := url.Values{"usernames": batch} // usernames=a&usernames=b
err := client.Do(ctx, http.MethodGet, "/info-batch", q, nil, &page)
if err != nil {
return err
}
users = append(users, page.Users...)
}
无论返回什么,每次请求的价格都一样,所以一批 100 条推文的花费,和一条推文一样:每 1,000 条推文 $0.014 起,比官方 X API 的 $5.00 便宜 250 倍以上。url.Values 是字符串切片的 map,所以一个批次可以原样放进去,并以 GET /info-batch 读取的重复参数形式发出。批量响应没有游标:一个响应就包含整批内容,放在 tweets 或 users 下面。
#在每秒 20 次请求内运行 goroutine
Sorsa API 在所有套餐上允许每个密钥每秒 20 次请求,当工作可以拆成互相独立的游标链时,Go 程序用一小批 goroutine 就能达到这个速率。单条游标链无法拆分,因为每一页都需要上一页的游标。所以让每个 goroutine 各负责完整的链,一次处理一个账号或一天的搜索,并让共享的客户端给它们统一限速:
package sorsa
import (
"context"
"errors"
"fmt"
"sync"
)
// Collect runs every query through a pool of workers. They share
// one Client, so its pacer caps the key's total rate.
func Collect(ctx context.Context, c *Client, queries []string,
workers int) ([]Tweet, error) {
jobs := make(chan string)
var (
mu sync.Mutex
all []Tweet
errs []error
wg sync.WaitGroup
)
for range workers {
wg.Go(func() {
for q := range jobs {
for tweets, err := range c.SearchPages(ctx, q, 100) {
mu.Lock()
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", q, err))
} else {
all = append(all, tweets...)
}
mu.Unlock()
}
}
})
}
for _, q := range queries {
jobs <- q
}
close(jobs)
wg.Wait()
return all, errors.Join(errs...)
}
用 since: 和 until: 运算符把一次很长的搜索拆成按天的片段,再把这些天交给工作池:
start := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
var days []string
for d := range 31 { // October 1 to November 1
day := start.AddDate(0, 0, d)
days = append(days, fmt.Sprintf("golang lang:en since:%s until:%s",
day.Format(time.DateOnly), day.AddDate(0, 0, 1).Format(time.DateOnly)))
}
tweets, err := sorsa.Collect(ctx, client, days, 20)
20 个 worker 绰绰有余:它们大部分时间都在等下一个 tick,worker 再多也只是等得更久。Go 1.25 新增的 wg.Go,一次调用就能启动 goroutine 并计数。每个密钥、每个进程只保留一个 Client:两个进程各有一个客户端,合起来可能以两倍的上限发起请求,所以给每个进程配各自的密钥,或各取一半的速率。按每秒 20 次请求,批量读取每秒最多可处理 2,000 条推文,翻页读取粉丝可处理 4,000 份用户资料,搜索页可处理 400 条推文。
须知
某一天失败时,
Collect会继续运行:它把错误连同引发错误的查询一起保存,最后通过errors.Join全部返回。要在第一次失败时就停掉整个任务,用context.WithCancel包装 context,并在保存错误的地方调用cancel();context 一结束,限速器和重试等待都会立刻返回。
#一个 Go 任务要花多少钱
Sorsa API 按请求计费,不管接口返回什么:按每批 100 条获取推文,每 1,000 条 $0.014 起,搜索结果每 1,000 条 $0.07 起,粉丝每 1,000 个 $0.007 起,失败的请求从不计费。官方 X API 则按对象收费:每 1,000 条帖子 $5.00,从 2026 年 11 月 22 日起,如果请求展开每条帖子的作者,每 1,000 条最高 $15.00。
官方 X API
$5,000
读取 100 万 条推文
每条帖子 $0.005
Sorsa API
$14 起
读取 100 万 条推文
每 1,000 条 $0.014 起
便宜 250 倍以上
每 1,000 次请求的价格,从 Starter 的 $4.90 到 Hyperscale 的 $1.40,套餐对所有接口一视同仁。服务可以免费监控自己的余额:GET /key-usage-info(在新标签页打开)不占用你的请求次数。
var usage struct {
Remaining int `json:"remaining_requests"`
ValidUntil time.Time `json:"valid_until"`
}
err := client.Do(ctx, http.MethodGet, "/key-usage-info", nil, nil, &usage)
if err == nil {
fmt.Println(usage.Remaining, "left until", usage.ValidUntil)
}
#把 gotwi 或 go-twitter 代码迁移到 Sorsa API
gotwi 和 go-twitter 只是官方 X API v2 路径外面的薄薄一层,所以它们的调用可以按路径对应到 Sorsa API。下面每条路径都有一个对应的 Sorsa API 接口,而且代码会更短:不用 expansions,也不用字段列表,因为每条推文本来就带着作者和互动数据。
| 官方 X API v2 路径 | Sorsa API 接口 | 有什么变化 |
|---|---|---|
GET /2/tweets/search/recent | POST /search-tweets | 可回溯到 2006 年的任意日期,不只是最近 7 天 |
GET /2/tweets/search/all | POST /search-tweets | 同一个接口,加上 since: 和 until: |
GET /2/users/:id/tweets | POST /user-tweets(在新标签页打开) | 只需用户名;没有 3,200 条帖子的上限 |
GET /2/tweets | POST /tweet-info-bulk | 每次请求 100 个 ID 或链接 |
GET /2/users/by | GET /info-batch | 每次请求 100 个用户名 |
GET /2/users/:id/followers | GET /followers | 每页 200 个粉丝 |
GET /2/tweets/:id/retweeted_by | POST /retweeters(在新标签页打开) | 同样的游标循环 |
POST /2/tweets | 无 | 只读:发帖请通过官方 X API |
GET /2/tweets/search/recent- Sorsa API 接口
POST /search-tweets- 有什么变化
- 可回溯到 2006 年的任意日期,不只是最近 7 天
GET /2/tweets/search/all- Sorsa API 接口
POST /search-tweets- 有什么变化
- 同一个接口,加上
since:和until:
GET /2/users/:id/tweets- Sorsa API 接口
POST /user-tweets(在新标签页打开)- 有什么变化
- 只需用户名;没有 3,200 条帖子的上限
GET /2/tweets- Sorsa API 接口
POST /tweet-info-bulk- 有什么变化
- 每次请求 100 个 ID 或链接
GET /2/users/by- Sorsa API 接口
GET /info-batch- 有什么变化
- 每次请求 100 个用户名
GET /2/users/:id/followers- Sorsa API 接口
GET /followers- 有什么变化
- 每页 200 个粉丝
GET /2/tweets/:id/retweeted_by- Sorsa API 接口
POST /retweeters(在新标签页打开)- 有什么变化
- 同样的游标循环
POST /2/tweets- Sorsa API 接口
- 无
- 有什么变化
- 只读:发帖请通过官方 X API
字段名随路径一起变化:text 变成 full_text,public_metrics.like_count 变成 likes_count,author_id 加 includes.users 变成嵌套的 user,meta.next_token 变成 next_cursor。速率限制的处理也更简单,因为 X 的 15 分钟窗口换成了每秒重置一次的单一限制。要迁移整个代码库,v2 的每个接口和响应字段,都在指南从 Twitter/X API 迁移里一一对应好了。
#今天就在 Go 里跑通第一次搜索
把 X 数据读进 Go,只需要标准库、一个密钥和四个部件:类型化结构体、带重试和限速器的 Client、遍历游标的迭代器,以及应对多条链的工作池。基于 Sorsa API,也就是大规模读取 X 公开数据的 Twitter API 来构建:每次搜索请求 20 条推文,每 1,000 条 $0.07 起,每批 100 条 $0.014 起,所有套餐统一每秒 20 次请求,另有 100 次免费请求,无需绑卡,永不过期。其他语言的同样调用,见指南 Node.js 调用 Twitter API 和 PHP 调用 Twitter API。
#常见问题
有给 Go 用的免费 Twitter API 吗?
官方 X API 对新开发者没有免费额度:gotwi 和其他 Go 客户端本身免费,但每次读取都要消耗预付额度,每条帖子 $0.005。Sorsa API 给每个新账号 100 次免费请求,永不过期,无需绑卡。用 Go 调用,它们可以通过搜索收集 2,000 条推文,或者按每批 100 条读取最多 1 万条推文。
在 Go 里该用哪个 Twitter API?
在 Go 里读取 X 数据,用 Sorsa API:标准库的 net/http 加一个密钥取代 OAuth,搜索结果每 1,000 条推文 $0.07 起,批量读取 $0.014 起,所有套餐统一每秒 20 次请求,供 goroutine 共享,搜索可回溯到 2006 年,而且不需要 X 登录信息,所以你的账号永远不会有风险。要同时发帖,就搭配官方 X API 使用。
不用开发者账号,能在 Go 里获取 X 数据吗?
可以。Sorsa API 只需要它自己的密钥:注册,设置 ApiKey 请求头,就能在 Go 里调用它的 45 个接口中的任意一个,不需要 X 开发者账号、应用审核或 OAuth。那些读取 X 网站的 Go 库,则要用账号的 Cookie 或账号池登录,这会让这些账号面临风险。各种方案的对比,见不用开发者账号使用 Twitter API。
能用 Sorsa API 在 Go 里发推文吗?
不能。Sorsa API 从设计上就是只读的:它读取公开的帖子、用户资料和粉丝,从不发帖、点赞、关注或发送消息,所以一个密钥无法对任何 X 账号执行操作。要在 Go 里发帖,直接或通过 gotwi 调用官方 X API 的 POST /2/tweets,使用 OAuth 用户令牌,每条帖子 $0.015,读取则继续用 Sorsa API。
Sorsa API 有 Go SDK 或模块吗?
不需要模块:Sorsa API 是普通的 REST 加 JSON,本指南里的客户端用标准库写成,大约 100 行。线上的 OpenAPI 参考 api.sorsa.io/v3/swagger.json 列出了全部 45 个接口及其字段,所以如果你更想要生成的类型,可以用 oapi-codegen 这样的生成器产出带类型的请求和响应代码。
#我们如何核实
本指南中的每个请求,都依据 2026 年 11 月 2 日读取的 Sorsa API 线上 OpenAPI 参考(45 个接口)及其速率限制与分页文档编写;每个 Go 语法结构都参照标准库文档和发布说明(迭代器和 slices.Chunk 来自 Go 1.23,WaitGroup.Go 来自 Go 1.25;当前版本是 Go 1.27)。这次我们的机器上没有 Sorsa API 密钥,也没有 Go 工具链,所以响应显示的是按文档结构编写的演示值,并且我们对照这些文档逐行检查了每个代码块,而没有实际运行。同一天,我们读了官方 X API 的价目表、X 关于 expansions 计费的通知、docs.x.com 上官方 SDK 与社区库的清单,以及 GitHub 上 gotwi、两个 go-twitter 项目和 xurl 的发布历史。
来源
- X API pay-per-usage pricing (docs.x.com)(在新标签页打开)
- X API PPU: expansions update (X Developers, October 23, 2026)(在新标签页打开)
- Tools and libraries: official SDKs and community libraries (docs.x.com)(在新标签页打开)
- gotwi releases (GitHub)(在新标签页打开)
- Go 1.23 release notes: range-over-func iterators(在新标签页打开)
- Go 1.25 release notes: WaitGroup.Go(在新标签页打开)
- Sorsa API reference, OpenAPI
- Sorsa API rate limits(在新标签页打开)
- Sorsa API pagination(在新标签页打开)