├── README.md
├── authenticator.go
├── constants.go
├── contrib
└── get_statuses.go
├── examples
├── auth.go
├── goroutines.go
├── sample.jpg
└── weibo.go
├── license.txt
├── structs.go
└── weibo.go
/README.md:
--------------------------------------------------------------------------------
1 | gobo
2 | ====
3 |
4 | 新浪微博Go语言SDK,支持所有微博API功能
5 |
6 | # 安装/更新
7 |
8 | ```
9 | go get -u github.com/huichen/gobo
10 | ```
11 |
12 | # 使用
13 |
14 | 抓取@人民日报的最近10条微博:
15 |
16 | ```go
17 | package main
18 |
19 | import (
20 | "flag"
21 | "fmt"
22 | "github.com/huichen/gobo"
23 | )
24 |
25 | var (
26 | weibo = gobo.Weibo{}
27 | access_token = flag.String("access_token", "", "用户的访问令牌")
28 | )
29 |
30 | func main() {
31 | // 解析命令行参数
32 | flag.Parse()
33 |
34 | // 调用API
35 | var statuses gobo.Statuses
36 | params := gobo.Params{"screen_name": "人民日报", "count": 10}
37 | err := weibo.Call("statuses/user_timeline", "get", *access_token, params, &statuses)
38 |
39 | // 处理返回结果
40 | if err != nil {
41 | fmt.Println(err)
42 | return
43 | }
44 | for _, status := range statuses.Statuses {
45 | fmt.Println(status.Text)
46 | }
47 | }
48 | ```
49 |
50 | 用命令行参数-access_token传入访问令牌,令牌可以通过API测试工具或者gobo.Authenticator得到。
51 |
52 | 更多API调用的例子见 examples/weibo.go。
53 |
--------------------------------------------------------------------------------
/authenticator.go:
--------------------------------------------------------------------------------
1 | package gobo
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/url"
9 | )
10 |
11 | // Authenticator结构体实现了微博应用授权功能
12 | type Authenticator struct {
13 | redirectUri string
14 | clientId string
15 | clientSecret string
16 | initialized bool
17 | httpClient *http.Client
18 | }
19 |
20 | // 初始化结构体
21 | //
22 | // 在调用其它函数之前必须首先初始化。
23 | func (auth *Authenticator) Init(redirectUri string, clientId string, clientSecret string) error {
24 | // 检查结构体是否已经初始化
25 | if auth.initialized {
26 | return &ErrorString{"Authenticator结构体已经初始化"}
27 | }
28 |
29 | auth.redirectUri = redirectUri
30 | auth.clientId = clientId
31 | auth.clientSecret = clientSecret
32 | auth.httpClient = new(http.Client)
33 | auth.initialized = true
34 | return nil
35 | }
36 |
37 | // 得到授权URI
38 | func (auth *Authenticator) Authorize() (string, error) {
39 | // 检查结构体是否初始化
40 | if !auth.initialized {
41 | return "", &ErrorString{"Authenticator结构体尚未初始化"}
42 | }
43 |
44 | return fmt.Sprintf("%s/oauth2/authorize?redirect_uri=%s&response_type=code&client_id=%s", ApiDomain, auth.redirectUri, auth.clientId), nil
45 | }
46 |
47 | // 从授权码得到访问令牌
48 | func (auth *Authenticator) AccessToken(code string) (AccessToken, error) {
49 | // 检查结构体是否初始化
50 | token := AccessToken{}
51 | if !auth.initialized {
52 | return token, &ErrorString{"Authenticator结构体尚未初始化"}
53 | }
54 |
55 | // 生成请求URI
56 | queries := url.Values{}
57 | queries.Add("client_id", auth.clientId)
58 | queries.Add("client_secret", auth.clientSecret)
59 | queries.Add("redirect_uri", auth.redirectUri)
60 | queries.Add("grant_type", "authorization_code")
61 | queries.Add("code", code)
62 |
63 | // 发送请求
64 | err := auth.sendPostHttpRequest("oauth2/access_token", queries, &token)
65 | return token, err
66 | }
67 |
68 | // 得到访问令牌对应的信息
69 | func (auth *Authenticator) GetTokenInfo(token string) (AccessTokenInfo, error) {
70 | // 检查结构体是否初始化
71 | info := AccessTokenInfo{}
72 | if !auth.initialized {
73 | return info, &ErrorString{"Authenticator结构体尚未初始化"}
74 | }
75 |
76 | // 生成请求URI
77 | queries := url.Values{}
78 | queries.Add("access_token", token)
79 |
80 | // 发送请求
81 | err := auth.sendPostHttpRequest("oauth2/get_token_info", queries, &info)
82 | return info, err
83 | }
84 |
85 | // 解除访问令牌的授权
86 | func (auth *Authenticator) Revokeoauth2(token string) error {
87 | // 检查结构体是否初始化
88 | if !auth.initialized {
89 | return &ErrorString{"Authenticator结构体尚未初始化"}
90 | }
91 |
92 | // 生成请求URI
93 | queries := url.Values{}
94 | queries.Add("access_token", token)
95 |
96 | // 发送请求
97 | type Result struct {
98 | Result string
99 | }
100 | var result Result
101 | err := auth.sendPostHttpRequest("oauth2/revokeoauth2", queries, &result)
102 | return err
103 | }
104 |
105 | func (auth *Authenticator) sendPostHttpRequest(apiName string, queries url.Values, response interface{}) error {
106 | // 生成请求URI
107 | requestUri := fmt.Sprintf("%s/%s", ApiDomain, apiName)
108 |
109 | // 发送POST Form请求
110 | resp, err := auth.httpClient.PostForm(requestUri, queries)
111 | if err != nil {
112 | return err
113 | }
114 | defer resp.Body.Close()
115 |
116 | // 解析返回内容
117 | bytes, _ := ioutil.ReadAll(resp.Body)
118 | if resp.StatusCode == 200 {
119 | err := json.Unmarshal(bytes, &response)
120 | if err != nil {
121 | return err
122 | }
123 | } else {
124 | var weiboErr WeiboError
125 | err := json.Unmarshal(bytes, &weiboErr)
126 | if err != nil {
127 | return err
128 | }
129 | return weiboErr
130 | }
131 | return nil
132 | }
133 |
--------------------------------------------------------------------------------
/constants.go:
--------------------------------------------------------------------------------
1 | package gobo
2 |
3 | // 微博API相关的常数
4 | const (
5 | ApiDomain string = "https://api.weibo.com"
6 | ApiVersion string = "2"
7 | UploadAPIName string = "statuses/upload"
8 | ApiNamePostfix string = ".json"
9 | )
10 |
--------------------------------------------------------------------------------
/contrib/get_statuses.go:
--------------------------------------------------------------------------------
1 | package contrib
2 |
3 | import (
4 | "github.com/huichen/gobo"
5 | "math"
6 | "sort"
7 | "time"
8 | )
9 |
10 | const (
11 | STATUSES_PER_PAGE = 100
12 | MAX_THREADS = 20
13 | )
14 |
15 | // 并行抓取指定用户的微博
16 | //
17 | // 输入参数:
18 | // weibo gobo.Weibo结构体指针
19 | // access_token 用户的访问令牌
20 | // userName 微博用户名
21 | // userId 微博用户ID,注意仅当userName为空字符串时使用此值
22 | // numStatuses 需要抓取的总微博数,注意由于新浪的限制,最多只能抓取最近2000条微博,当此参数大于2000时取2000
23 | // timeout 超时退出,单位为毫秒,当值为0时不设超时
24 | //
25 | // 返回按照ID逆序排序的微博
26 | func GetStatuses(weibo *gobo.Weibo, access_token string, userName string, userId int64, numStatuses int, timeout int) ([]*gobo.Status, error) {
27 | // 检查输入参数的有效性
28 | if userName == "" && userId == 0 {
29 | return nil, &gobo.ErrorString{"userName和userId不可以都是无效值"}
30 | }
31 |
32 | // 计算需要启动的进程数
33 | if numStatuses <= 0 {
34 | return nil, &gobo.ErrorString{"抓取微博数必须大于零"}
35 | }
36 | numThreads := int(math.Ceil(float64(numStatuses) / STATUSES_PER_PAGE))
37 | if numThreads > MAX_THREADS {
38 | numThreads = MAX_THREADS
39 | }
40 |
41 | // output通道中收集所有线程抓取的微博
42 | output := make(chan *gobo.Status, STATUSES_PER_PAGE*numThreads)
43 |
44 | // done通道中收集线程抓取微博的数目,并负责通知主线程是否全部子线程已经完成
45 | done := make(chan int, numThreads)
46 |
47 | // 启动子线程
48 | for i := 0; i < numThreads; i++ {
49 | // 开辟numThreads个新线程负责分页抓取微博
50 | go func(page int) {
51 | var posts gobo.Statuses
52 | var params gobo.Params
53 | if userName != "" {
54 | params = gobo.Params{"screen_name": userName, "count": STATUSES_PER_PAGE, "page": page}
55 | } else {
56 | params = gobo.Params{"uid": userId, "count": STATUSES_PER_PAGE, "page": page}
57 | }
58 | err := weibo.Call("statuses/user_timeline", "get", access_token, params, &posts)
59 | if err != nil {
60 | done <- 0
61 | return
62 | }
63 | for _, p := range posts.Statuses {
64 | select {
65 | case output <- p:
66 | default:
67 | }
68 | }
69 | done <- len(posts.Statuses)
70 | }(i + 1)
71 | }
72 |
73 | // 循环监听线程通道
74 | numCompletedThreads := 0
75 | numReceivedStatuses := 0
76 | numTotalStatuses := 0
77 | statuses := make([]*gobo.Status, 0, numThreads*STATUSES_PER_PAGE) // 长度为零但预留足够容量
78 | isTimeout := false
79 | t0 := time.Now()
80 | for {
81 | // 非阻塞监听output和done通道
82 | select {
83 | case status := <-output:
84 | statuses = append(statuses, status)
85 | numReceivedStatuses++
86 | case numThreadStatuses := <-done:
87 | numCompletedThreads++
88 | numTotalStatuses = numTotalStatuses + numThreadStatuses
89 | case <-time.After(time.Second): // 让子线程飞一会儿
90 | }
91 |
92 | // 超时退出
93 | if timeout > 0 {
94 | t1 := time.Now()
95 | if t1.Sub(t0).Nanoseconds() > int64(timeout)*1000000 {
96 | isTimeout = true
97 | break
98 | }
99 | }
100 |
101 | // 当所有线程完成并且从output通道收集齐全部微博时退出循环
102 | if numCompletedThreads == numThreads && numTotalStatuses == numReceivedStatuses {
103 | break
104 | }
105 | }
106 |
107 | if isTimeout {
108 | return nil, &gobo.ErrorString{"抓取超时"}
109 | }
110 |
111 | // 将所有的微博按照id顺序排序
112 | sort.Sort(StatusSlice(statuses))
113 |
114 | // 删除掉重复的微博
115 | sortedStatuses := make([]*gobo.Status, 0, len(statuses))
116 | numStatusesToReturn := 0
117 | for i := 0; i < len(statuses); i++ {
118 | // 跳过重复微博
119 | if i > 0 && statuses[i].Id == statuses[i-1].Id {
120 | continue
121 | }
122 |
123 | sortedStatuses = append(sortedStatuses, statuses[i])
124 | numStatusesToReturn++
125 |
126 | // 最多返回numStatuses条微博
127 | if numStatusesToReturn == numStatuses {
128 | break
129 | }
130 | }
131 | return sortedStatuses, nil
132 | }
133 |
134 | // 为了方便将微博排序定义下列结构体和成员函数
135 |
136 | type StatusSlice []*gobo.Status
137 |
138 | func (ss StatusSlice) Len() int {
139 | return len(ss)
140 | }
141 | func (ss StatusSlice) Swap(i, j int) {
142 | ss[i], ss[j] = ss[j], ss[i]
143 | }
144 | func (ss StatusSlice) Less(i, j int) bool {
145 | return ss[i].Id > ss[j].Id
146 | }
147 |
--------------------------------------------------------------------------------
/examples/auth.go:
--------------------------------------------------------------------------------
1 | // 例子程序:微博应用授权
2 | // 展示功能包括得到授权URI,通过授权码得到访问令牌,获得令牌对应的信息和解除访问令牌授权等功能。
3 | package main
4 |
5 | import (
6 | "bufio"
7 | "flag"
8 | "fmt"
9 | "github.com/huichen/gobo"
10 | "os"
11 | "strings"
12 | )
13 |
14 | var (
15 | redirect_uri = flag.String("redirect_uri", "", "应用的重定向地址")
16 | client_id = flag.String("client_id", "", "应用的client id")
17 | client_secret = flag.String("client_secret", "", "应用的client secret")
18 | auth = gobo.Authenticator{}
19 | )
20 |
21 | func main() {
22 | flag.Parse()
23 |
24 | // 初始化
25 | err := auth.Init(*redirect_uri, *client_id, *client_secret)
26 | if err != nil {
27 | fmt.Println(err)
28 | return
29 | }
30 |
31 | // 得到重定向地址
32 | uri, err := auth.Authorize()
33 | if err != nil {
34 | fmt.Println(err)
35 | return
36 | }
37 | fmt.Printf("请在浏览器中打开下面地址\n%s\n", uri)
38 |
39 | // 从终端读取用户输入的认证码
40 | fmt.Print("请输入浏览器返回的授权码:")
41 | reader := bufio.NewReader(os.Stdin)
42 | input, _ := reader.ReadString('\n')
43 | code := strings.TrimSuffix(string([]byte(input)), "\n")
44 |
45 | // 从授权码得到token
46 | token, err := auth.AccessToken(code)
47 | if err != nil {
48 | fmt.Println(err)
49 | return
50 | }
51 | fmt.Printf("访问令牌 = %#v\n", token)
52 |
53 | // 从token得到相关信息
54 | info, err := auth.GetTokenInfo(token.Access_Token)
55 | if err != nil {
56 | fmt.Println(err)
57 | return
58 | }
59 | fmt.Printf("访问令牌信息 = %#v\n", info)
60 |
61 | // 解除token授权
62 | revokeErr := auth.Revokeoauth2(token.Access_Token)
63 | if revokeErr != nil {
64 | fmt.Println(revokeErr)
65 | return
66 | }
67 | fmt.Println("解除授权成功")
68 | }
69 |
--------------------------------------------------------------------------------
/examples/goroutines.go:
--------------------------------------------------------------------------------
1 | // 例子程序:利用goroutines并行抓取微博
2 | package main
3 |
4 | import (
5 | "flag"
6 | "fmt"
7 | "github.com/huichen/gobo"
8 | "github.com/huichen/gobo/contrib"
9 | "time"
10 | )
11 |
12 | var (
13 | access_token = flag.String("access_token", "", "用户的访问令牌")
14 | weibo = gobo.Weibo{}
15 | timeout = flag.Int("timeout", 0, "超时,单位毫秒")
16 | )
17 |
18 | func main() {
19 | flag.Parse()
20 | fmt.Println("==== 测试并行调用 statuses/user_timeline ====")
21 |
22 | // 记录初始时间
23 | t0 := time.Now()
24 |
25 | // 抓微博
26 | statuses, err := contrib.GetStatuses(&weibo, *access_token,
27 | "人民日报", // 微博用户名
28 | 0, // 微博用户ID,仅当用户名为空字符串时使用
29 | 211, // 抓取微博数
30 | *timeout) // 不设超时
31 | if err != nil {
32 | fmt.Println(err)
33 | return
34 | }
35 | fmt.Printf("抓取的总微博数 %d\n", len(statuses))
36 |
37 | // 记录终止时间
38 | t1 := time.Now()
39 | fmt.Printf("并行抓取花费时间 %v\n", t1.Sub(t0))
40 |
41 | // 打印最后五条微博内容
42 | for i, status := range statuses {
43 | if i == 5 {
44 | break
45 | }
46 | fmt.Println(status.Text)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/examples/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huichen/gobo/a483eda6582e80e8cae69727294f3377f72029cd/examples/sample.jpg
--------------------------------------------------------------------------------
/examples/weibo.go:
--------------------------------------------------------------------------------
1 | // 例子程序:调用微博API
2 | package main
3 |
4 | import (
5 | "flag"
6 | "fmt"
7 | "github.com/huichen/gobo"
8 | "math/rand"
9 | "os"
10 | "path/filepath"
11 | "strconv"
12 | "time"
13 | )
14 |
15 | var (
16 | access_token = flag.String("access_token", "", "用户的访问令牌")
17 | image = flag.String("image", "", "上传图片的位置")
18 | random = rand.New(rand.NewSource(time.Now().UnixNano()))
19 | weibo = gobo.Weibo{}
20 | )
21 |
22 | func showUser() {
23 | fmt.Println("==== 测试 users/show ====")
24 | var user gobo.User
25 | params := gobo.Params{"screen_name": "人民日报"}
26 | err := weibo.Call("users/show", "get", *access_token, params, &user)
27 | if err != nil {
28 | fmt.Println(err)
29 | } else {
30 | fmt.Printf("%#v\n", user)
31 | }
32 | }
33 |
34 | func getFriendsStatuses() {
35 | fmt.Println("==== 测试 statuses/friends_timeline ====")
36 | var statuses gobo.Statuses
37 | params := gobo.Params{"count": 10}
38 | err := weibo.Call("statuses/friends_timeline", "get", *access_token, params, &statuses)
39 | if err != nil {
40 | fmt.Println(err)
41 | } else {
42 | for _, status := range statuses.Statuses {
43 | fmt.Println(status.Text)
44 | }
45 | }
46 | }
47 |
48 | func getUserStatus() {
49 | fmt.Println("==== 测试 statuses/user_timeline ====")
50 | var statuses gobo.Statuses
51 | params := gobo.Params{"screen_name": "人民日报", "count": 1}
52 | err := weibo.Call("statuses/user_timeline", "get", *access_token, params, &statuses)
53 | if err != nil {
54 | fmt.Println(err)
55 | } else if len(statuses.Statuses) > 0 {
56 | fmt.Printf("%#v\n", statuses.Statuses[0])
57 | }
58 | }
59 |
60 | func updateStatus() {
61 | fmt.Println("==== 测试 statuses/update ====")
62 | var status gobo.Status
63 | params := gobo.Params{"status": "测试" + strconv.Itoa(rand.Int())}
64 | err := weibo.Call("statuses/update", "status", *access_token, params, &status)
65 | if err != nil {
66 | fmt.Println(err)
67 | } else {
68 | fmt.Printf("%#v\n", status)
69 | }
70 | }
71 |
72 | func uploadStatus() {
73 | fmt.Println("==== 测试 statuses/upload ====")
74 | var status gobo.Status
75 | params := gobo.Params{"status": "测试" + strconv.Itoa(rand.Int())}
76 | img, err := os.Open(*image)
77 | if err != nil {
78 | fmt.Println(err)
79 | }
80 | err = weibo.Upload(*access_token, params, img, filepath.Ext(*image), &status)
81 | if err != nil {
82 | fmt.Println(err)
83 | } else {
84 | fmt.Printf("%#v\n", status)
85 | }
86 | }
87 |
88 | func main() {
89 | flag.Parse()
90 | showUser()
91 | getFriendsStatuses()
92 | getUserStatus()
93 | //updateStatus()
94 | //uploadStatus()
95 | }
96 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Copyright 2013 Hui Chen
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/structs.go:
--------------------------------------------------------------------------------
1 | package gobo
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // 微博API返回对象数据结构
8 | //
9 | // 结构体根据下面的文档定义
10 | // http://open.weibo.com/wiki/常见返回对象数据结构
11 | //
12 | // JSON字段名和golang结构体字段名有这样的一一对应关系
13 | //
14 | // JSON字段: golang结构体字段:
15 | // name_of_a_field Name_Of_A_Field
16 |
17 | type Status struct {
18 | Created_At string
19 | Id int64
20 | Mid string
21 | Text string
22 | Idstr string
23 | Source string
24 | Favorited bool
25 | Trucated bool
26 | In_Reply_To_Status_Id string
27 | In_Reply_To_User_Id string
28 | In_Reply_To_Screen_Name string
29 | Thumbnail_Pic string
30 | Bmiddle_Pic string
31 | Original_Pic string
32 | Geo *Geo
33 | User *User
34 | Retweeted_Status *Status
35 | Reposts_Count int
36 | Comments_Count int
37 | Attitudes_Count int
38 | Mlevel int
39 | Visible *Visible
40 | Pic_Urls []*Pic_Url
41 | }
42 |
43 | type Comment struct {
44 | Created_At string
45 | Id int64
46 | Text string
47 | Source string
48 | User *User
49 | Mid string
50 | Idstr string
51 | Status string
52 | Reply_Comment *Comment
53 | }
54 |
55 | type User struct {
56 | Id int64
57 | Idstr string
58 | Screen_Name string
59 | Name string
60 | Province string
61 | City string
62 | Location string
63 | Description string
64 | Url string
65 | Profile_Image_Url string
66 | Profile_Url string
67 | Domain string
68 | Weihao string
69 | Gender string
70 | Followers_Count int
71 | Friends_Count int
72 | Statuses_Count int
73 | Favourites_Count int
74 | Created_At string
75 | Following bool
76 | Allow_All_Act_Msg bool
77 | Geo_Enabled bool
78 | Verified bool
79 | Verified_Type int
80 | Remark string
81 | Status *Status
82 | Allow_All_Comment bool
83 | Avatar_Large string
84 | Verified_Reason string
85 | Follow_Me bool
86 | Online_Status int
87 | Bi_Followers_Count int
88 | Lang string
89 | }
90 |
91 | type Privacy struct {
92 | Comment int
93 | Geo int
94 | Message int
95 | Realname int
96 | Badge int
97 | Mobile int
98 | Webim int
99 | }
100 |
101 | type Remind struct {
102 | Status int
103 | Follower int
104 | Cmt int
105 | Dm int
106 | Mention_Status int
107 | Mention_Cmt int
108 | Group int
109 | Private_Group int
110 | Notice int
111 | Invite int
112 | Badge int
113 | Photo int
114 | }
115 |
116 | type Url_Short struct {
117 | Url_Short string
118 | Url_Long string
119 | Type int
120 | Result bool
121 | }
122 |
123 | type Geo struct {
124 | Longitude string
125 | Latitude string
126 | City string
127 | Province string
128 | City_Name string
129 | Province_Name string
130 | Address string
131 | Pinyin string
132 | More string
133 | }
134 |
135 | // 其他的常用结构体
136 |
137 | type ErrorString struct {
138 | S string
139 | }
140 |
141 | func (e *ErrorString) Error() string {
142 | return "Gobo错误:" + e.S
143 | }
144 |
145 | type WeiboError struct {
146 | Err string `json:"Error"`
147 | Error_Code int64
148 | Request string
149 | }
150 |
151 | func (e WeiboError) Error() string {
152 | return fmt.Sprintf("微博API访问错误 %d [%s] %s", e.Error_Code, e.Request, e.Err)
153 | }
154 |
155 | type AccessToken struct {
156 | Access_Token string
157 | Remind_In string
158 | Expires_In int
159 | Uid string
160 | }
161 |
162 | type AccessTokenInfo struct {
163 | Uid int64
164 | Appkey string
165 | Scope string
166 | Created_At int
167 | Expire_In int
168 | }
169 |
170 | type Statuses struct {
171 | Statuses []*Status
172 | }
173 |
174 | type Visible struct {
175 | Type int
176 | List_Id int
177 | }
178 |
179 | type Pic_Url struct {
180 | Thumbnail_Pic string
181 | }
182 |
--------------------------------------------------------------------------------
/weibo.go:
--------------------------------------------------------------------------------
1 | package gobo
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "mime/multipart"
10 | "net/http"
11 | "net/url"
12 | )
13 |
14 | // Params类型用来表达微博API的JSON输入参数。注意:
15 | // 1. Params不应当包含访问令牌(access_token),因为它已经是Call和Upload函数的参数
16 | // 2. 在Upload函数中,Params参数不应当包含pic参数,上传的图片内容和类型应当通过reader和imageFormat指定
17 | type Params map[string]interface{}
18 |
19 | // Weibo结构体定义了微博API调用功能
20 | type Weibo struct {
21 | httpClient http.Client
22 | }
23 |
24 | // 调用微博API
25 | //
26 | // 该函数可用来调用除了statuses/upload(见Upload函数)和微博授权(见Authenticator结构体)外的所有微博API。
27 | //
28 | // 输入参数
29 | // method API方法名,比如 "/statuses/user_timeline" 又如 "comments/show"
30 | // httpMethod HTTP请求方式,只能是"get"或者"post"之一,否则出错
31 | // token 用户授权的访问令牌
32 | // params JSON输入参数,见Params结构体的注释
33 | // response API服务器的JSON输出将被还原成该结构体
34 | //
35 | // 当出现异常时输出非nil错误
36 | func (weibo *Weibo) Call(method string, httpMethod string, token string, params Params, response interface{}) error {
37 | apiUri := fmt.Sprintf("%s/%s/%s%s", ApiDomain, ApiVersion, method, ApiNamePostfix)
38 | if httpMethod == "get" {
39 | return weibo.sendGetHttpRequest(apiUri, token, params, response)
40 | } else if httpMethod == "post" {
41 | return weibo.sendPostHttpRequest(apiUri, token, params, nil, "", response)
42 | }
43 | return &ErrorString{"HTTP方法只能是\"get\"或者\"post\""}
44 | }
45 |
46 | // 调用/statuses/upload发带图片微博
47 | //
48 | // 输入参数
49 | // token 用户授权的访问令牌
50 | // params JSON输入参数,见Params结构体的注释
51 | // reader 包含图片的二进制流
52 | // imageFormat 图片的格式,比如 "jpg" 又如 "png"
53 | // response API服务器的JSON输出将被还原成该结构体
54 | //
55 | // 当出现异常时输出非nil错误
56 | func (weibo *Weibo) Upload(token string, params Params, reader io.Reader, imageFormat string, response interface{}) error {
57 | apiUri := fmt.Sprintf("%s/%s/%s%s", ApiDomain, ApiVersion, UploadAPIName, ApiNamePostfix)
58 | return weibo.sendPostHttpRequest(apiUri, token, params, reader, imageFormat, response)
59 | }
60 |
61 | // 向微博API服务器发送GET请求
62 | func (weibo *Weibo) sendGetHttpRequest(uri string, token string, params Params, response interface{}) error {
63 | // 生成请求URI
64 | var uriBuffer bytes.Buffer
65 | uriBuffer.WriteString(fmt.Sprintf("%s?access_token=%s", uri, token))
66 | for k, v := range params {
67 | value := fmt.Sprint(v)
68 | if k != "" && value != "" {
69 | uriBuffer.WriteString(fmt.Sprintf("&%s=%s", k, value))
70 | }
71 | }
72 | requestUri := uriBuffer.String()
73 |
74 | // 发送GET请求
75 | resp, err := weibo.httpClient.Get(requestUri)
76 | if err != nil {
77 | return err
78 | }
79 | defer resp.Body.Close()
80 |
81 | // 解析API服务器返回内容
82 | bytes, _ := ioutil.ReadAll(resp.Body)
83 | if resp.StatusCode == 200 {
84 | err := json.Unmarshal(bytes, &response)
85 | if err != nil {
86 | return err
87 | }
88 | return nil
89 | } else {
90 | var weiboErr WeiboError
91 | err := json.Unmarshal(bytes, &weiboErr)
92 | if err != nil {
93 | return err
94 | }
95 | return weiboErr
96 | }
97 | return nil
98 | }
99 |
100 | // 向微博API服务器发送POST请求
101 | //
102 | // 输入参数的含义请见Upload函数注释。当reader == nil时使用query string模式,否则使用multipart。
103 | func (weibo *Weibo) sendPostHttpRequest(uri string, token string, params Params, reader io.Reader, imageFormat string, response interface{}) error {
104 | // 生成POST请求URI
105 | requestUri := fmt.Sprintf("%s?access_token=%s", uri, token)
106 |
107 | // 生成POST内容
108 | var bodyBuffer bytes.Buffer
109 | var writer *multipart.Writer
110 | if reader == nil {
111 | // reader为nil时无文件上传,因此POST body为简单的query string模式
112 | pb := url.Values{}
113 | pb.Add("access_token", token)
114 |
115 | for k, v := range params {
116 | value := fmt.Sprint(v)
117 | if k != "" && value != "" {
118 | pb.Add(k, value)
119 | }
120 | }
121 | bodyBuffer = *bytes.NewBufferString(pb.Encode())
122 | } else {
123 | // 否则POST body使用multipart模式
124 | writer = multipart.NewWriter(&bodyBuffer)
125 | imagePartWriter, _ := writer.CreateFormFile("pic", "image."+imageFormat)
126 | io.Copy(imagePartWriter, reader)
127 | for k, v := range params {
128 | value := fmt.Sprint(v)
129 | if k != "" && value != "" {
130 | writer.WriteField(k, value)
131 | }
132 | }
133 | writer.Close()
134 | }
135 |
136 | // 生成POST请求
137 | req, err := http.NewRequest("POST", requestUri, &bodyBuffer)
138 | if err != nil {
139 | return err
140 | }
141 | if reader == nil {
142 | // reader为nil时使用一般的内容类型
143 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
144 | } else {
145 | // 否则使用带boundary的multipart类型
146 | req.Header.Set("Content-Type", writer.FormDataContentType())
147 | }
148 |
149 | // 发送请求
150 | resp, err := weibo.httpClient.Do(req)
151 | if err != nil {
152 | return err
153 | }
154 | defer resp.Body.Close()
155 |
156 | // 解析API服务器返回内容
157 | bytes, _ := ioutil.ReadAll(resp.Body)
158 | if resp.StatusCode == 200 {
159 | err := json.Unmarshal(bytes, &response)
160 | if err != nil {
161 | return err
162 | }
163 | return nil
164 | } else {
165 | var weiboErr WeiboError
166 | err := json.Unmarshal(bytes, &weiboErr)
167 | if err != nil {
168 | return err
169 | }
170 | return weiboErr
171 | }
172 | return nil
173 | }
174 |
--------------------------------------------------------------------------------