Go言語の練習用にTwitterのOAuth認証をフルスクラッチしてみた

何となく気が向いたので昨日はじめたばかりのGo言語で練習がてら書いてみました。JSONのレスポンスはエラーチェック用にしか行っておらず、正常時はそのまま文字列として返します。しかも奇形レスポンスのチェックは省いているのでかなり手抜きです。でも標準パッケージしか使ってないからいいんだ、うん！

```go:goisgod/goisgod.go
/**
 * GO is GOD
 */
package goisgod

import (
    "fmt"
    "time"
    "sort"
    "strings"
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha1"
    "encoding/base64"
    "encoding/json"
    "net/http"
    "net/url"
    "io/ioutil"
)

/**
 * クレデンシャルを保持する構造体
 */
type OAuth struct {
    ConsumerKey string
    ConsumerSecret string
    OAuthToken string
    OAuthTokenSecret string
}

/**
 * レスポンスのメタ情報
 */
type ResponseStatus struct {
    IsError bool
    StatusCode int
    ErrorCode int
    ErrorMessage string
}

/**
 * PHPのarray_merge的な関数が欲しかった
 */
func mergeMap(maps ...map[string]string) map[string]string {
    hint := 0
    for _, m := range maps {
        hint += len(m)
    }
    newmap := make(map[string]string, hint)
    for _, m := range maps {
        for k, v := range m {
            newmap[k] = v
        }
    }
    return newmap
}

/**
 * PHPのarray_map的な関数が欲しかった
 */
func forStrings(fn func(string) string, strs []string) []string {
    newstrs := make([]string, len(strs))
    for i, s := range strs {
        newstrs[i] = fn(s)
    }
    return newstrs
}

/**
 * PHPのrawurlencode関数相当の処理
 * これに1時間以上悩まされた
 */
func encodeRFC3986(s string) string {
    return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
}

/**
 * mapをキー基準でソートしてクエリ文字列にする関数
 * Go言語のmapには順序保証が無いようであった
 */
func encodeMap(
    m map[string]string,
    separator string,
    enclosure string,
) string {
    keys := make([]string, len(m))
    i := 0
    for k := range m {
        keys[i] = k
        i++
    }
    sort.Strings(keys)
    sets := make([]string, len(m))
    for i, k := range keys {
        ek := encodeRFC3986(k)
        ev := encodeRFC3986(m[k])
        sets[i] = fmt.Sprintf("%s=%s%s%s", ek, enclosure, ev, enclosure)
    }
    return strings.Join(sets, separator)
}

/**
 * ランダム文字列生成
 */
func generateNonce(n int) string {
    const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    bytes := make([]byte, n)
    rand.Read(bytes)
    for i, b := range bytes {
        bytes[i] = chars[b % byte(len(chars))]
    }
    return string(bytes)
}

/**
 * oauth_signatureの生成
 */
func generateSignature(data string, key string) string {
    hash := hmac.New(sha1.New, []byte(key))
    hash.Write([]byte(data))
    return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}

/**
 * Authorizationヘッダの生成
 * ここを気持ちよくファンクショナルに書くために全力を尽くした
 */
func (t *OAuth) generateAuthorizationHeader(
    method string,
    request_url string,
    additional_params map[string]string,
) string {
    oauth_params := map[string]string {
        "oauth_consumer_key": t.ConsumerKey,
        "oauth_signature_method": "HMAC-SHA1",
        "oauth_timestamp": fmt.Sprint(uint32(time.Now().Unix())),
        "oauth_version": "1.0a",
        "oauth_nonce": generateNonce(32),
        "oauth_token": t.OAuthToken,
    }
    base := mergeMap(additional_params, oauth_params)
    oauth_params["oauth_signature"] = generateSignature(
        strings.Join(forStrings(encodeRFC3986, []string {
            method,
            request_url,
            encodeMap(base, "&", ""),
        }), "&"),
        strings.Join(forStrings(encodeRFC3986, []string {
            t.ConsumerSecret,
            t.OAuthTokenSecret,
        }), "&"),
    )
    return encodeMap(oauth_params, ", ", "\"")
}

/**
 * リクエストを処理する関数
 * 成功時にはJSON文字列そのまま、失敗時には空文字列を返す
 * レスポンスステータスは常に返す
 */
func processRequest(req *http.Request)(string, *ResponseStatus) {
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", &ResponseStatus {
            true,
            0,
            0,
            fmt.Sprint(err),
        }
    }
    defer resp.Body.Close()
    content, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", &ResponseStatus {
            true,
            resp.StatusCode,
            0,
            fmt.Sprint(err),
        }
    }
    if resp.StatusCode >= 400 {
        type ErrorObject struct {
            Code int
            Message string
        }
        type ErrorListObject struct {
            Errors [1]ErrorObject
        }
        var elo ErrorListObject
        json.Unmarshal(content, &elo)
        fmt.Println(string(content), elo)
        if elo.Errors[0].Code == 0 || elo.Errors[0].Message == "" {
            return "", &ResponseStatus {
                true,
                resp.StatusCode,
                0,
                "Unknown Error",
            }
        }
        return "", &ResponseStatus {
            true,
            resp.StatusCode,
            elo.Errors[0].Code,
            elo.Errors[0].Message,
        }
    }
    return string(content), &ResponseStatus {
        false,
        resp.StatusCode,
        0,
        "",
    }
}

/**
 * GETリクエスト用
 */
func (t *OAuth) Get(request_url string, params map[string]string)(string, *ResponseStatus) {
    ah := t.generateAuthorizationHeader("GET", request_url, params)
    req, _ := http.NewRequest("GET", request_url + "?" + encodeMap(params, "&", ""), nil)
    req.Header.Add("Authorization", "OAuth " + ah)
    return processRequest(req)
}

/**
 * POSTリクエスト用
 */
func (t *OAuth) Post(request_url string, params map[string]string)(string, *ResponseStatus) {
    ah := t.generateAuthorizationHeader("POST", request_url, params)
    reader := strings.NewReader(encodeMap(params, "&", ""))
    req, _ := http.NewRequest("POST", request_url, reader)
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Add("Authorization", "OAuth " + ah)
    return processRequest(req)
}
```

```go:example.go
package main

import (
    "fmt"
    "log"
    "./goisgod"
)

func main() {
    gg := &goisgod.OAuth {
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    }
    url := "https://api.twitter.com/1.1/statuses/update.json"
    params := map[string]string {
        "status": "GO is GOD",
    }
    result, status := gg.Post(url, params)
    if status.IsError {
        log.Fatal(fmt.Sprint(status))
    }
    fmt.Println(result)
}
```

```go:example_async.go
package main

import (
    "fmt"
    "log"
    "sync"
    "strings"
    "./goisgod"
)

func main() {
    gg := &goisgod.OAuth {
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    }
    url := "https://api.twitter.com/1.1/statuses/update.json"
    wg := &sync.WaitGroup {}
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func (i int) {
            defer wg.Done()
            params := map[string]string {"status": "@null GO is GOD " + strings.Repeat("!", i)}
            result, status := gg.Post(url, params)
            if status.IsError {
                log.Fatal(fmt.Sprint(status))
            }
            fmt.Println(result)
        }(i)
    }
    wg.Wait()
}
```