ルール
- go の勉強を始めたので、アウトプットする
- 毎回お題を設け、取りあえずの達成を目指す(覚えたことは書く)
- 同志(プログラミング初心者が スターティングGo言語 を読んだレベル)へ、分かり易さを意識して書く
お題
Slack になんか通知する
参考:https://qiita.com/xuj/items/4c45185bb3b3862cd7f1
ソース
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)
// IncomingURL - Get it from here https://slack.com/services/new/incoming-webhook
var IncomingURL string = "https://hooks.slack.com/services/XXX/XXX/XXX"
// Slack struct - payload parameter of json to post.
type Slack struct {
Text string `json:"text"`
Username string `json:"username"`
IconEmoji string `json:"icon_emoji"`
IconURL string `json:"icon_url"`
Channel string `json:"channel"`
}
func main() {
arg := strings.Join(os.Args[1:], "")
params := Slack{
Text: fmt.Sprintf("%s", arg),
Username: "From golang to slack hello",
IconEmoji: ":gopher:",
IconURL: "",
Channel: "",
}
jsonparams, _ := json.Marshal(params)
resp, _ := http.PostForm(
IncomingURL,
url.Values{"payload": {string(jsonparams)}},
)
body, _ := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
println(string(body))
}
実行
$ go run main.go hello, slack
ok
達成
調べたこと
構造体のタグ
type Slack struct {
Text string `json:"text"`
Username string `json:"username"`
IconEmoji string `json:"icon_emoji"`
IconURL string `json:"icon_url"`
Channel string `json:"channel"`
}
構造体は進研ゼミで習ったつもりで、変数をひとまとめにするやつ
中身をフィールドと言って、名前と型で定義するやつ、、で後ろの json なんとかってなに…?
struct にはタグが付与できるとのこと(メタデータ的な!?)
A field declaration may be followed by an optional string literal tag,
(フィールド宣言の後にオプションの文字列リテラルタグを続けることができます。)
で、構造体のデータを JSON 形式に出力する encoding/json パッケージというのがある
Marshal() か、インデントしてくれる MarshalIndent() で json エンコーディングできた
Marshal() は以下の通り、引数にinterface{}型を与え、[]byte型で取得する
func Marshal(v interface{}) ([]byte, error)
構造体タグの指定方法とその説明
Examples of struct field tags and their meanings:
// Field appears in JSON as key "myName".
Field int `json:"myName"`
// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`
// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`
// Field is ignored by this package.
Field int `json:"-"`
// Field appears in JSON as key "-".
Field int `json:"-,"`
試しに、分かり易く1つだけ構造体にタグを定義してjsonエンコーディングしてみる
type Slack struct {
Text string `json:"test_text"`
}
func main() {
a := Slack{Text: "hello, slack"} // 構造体の初期化
b, _ := json.Marshal(a) // jsonエンコーディングを[]byte型で受け取る errorは _ で捨てる
fmt.Printf("type: %T\n", b) // type(型)を確認
fmt.Println(b) // そのまま[]byte型で表示
fmt.Println(string(b)) // stringにキャストして表示
}
実行するとこうなる
$ go run sample.go
type: []uint8
[123 34 116 101 115 116 95 116 101 120 116 34 58 34 104 101 108 108 111 44 32 115 108 97 99 107 34 125]
{"test_text":"hello, slack"}
直接関係ないけど、byte型はuint8型の別名だそうです。。うん、ようわからん…w
構造体タグのまとめ
jsonフォーマット作りたい時は、構造体タグと encoding/json パッケージ使えばできる
os.Args
arg := strings.Join(os.Args[1:], "")
os.Args とは
Args hold the command-line arguments, starting with the program name.
(Argsはプログラム名から始まるコマンドライン引数を保持します。)
コマンド自体と引数が格納されるみたい
Args 自体はただのosパッケージの変数で []string
型(string型のスライス)
var Args []string
試してみる
func main() {
arg := os.Args // 変数に格納
fmt.Println(arg) // ①普通に表示
fmt.Printf("type: %T\n", arg) // ②タイプ(型)を表示
fmt.Printf("length: %d\n", len(arg)) // ③スライスの要素数を表示
fmt.Println("######")
arg2 := os.Args[1:] // 引数だけを取得
fmt.Println(arg2) // ④引数だけを表示
}
コマンドと引数(golang,arg,test)3つを与えて実行してみる
$ arg.exe golang arg test
[C:\Users\andromeda\bin\arg.exe golang arg test] # ①
type: []string # ②
length: 4 # ③
######
[golang arg test] #④
フムフム…で strings.Join()
に渡してるのか
Join concatenates the elements of a to create a single string. The separator string sep is placed between elements in the resulting string.
(Joinはaの要素を連結して単一の文字列を作成します。セパレータ文字列sepは、結果の文字列内の要素の間に配置されます。)
第 1 引数に []string
型を、第 2 引数に string
を取り、第 2 引数のセパレート文字(sep)で連結してstringで返す
func Join(a []string, sep string) string
つまり、以下の場合、os.Args[1:]
でコマンド以外(インデックス1以降)の引数のみのstring[]をセパレート文字("")で連結しstringで返している
arg := strings.Join(os.Args[1:], "")
試してみる
arg := strings.Join(os.Args[1:], "")
fmt.Printf("arg: %v\n", arg)
fmt.Printf("arg type: %T\n", arg)
コマンドと引数(golang,test)2つを与えて実行してみる
$ go run arg.go golang, test
arg: golang,test
arg type: string
なるほど
os.Args まとめ
簡易的に使う場合はこれで、もっと高機能に扱いたい場合は flag パッケージを使うみたい
http.PostForm()
resp, _ := http.PostForm(
IncomingURL,
url.Values{"payload": {string(jsonparams)}},
)
PostForm() とは
PostForm issues a POST to the specified URL, with data's keys and values URL-encoded as the request body.
(PostFormは指定されたURLにPOSTを発行し、データのキーと値はリクエスト本体としてURLエンコードされます。)
第 1 引数に POST したい URL 指定して、第 2 引数に url.Values 型で POST したいデータを指定
func PostForm(url string, data url.Values) (resp *Response, err error)
url.Values 型…?
url.Values 型とは
Values maps a string key to a list of values.It is typically used for query parameters and form values.
(値は文字列キーを値のリストにマップします。これは、通常、クエリパラメータとフォーム値に使用されます。 )
Values
自体は、ただの map[string][]string
型でクエリパラメータをkey-valueで持たせる為の型だそうで
で、クエリパラメータを組み立てる為に、Add とか Del のメソッドを持っている
type Values map[string][]string
//func (Values) Add
func (v Values) Add(key, value string)
//func (Values) Del
func (v Values) Del(key string)
一応試してみる
values := url.Values{"test-key": {"value1", "value2"}}
fmt.Printf("values type: %T\n", values)
fmt.Printf("values: %#v\n", values)
values.Add("test-key2", "value3")
fmt.Printf("values: %#v\n", values)
実行すると
$ go run value.go
values type: url.Values
values: url.Values{"test-key":[]string{"value1", "value2"}}
values: url.Values{"test-key":[]string{"value1", "value2"}, "test-key2":[]string{"value3"}}
うん、 url.Values
型(map[string][]string型)だった そして追加(ADD)もできた
話を元に戻して、以下を実行すると、第 1 引数に通知するslackのwebhook、第 2 引数にPOSデータを指定するとPOSTが発行されることが分かった
resp, _ := http.PostForm(
IncomingURL,
url.Values{"payload": {string(jsonparams)}},
)
fmt.Println(resp)
fmt.Printf("resp type: %T\n", resp)
実行して戻り値とか型とかを見てみると、こんな感じだった
$ go run postform.go hello
&{200 OK 200 HTTP/2.0 2 0 map[X-Via:[haproxy-www-muuk] X-Slack-Exp:[1] Access-Control-Allow-Origin:[*] X-Slack-Router:[p] Vary:[Accept-Encoding] X-Cache:[Miss from cloudfront] X-Amz-Cf-Id:[WD5-x] Date:[Sat, 08 Dec 2018 15:27:31 GMT] X-Slack-Backend:[h] X-Frame-Options:[SAMEORIGIN] Via:[1.1 x (CloudFront)] Strict-Transport-Security:[max-age=31536000; includeSubDomains; preload] Content-Type:[text/html] Server:[Apache] Referrer-Policy:[no-referrer]] x -1 [] false true map[] xxx xxx}
resp type: *http.Response
*http.Response
型?
Response represents the response from an HTTP request.
(Responseは、HTTP要求からの応答を表します。)
…まぁそうですよね
Get / Head とかも戻り値が *Response型
で返されるみたい
一応いくつかフィールドを見てみる
resp, _ := http.PostForm(
IncomingURL,
url.Values{"payload": {string(jsonparams)}},
)
fmt.Println(resp.Status)
fmt.Printf("resp.Status type: %T\n", resp.Status)
fmt.Println(resp.Proto)
fmt.Printf("resp.Proto type: %T\n", resp.Proto)
fmt.Println(resp.Body)
fmt.Printf("resp.Body type: %T\n", resp.Body)
実行すると
$ go run postform.go test
200 OK
resp.Status type: string
HTTP/2.0
resp.Proto type: string
&{{0xc00004f040} <nil> <nil>}
resp.Body type: *http.http2gzipReader
Body
の型が *http.http2gzipReader
ん…さっき type Response を見たとき、Body
は io.ReadCloser
型だったはず…?
Body io.ReadCloser
まず、 ReadCloser 型を見てみよう
type ReadCloser interface {
Reader
Closer
}
ん…これはインタフェースが埋め込まれてる?
では次に Reader 型を見てみよう
type Reader interface {
Read(p []byte) (n int, err error)
}
おっ…メソッドが定義されてる
では、次に http.http2gzipReader を見てみると…
func (gz *http2gzipReader) Read(p []byte) (n int, err error)
Read(p []byte) (n int, err error)
メソッドを実装してる!!!
(これが俗に言うダックタイピングか…)
(疑問)
その型(今回であれば http.http2gzipReader
型)が実装してるインタフェースてどうやって調べるもんなの? そもそも考え方が違う?
ioutil.ReadAll()
// snip
body, _ := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
println(string(body))
//snip
ReadAll とは
ReadAll reads from r until an error or EOF and returns the data it read. A successful call returns err == nil, not err == EOF. Because ReadAll is defined to read from src until EOF, it does not treat an EOF from Read as an error to be reported.
(ReadAllはrからエラーまたはEOFまで読み取り、読み取ったデータを返します。
呼び出しが成功すると、err == nil、err == EOFではなくerr == nilが返されます。 ReadAllはsrcからEOFまで読み取るように定義されているため、ReadからのEOFをエラーとして処理することは報告されません。)
説明はよう分からんが、ReadAllは io.Reader
型を取り、byteスライスを返すようです
func ReadAll(r io.Reader) ([]byte, error)
そして、さきほど、 resp.Body
が io.Reader
型(インタフェースを実装している)である事もわかりました
要するにBodyの中身を取り出したんですね そしてstringにキャストしている
body, _ := ioutil.ReadAll(resp.Body)
println(string(body))
最後に、これは単純でこれをやらないとコネクションがクローズされない
defer resp.Body.Close()
公式にもちゃんと書いてある
The client must close the response body when finished with it:
レスポンスを受け取ったら黙って defer resp.Body.Close()
と書いとけばよい
所感、次やりたいこと、とか
- まずは、ヤクの毛刈り的に、気になる事を次々とある程度納得できるまで調べた
構造体の埋め込みとか、メソッドを持たせるとインタフェース実装出来る、とか実践での使われ方が何となく理解できた(やっぱ実践強ぇ)
しばらくこのやり方で進めていくうちに、初心者から抜け出せそうな気がします - 次回以降のネタ候補
- flag とか、urfave/cli とか使って、なんかコマンド作ってみたい
- http扱うなら、net/http が色んなサイトに使い方載ってそう
- サードパーティの labstack/echo が有名で高速らしい
- この辺 参考にslackbot 作ってみたい