LoginSignup
7
6

More than 5 years have passed since last update.

【Golang初心者 part1】お題:Slack になんか通知する

Posted at

ルール

  • go の勉強を始めたので、アウトプットする
  • 毎回お題を設け、取りあえずの達成を目指す(覚えたことは書く)
  • 同志(プログラミング初心者が スターティングGo言語 を読んだレベル)へ、分かり易さを意識して書く

お題

Slack になんか通知する

参考:https://qiita.com/xuj/items/4c45185bb3b3862cd7f1

ソース

main.go
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))
}

実行

shell
 $ go run main.go hello, slack
ok

image.png

達成 :beers:

調べたこと

構造体のタグ

main.go
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型で取得する

Marshal()
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エンコーディングしてみる

sample.go
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にキャストして表示
}

実行するとこうなる

shell
$ 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

main.go
arg := strings.Join(os.Args[1:], "")

os.Args とは

Args hold the command-line arguments, starting with the program name.
(Argsはプログラム名から始まるコマンドライン引数を保持します。)

コマンド自体と引数が格納されるみたい
Args 自体はただのosパッケージの変数で []string 型(string型のスライス)

os.Args
var Args []string

試してみる

arg.go
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つを与えて実行してみる

shell
$ arg.exe golang arg test
[C:\Users\andromeda\bin\arg.exe golang arg test] # ①
type: []string    # ②
length: 4         # ③
######
[golang arg test] #④

フムフム…で strings.Join() に渡してるのか


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で返している

main.go
arg := strings.Join(os.Args[1:], "")

試してみる

arg.go
    arg := strings.Join(os.Args[1:], "")
    fmt.Printf("arg: %v\n", arg)
    fmt.Printf("arg type: %T\n", arg)

コマンドと引数(golang,test)2つを与えて実行してみる

shell
$ go run arg.go golang, test
arg: golang,test
arg type: string

なるほど

os.Args まとめ

簡易的に使う場合はこれで、もっと高機能に扱いたい場合は flag パッケージを使うみたい


http.PostForm()

main.go
    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 したいデータを指定

PostForm()
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 のメソッドを持っている

url.Values
type Values map[string][]string
//func (Values) Add
func (v Values) Add(key, value string)
//func (Values) Del
func (v Values) Del(key string)

一応試してみる

value.go
    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)

実行すると

shell
$ 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が発行されることが分かった

postform.go
    resp, _ := http.PostForm(
        IncomingURL,
        url.Values{"payload": {string(jsonparams)}},
    )
    fmt.Println(resp)
    fmt.Printf("resp type: %T\n", resp)

実行して戻り値とか型とかを見てみると、こんな感じだった

shell
$ 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 型?


http.Response型とは

Response represents the response from an HTTP request.
(Responseは、HTTP要求からの応答を表します。)

…まぁそうですよね
Get / Head とかも戻り値が *Response型で返されるみたい
一応いくつかフィールドを見てみる

postform.go
    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)

実行すると

shell
$ 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 を見たとき、Bodyio.ReadCloser 型だったはず…?

http.Response
Body io.ReadCloser

まず、 ReadCloser 型を見てみよう

ReadCloser
type ReadCloser interface {
        Reader
        Closer
}

ん…これはインタフェースが埋め込まれてる?
では次に Reader 型を見てみよう

Reader
type Reader interface {
        Read(p []byte) (n int, err error)
}

おっ…メソッドが定義されてる
では、次に http.http2gzipReader を見てみると…

http2gzipReader
func (gz *http2gzipReader) Read(p []byte) (n int, err error)

Read(p []byte) (n int, err error) メソッドを実装してる!!!
(これが俗に言うダックタイピングか…)

(疑問)
その型(今回であれば http.http2gzipReader 型)が実装してるインタフェースてどうやって調べるもんなの? そもそも考え方が違う?


ioutil.ReadAll()

main.go
// 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スライスを返すようです

ReadAll()
func ReadAll(r io.Reader) ([]byte, error)

そして、さきほど、 resp.Bodyio.Reader 型(インタフェースを実装している)である事もわかりました
要するにBodyの中身を取り出したんですね そしてstringにキャストしている

main.go
    body, _ := ioutil.ReadAll(resp.Body)
    println(string(body))

最後に、これは単純でこれをやらないとコネクションがクローズされない

main.go
    defer resp.Body.Close()

公式にもちゃんと書いてある

The client must close the response body when finished with it:

レスポンスを受け取ったら黙って defer resp.Body.Close() と書いとけばよい

所感、次やりたいこと、とか

  • まずは、ヤクの毛刈り的に、気になる事を次々とある程度納得できるまで調べた
    構造体の埋め込みとか、メソッドを持たせるとインタフェース実装出来る、とか実践での使われ方が何となく理解できた(やっぱ実践強ぇ)
    しばらくこのやり方で進めていくうちに、初心者から抜け出せそうな気がします:v:
  • 次回以降のネタ候補
    • flag とか、urfave/cli とか使って、なんかコマンド作ってみたい
    • http扱うなら、net/http が色んなサイトに使い方載ってそう
    • サードパーティの labstack/echo が有名で高速らしい
    • この辺 参考にslackbot 作ってみたい
7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6