もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

write-blog-every-weekの通知ロジックをPythonからGoに書きなおした

はじめに

こんばんは。

絶賛Go勉強中の僕です。

前回・前々回と基礎的なところを勉強してみたので、今回は現状Pythonで実装しているブログSlackの通知ロジックをGoに書き直してみようと思いました。

その中でいろいろと学んだことをさらに記録しておこうと思います。

コード

設定ファイルどうする?

pythonで実装していたときは、YAMLファイルで用意してたのですが、Goはどうなのかなといろいろ調べてると、TOMLで用意するという記事がみかけたので、それに習ってみました。

インストール
$ go get github.com/BurntSushi/toml
config.toml
[Slack]
RequestToken    = "XXXXXX"
GetAPIURL       = "https://slack.com/api/channels.history"
GetChannelID    = "XXXXXX"
SendAPIURL      = "https://hooks.slack.com/XXXXXXX"
SendChannelName = "XXXXXX"

[[MemberData]]
BlogTitle = "もがき系プログラマの日常"
UserID    = "XXXXX"

[[MemberData]]
BlogTitle = "XXXXXXXXXXXXXXX"
UserID    = "XXXXX"
main.go
package main

import (
    "fmt"

    "github.com/BurntSushi/toml"
)

type ConfigData struct {
    Slack      Slack
    MemberData []MemberData
}

type Slack struct {
    RequestToken    string
    GetAPIURL       string
    GetChannelID    string
    SendAPIURL      string
    SendChannelName string
}

type MemberData struct {
    BlogTitle string
    UserID    string
}

func main() {
    var configData ConfigData
    _, err := toml.DecodeFile("config.toml", &configData)
    if err != nil {
        panic("tomlファイルを読み込めません")
    }

    fmt.Println(configData.Slack.GetAPIURL)
    fmt.Println(configData.Slack.RequestToken)

    for i := 0; i < len(configData.MemberData); i++ {
        fmt.Println(configData.MemberData[i].BlogTitle)
    }
}g
実行
$ go run main.go
https://slack.com/api/channels.history
XXXXXX
もがき系プログラマの日常
XXXXXXXXXXXXXXX

日付の計算とかどうする?

月曜日基準で考えて、水曜・金曜・日曜にリマインド通知を行っているので、それぞれの曜日で月曜までの時間の差を求めるみたいなことをしていました。

その求めた時間を、slackの発言履歴の時間の範囲指定で使うためです。

Pythonでは以下みたいなコードで対象の日付を取得してました。

def execute():
    nowDate    = GetNowDate()
    thisMonday = getThisMonday(nowDate.replace(hour=0)) #開始日は0時から取得したいので、

def getWeekDayNumber():
    u"""
    曜日の番号を返す
    @return int
    """
    return datetime.date.today().weekday()

def GetNowDate():
    u"""
    現在の日付を取得
   15時に通知するためhourは決め打ち
    @return string
    """
    return datetime.datetime.now().replace(hour=15, minute=0, second=0, microsecond=0)
    
def getThisMonday(nowDate):
    u"""
    今週の月曜の日付を取得
    @return string
    """
    weekday = getWeekDayNumber()
    return nowDate + datetime.timedelta(days=-weekday)

Goではいろいろ調べて以下みたいな感じにしました。

func main() {
    nowDate := GetNowDate()
    thisMonday := GetThisMonday(nowDate)

    fmt.Println("現在の日付 => " + nowDate.String())
    fmt.Println("月曜の日付 => " + thisMonday.String())
}

/**
 * 曜日の番号を返す
 * Goの場合
 * 0 => 日曜
 * 6 => 土曜
 *
 * になるので、Pythonにあわすため、以下にする
 * 0 => 月曜
 * 6 => 日曜
 */
func GetWeekDayNumber() int {
    weekday := int(time.Now().Weekday()) - 1
    if weekday == -1 {
        weekday = 6
    }

    return weekday
}

/**
 * 現在の日付を取得する
 */
func GetNowDate() time.Time {
    t := time.Now()
    return time.Date(t.Year(), t.Month(), t.Day(), 15, 00, 00, 0, time.Local)
}

/**
 * 今週の月曜の日付を取得する
 */
func GetThisMonday(nowDate time.Time) time.Time {
    weekday := GetWeekDayNumber()
    nowDate = time.Date(nowDate.Year(), nowDate.Month(), nowDate.Day(), 00, 00, 00, 0, time.Local)
    return nowDate.Add(time.Duration(-24*weekday) * time.Hour)
}
$ go run main.go
現在の日付 => 2018-10-14 15:00:00 +0900 JST
月曜の日付 => 2018-10-08 00:00:00 +0900 JST

URLエンコードどうする?

net/urlパッケージの Encode関数で再現できました。

func sendMessageAPI(configData ConfigData, latest time.Time, oldest time.Time) {
    params := url.Values{}
    params.Set("token", configData.Slack.RequestToken)
    params.Add("channel", configData.Slack.GetChannelID)
    params.Add("latest", strconv.FormatInt(latest.Unix(), 10))
    params.Add("oldest", strconv.FormatInt(oldest.Unix(), 10))
    params.Add("count", "700")
    fmt.Println(params.Encode())
}
$ go run main.go
channel=XXXXXX&count=700&latest=1539496800&oldest=1538924400&token=XXXXXX

また、ここを調べてるところで、

int64 => stringへのキャスト方法は、いつものstrconv.Atoi()ではなく、strconv.FormatInt()である

ということも学びました。第2引数の10は進数とのことです。

jsonのデータをデコードしたい

slackのchannel.historyAPIを実行し、結果のjsonを配列にしてpythonでは処理してました。

goではどうするのかなと調べてると、構造体とかに保存する必要があるみたいです。。

うぇぇぇ。。と思ってたら、jsonのデータをもとに構造体の定義を生成するサイトが合ったので、ソッコーで使いました。

JSON-to-Go

パッケージは encoding/json を使うようです。

type ChannelHistoriesData struct {
    Ok       bool   `json:"ok"`
    Latest   string `json:"latest"`
    Oldest   string `json:"oldest"`
    Messages []struct {
        Type        string `json:"type"`
        User        string `json:"user,omitempty"`
        Text        string `json:"text"`
        ClientMsgID string `json:"client_msg_id,omitempty"`
        ThreadTs    string `json:"thread_ts,omitempty"`
        Ts          string `json:"ts"`
        Username    string `json:"username,omitempty"`
        Icons       struct {
            Image36 string `json:"image_36"`
            Image48 string `json:"image_48"`
            Image72 string `json:"image_72"`
        } `json:"icons,omitempty"`
    } `json:"messages"`
    HasMore bool `json:"has_more"`
}

/**
 * ChannelHistoryAPIを実行する
 */
func sendChannelHistoryAPI(configData ConfigData, latest time.Time, oldest time.Time) {
    // パラメータをURLエンコードしたものを取得
    params := url.Values{}
    params.Set("token", configData.Slack.RequestToken)
    params.Add("channel", configData.Slack.GetChannelID)
    params.Add("latest", strconv.FormatInt(latest.Unix(), 10))
    params.Add("oldest", strconv.FormatInt(oldest.Unix(), 10))
    params.Add("count", "200")
    urlParams := params.Encode()

    // channel history apiを実行する
    response, httpRequestError := http.Get(configData.Slack.GetAPIURL + "?" + urlParams)
    if httpRequestError != nil {
        panic("sendChannelHistoryAPIのリクエストに失敗しました。")
    }
    defer response.Body.Close()

    // responseBodyを最終的に配列へ変換する
    byteData, _ := ioutil.ReadAll(response.Body)
    data := new(ChannelHistoriesData)
    jsonDecodeError := json.Unmarshal(byteData, data)
    if jsonDecodeError != nil {
        panic("JSONのデコードに失敗しました。")
    }
 
    return data
}

配列内(Slice内)の重複データを削除したい

現状の通知プログラムでは、通知アプリが発言したデータを配列に入れていたので、同じことをしようと思ったのですが、phpでいうarray_unique()みたいなのは調べるとGoではないようだったので、参考サイト様をみつけたので、使わせていただきました。

https://ema-hiro.hatenablog.com/entry/20170712/1499785693

/**
 * Slice内に存在する重複した値を削る
 */
func uniqueSlice(data []string) []string {
    m := make(map[string]bool)
    results := []string{}
    for _, ele := range data {
        if !m[ele] {
            m[ele] = true
            results = append(results, ele)
        }
    }

    return results
}

一旦trueを入れておいて、次のループでtrueが入ってたら、if文に入らず結果uniqueになるということです。

配列を文字列で結合したい

strings パッケージの、Joinで出来ました。

/**
 * Slackへ送信する用のメッセージを作成する
 */
func MakeNormalSendText(configData ConfigData, messageData []string) string {
    targetUsers := getTargetUsers(configData, messageData)
    textData := "<!channel>\nまだブログを書けていないユーザーがいます!\n今週中に書けるようみんなで煽りましょう!\n書けていないユーザー\n================\n" + strings.Join(targetUsers, "\n")

    return textData
}

終わりに

とりあえず移植は成功しました。

ただ、こんな夜中にテストで通知を飛ばしてしまって、すぐ消しましたが、ご迷惑をおかけしました。。。

f:id:kojirooooocks:20181014234759p:plain

あーまにあった。。。

Go言語 勉強(基礎編2)

はじめに

前回の続きです。

やってみた

制御構文

if文

if の条件式の中で変数定義ができ、その変数のスコープはif文の中のみ有効になる

func main() {
    if aaa := "test"; aaa == "test" {
        fmt.Println("OK => " + aaa)
    } else {
        fmt.Println("NG => " + aaa)
    }

    // ここではaaaは使えない
    // fmt.Println(aaa)
}
// OK => test

switch文

switch文の各caseでbreakは必要ない。

func main() {
    var aaa string = "午後"
    switch aaa {
    case "午前":
        fmt.Println("おはよう")
    case "午後":
        fmt.Println("こんにちは")
    case "夜":
        fmt.Println("こんばんは")
    }
}
// こんにちは

if文と同じような書き方もできる

func main() {
    switch aaa := "午後"; aaa {
    case "午前":
        fmt.Println("おはよう")
    case "午後":
        fmt.Println("こんにちは")
    case "夜":
        fmt.Println("こんばんは")
    }

    // ここではaaaは使えない
    // fmt.Println(aaa)    
}
// こんにちは

こんな書き方も可能

func main() {
    var aaa string = "午後"
    switch {
    case aaa == "午前":
        fmt.Println("おはよう")
    case aaa == "午後":
        fmt.Println("こんにちは")
    case aaa == "夜":
        fmt.Println("こんばんは")
    }
}
// こんにちは

関数

複数の戻り値が設定できる

func main() {
    var price, discountPrice = GetPrice(20)
    fmt.Println("価格 => " + strconv.Itoa(price))
    fmt.Println("割引価格 => " + strconv.Itoa(discountPrice))
}

func GetPrice(discount int) (int, int) {
    var price int = 100
    return price, price - discount
}
// 価格 => 100
// 割引価格 => 80

戻り値に名前をつけられる

func main() {
    var normalPrice, discountPrice = GetPrice(100)
    fmt.Println("通常価格 => " + strconv.Itoa(normalPrice))
    fmt.Println("割引価格 => " + strconv.Itoa(discountPrice))
}

func GetPrice(discount int) (normalPrice int, discountPrice int) {
    // 戻り値の名前をしていると、それをそのまま関数内で使用できて、returnに値をセットする必要がなくなる
    normalPrice = 500
    discountPrice = normalPrice - discount
    return
}
// 通常価格 => 500
// 割引価格 => 400

関数を変数に代入できる。型を指定しなければいけないところがなるほどと思った。

func main() {
    var priceFunc func(int) (int, int)
    priceFunc = GetPrice
    fmt.Println(priceFunc(200))
}

func GetPrice(discount int) (normalPrice int, discountPrice int) {
    normalPrice = 500
    discountPrice = normalPrice - discount
    return
}
// 500 300

defer文

defer で指定した処理が、関数の終了時に実行されるようになる。

__destruct()みたいな感じかもしれない。

func main() {
    Story()
}

func Story() {
    defer fmt.Println("おしまい")

    fmt.Println("昔々あるところにおじいさんとおばあさんがいました")
    fmt.Println("おじいさんは山へリオレウスを狩りに")
    fmt.Println("おばあさんは川へフルフルを狩りにでかけました")
}
// 昔々あるところにおじいさんとおばあさんがいました
// おじいさんは山へリオレウスを狩りに
// おばあさんは川へフルフルを狩りにでかけました
// おしまい

パニック

処理を停止させる。関数?

スタックトレースが表示される。

func main() {
    Story()
}

func Story() {
    fmt.Println("昔々あるところにおじいさんとおばあさんがいました")
    fmt.Println("おじいさんは山へリオレウスを狩りに")
    panic("パニック!モンハンと桃太郎の世界戦が一緒だ!")
 
    // ↑でpanicを呼び出しているので、このPrintは呼び出されない
    fmt.Println("おばあさんは川へフルフルを狩りにでかけました")
}
// 昔々あるところにおじいさんとおばあさんがいました
// おじいさんは山へリオレウスを狩りに
// panic: パニック!モンハンと桃太郎の世界戦が一緒だ!
// goroutine 1 [running]:
// main.Story()
//         /xxx/xxx/study_02/main.go:72 +0xbd
// main.main()
//         /xxx/xxx/study_02/main.go:63 + 0x20
// exit status 2

ちなみにdeferをつけると、deferを呼び出した後、panicへ流れるようだった。

func main() {
    Story()
}

func Story() {
    defer fmt.Println("おしまい")
    fmt.Println("昔々あるところにおじいさんとおばあさんがいました")
    fmt.Println("おじいさんは山へリオレウスを狩りに")
    panic("パニック!モンハンと桃太郎の世界戦が一緒だ!")
 
    // ↑でpanicを呼び出しているので、このPrintは呼び出されない
    fmt.Println("おばあさんは川へフルフルを狩りにでかけました")
}
// 昔々あるところにおじいさんとおばあさんがいました
// おじいさんは山へリオレウスを狩りに
// おしまい
// panic: パニック!モンハンと桃太郎の世界戦が一緒だ!
// goroutine 1 [running]:
// main.Story()
//         /xxx/xxx/study_02/main.go:72 +0x119
// main.main()
//         /xxx/xxx/study_02/main.go:63 +0x20
// exit status 2

メソッド

前回の続き でも使用したけど。もう一回おさらい

ポインタ部分がすぐ変換できない。。。

type Item struct {
    name  string
    price int
    stock int
}

func (item Item) GetName() string {
    return item.name
}
func (item Item) GetPrice() int {
    return item.price
}
func (item Item) GetStock() int {
    return item.stock
}
func (item *Item) Reduce(count int) {
    item.stock -= count
}

func main() {
    var item = Item{
        name:  "黄金の林檎",
        price: 500,
        stock: 5,
    }

    item.Reduce(2)
    item.Reduce(1)

    fmt.Println("アイテム名: " + item.GetName())
    fmt.Println("金額: " + strconv.Itoa(item.GetPrice()))
    fmt.Println("在庫: " + strconv.Itoa(item.GetStock()))
}
// アイテム名: 黄金の林檎
// 金額: 500
// 在庫: 2

インターフェース

こんな形かな? もうちょっと規模が大きいものを書いて、その時に使用してみたい。

type Car interface {
    EngineStart()
}

type MyCar struct {
    started bool
}

func (myCar MyCar) EngineStart() {
    myCar.started = true
    fmt.Println("エンジン点火!ブルルルーン!")
}

func main() {
    var myCar Car = MyCar{
        started: false,
    }

    myCar.EngineStart()
}
// エンジン点火!ブルルルーン!

エラーハンドリング

Go言語例外処理がないらしいが、Errorインターフェースというのがあるみたい。

用意されてる標準関数とかも、エラー時はErrorインターフェースを返すみたいだ。

自作の関数とかでErrorを返却したい場合は、errorsパッケージを読み込んで New関数を使うよう。

import (
    "errors"
    "fmt"
    "strconv"
)

type Item struct {
    stock int
}

func (item Item) GetStock() int {
    return item.stock
}
func (item *Item) Reduce(count int) error {
    item.stock -= count
    if item.stock <= 0 {
        return errors.New("在庫はもうない!")
    }

    return nil
}

func main() {
    var item = Item{
        stock: 5,
    }

    for i := 0; i < 5; i++ {
        fmt.Println(strconv.Itoa(i+1) + "回目の購入!")
        e := item.Reduce(2)
        if e != nil {
            fmt.Println(e.Error())
            break
        }
        fmt.Println("在庫を2つ消費した!現在の在庫は" + strconv.Itoa(item.GetStock()))
    }
}
// 1回目の購入!
// 在庫を2つ消費した!現在の在庫は3
// 2回目の購入!
// 在庫を2つ消費した!現在の在庫は1
// 3回目の購入!
// 在庫はもうない!

ゴルーチン

並列処理を行える。

ゴルーチンで呼び出した関数の終了を待たないので、戻り値は受け取れない。

mainから呼び出した場合は、mainの処理が終了した時点で、呼び出したゴルーチンも途中で処理を中断して終了する。

import (
    "fmt"
    "time"
)

func main() {
    CountUp()
    go CountUp()

    time.Sleep(2 * time.Second)
    fmt.Println("Count Up終わり!")
}

func CountUp() {
    defer fmt.Println("おしまい")
    for count := 0; count < 5; count++ {
        fmt.Println(count)
        time.Sleep(1 * time.Second)
    }
}
// 0
// 1
// 2
// 3
// 4
// おしまい
// 0
// 1
// 2
// Count Up終わり!

チャネル

ゴルーチン間で値の送受信を行うために使う。

import (
    "fmt"
    "time"
)

func main() {
    var ch = make(chan string)

    go CountUp("Goルーチン1号君", ch)
    go CountUp("Goルーチン2号君", ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    close(ch)
}

func CountUp(title string, ch chan<- string) {
    fmt.Println(title + " 起床")
    time.Sleep(300 * time.Millisecond)
    fmt.Println(title + " 朝食")
    time.Sleep(300 * time.Millisecond)
    fmt.Println(title + " 通勤")
    time.Sleep(300 * time.Millisecond)
    fmt.Println(title + " 勤務")
    time.Sleep(300 * time.Millisecond)
    fmt.Println(title + " 帰宅")

    ch <- title + " 就寝。今日も一日お疲れ様"
}
// Goルーチン2号君 起床
// Goルーチン1号君 起床
// Goルーチン2号君 朝食
// Goルーチン1号君 朝食
// Goルーチン1号君 通勤
// Goルーチン2号君 通勤
// Goルーチン1号君 勤務
// Goルーチン2号君 勤務
// Goルーチン1号君 帰宅
// Goルーチン1号君 就寝。今日も一日お疲れ様
// Goルーチン2号君 帰宅
// Goルーチン2号君 就寝。今日も一日お疲れ様

とりあえずサンプル書いてみたけど、チャネルはあまり理解できてない。。

すごく単純なプログラムだからだと思うけど、この動きを見ると、goルーチンが終わるまで待っている感じ。

もう少し深く勉強しなければ。

おわりに

前回書いた後いろいろと調べると、変数も関数もUpperCamelCaseで書いている記事が複数あったのですが、関数は UpperCamelCase, 変数は LowerCamelCaseと分けている記事もありました。

参考にしている資料は、以下だったので、とりあえずこっちに合わせておこうと思います。

関数は UpperCamelCase, 変数は LowerCamelCase

もうちょい基礎頑張って次はフレームワークも触ってみます。

あ、10/8は技術書典行きます!

Go言語の勉強でありがたい指摘を頂いた

はじめに

前回のGo言語の勉強記事を書いた時、通知で例のブログSlackに流れるようになってるのですが、その場で、いろいろと指摘いただいたので、忘れないために、それをブログにまとめようと思いました。

消して記事数を稼ごうとしているわけではありません。

指摘事項

1. ローカル変数に関しては、型を指定できず、宣言も := で行う

どのスコープの話しているのかわかりませんが、ローカル変数でも型指定できますよ。

参考

他言語プログラマがgolangの基本を押さえる為のまとめ

これは、自分が勘違いしていたのですが、 := で指定するものが、ローカルの変数で、varを指定したものがグローバルな変数だと勘違いしていただけでした。

実際は、そうではなく、参考で記載されているとおり省略して書ける書き方として := があるみたいです。

fun main() {
    var data01 string = "ぼくはローカル変数だよ "

    data02 := "ぼくもローカル変数だよ varとか型宣言を省略できるよ"
}

2. package package_test というパッケージ名

`XXX_test`っていうパッケージ名は通常ビルドに含まれない特殊なパッケージ名なのでやめましょう。

参考

https://swet.dena.com/entry/2018/01/16/211035

パッケージ名_test をテストコードを記述するパッケージ名として利用することもできます。 パッケージ外に公開したAPIを通してのみテストしたい場合は、こちらを利用するとよさそうです。

まだ触ってなかったのですけど、Go言語でtestコードを書くためのtestingパッケージというのがあるらしいのですが、本来はそちらで使用されるべきパッケージ名で(XXX_test)というパッケージ名をつけるようです。

なるほどでした!

3. var data_array

Goでスネークケース書くのはやめましょう。フォーマッタでも怒られます。

参考

これからGolang開発を行うRubyistたちへ

Goでは、変数と関数は最初が大文字で始まるキャメルケースが使われています。

つまりUpperCamelCaseでかく必要があるってことのようです。

golintというのを入れて、実際にsnake_caseで書かれている変数を使用しているgoファイルをチェックしてみるとたしかにエラーが出ました!

$ golint struct.go 
struct.go:34:6: don't use underscores in Go names; var data_array should be dataArray

4. if player.is_die() == true {

bool関数の戻り値を比較するのはやめましょう。 `if player.is_die() {`でいいです(これはあまりGo関係ない

これが一番なるほどと思いましたw

これは自分の今お仕事している案件が、phpかつ、5.6系のプロジェクトであることに起因しているかもしれないなと思いました。

そもそもスカラ型の型指定がないphp5系のプロジェクトだと、自分は明示的に今回のように == true (phpの場合は === true)とつけて、確実にbooleanが返ってきてるよっていうのをコード上で示してあげる癖がついてました。

冗長だから必要ないという話もありますが、それよりも可読性のほうが重要かなと思ってそうしていました。

ただ、Go言語(phpも7系から)はそもそも戻り値の型の指定ができるのだから、booleanであることは明白なので、そもそも必要ないですよねということでした。

5. そこまで大規模に使うものでもないのかもしれません。

一応Goは大規模開発用(多人数開発用)に作られました(だから型があって予約語が少ない。formatterとかもろもろ標準装備されている) GAEとかLinuxじゃなくてGo製の疑似OS(gVisor)のコンテナーで動いてます。あとメルカリなどはPHP全部Goに置き換えてますね。

完全に僕の知識不足でしたw

終わりに

いろいろと指摘していただいて、とてもありがたかったです!

また、おすすめの本まで紹介してもらいました!

基本的なところを勉強した後、早速1冊購入してみようと思います!

また、このslackチームもいい循環ができていて、作ったかいがありました。

f:id:kojirooooocks:20181003145314p:plain

これからも頑張ろう。

Go言語 勉強(基礎編1)

はじめに

こんばんは。 今年も早くも10ヶ月たって、全く成長してない僕です。

今年のはじめに決めた目標はブレブレになってしまって、どれもこれも中途半端という不甲斐ない結果になっております。

まぁ嘆いてもしょうがないので、今月から気を引き締めて、またやっていこうと思います。

とりあえず今年10月から来年も踏まえて、Go、PythonJavascriptの3つを重点的に勉強しようと思ってます。

比率的には

Go: 4 Javascript: 4 Python: 2

といった比率で勉強しようと思ってます。

なんでこの比率かというと、

Goは、もともと勉強したいというのがあったのですが、知人からGoの案件を紹介してもらったことがきっかけでした。その時は実務経験も勉強もしてない状態だったのでスキル的にミスマッチだったので断ったのですが、せっかくのお誘いを自分のスキル不足で台無しにしてしまうのが悔しかったので、やろうと思いました。

Javascriptは、今年の目標の一つで、フロントエンド側の知見を貯めるというのに続いています。何回かReactやVue.jsの勉強やりましたが、実践で使えるほどの知見は全然溜まってないので、もう少し踏み込んで勉強をしていこうと思っています。FWはReactに絞って勉強しようと思ってます。

Pythonに関しては、唯一成長したというか、調べつつ書けるようになってきている実感があるので、ある程度勉強は続けていこうと思いこの比率です。

あとはSwiftも個人的にやってた時期があるので、どうにか滑り込ませたいなと思ってますが、時間的に厳しいかも。。。

とりあえず今回はGo言語の勉強ですね。

大昔にTour of Goやったくらいなので、さっぱり忘れてます。

さっそくいきます。

やってみた

定番のHello World

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

go build goファイルで、ビルドされたバイナリが作成されて、そのバイナリを実行すれば、Hello Worldが表示される

$ go build hello.go
$ ./hello
Hello World

$ ls -la
total 3920
drwxr-xr-x   4 kojirock  staff      128 10  3 00:07 .
drwxr-xr-x  21 kojirock  staff      672 10  2 23:54 ..
-rwxr-xr-x   1 kojirock  staff  1999368 10  3 00:07 hello
-rw-r--r--   1 kojirock  staff       72 10  3 00:01 hello.go

go run goファイル名で、ビルドと実行が同時に行われるけど、対象のバイナリは作られない

$ go run hello.go
Hello World

$ ls -la
total 8
drwxr-xr-x   3 kojirock  staff   96 10  3 00:03 .
drwxr-xr-x  21 kojirock  staff  672 10  2 23:54 ..
-rw-r--r--   1 kojirock  staff   72 10  3 00:01 hello.go

制御構文

  1. 繰り返し処理はforのみ、while, foreach, do whileなどはない。でもforのみで再現できる。
// 普通のfor
for i:=0; i < 5; i++ {
    fmt.Println(i)
}

// while
count := 0
for count < 10 {
    fmt.Println(count)
    count++
}

// foreach的なやつ
data_list := map[string]int{
    "aaa": 10,
    "bbb": 20,
    "ccc": 30,
}
for key, value := range data_list {
    fmt.Println("key => ", key, " value => ", value)
}

衝撃だったけど、forしかないことで逆にシンプルだと思った。

break continue は通常通り使えるとのこと。

関数とか変数とか

  1. 頭文字が小文字のメンバは、同パッケージ内からのみアクセス可能
  2. 頭文字が大文字のメンバは、別パッケージからもアクセス可能
  3. 基本的に変数はvar, 関数はfuncで定義
  4. 変数は型推論があるので、型の省略も可能
  5. ローカル変数に関しては、型を指定できず、宣言も := で行う
  6. const定義も可能で、かつ、型を持たない定数が可能。また、関数内でも定義できる。
  7. iotaという列挙型みたいなものをconstでは指定できる。

テストのために自分で作成したローカルパッケージを作成してimportする

f:id:kojirooooocks:20181003025445p:plain

test_01.go

package package_test

// All package_testの全メソッドを呼ぶ
func All() {
    aaa()
    bbb()
    ccc()
    ddd()
    println(bbbvar)
    println(dddconst01)
}

func aaa() {
    println("aaa")
}

test_02.go

package package_test

var bbbvar = 100

func bbb() {
    println("bbb")
}

test_03.go

package package_test

func ccc() {
    cccvar := 200
    println("ccc")
    println(cccvar)
}

test_04.go

package package_test

const dddconst01 = "dddconst01"

func ddd() {
    const dddconst02 = 2
    println(dddconst02)
    println("ddd")
}

hello.go

package main

import "./package_test"

const (
    Red = iota
    Blue
    Yellow
)

func main() {
    package_test.All()
    println(Red)
    println(Blue)
    println(Yellow)
}
$ go run hello.go
aaa
bbb
ccc
200
2
ddd
100
dddconst01
0
1
2

いろんな型

配列型

固定長の配列を作成する。

// 定義1
var data_array [3]int

// 定義2(初期値を指定できる)
var data_array = [3]int{10, 20, 30}

// 定義3(初期値を指定すると、長さの宣言は...で省略できる)
var data_array = [...]int{10, 20, 30}

// 定義4(indexを指定して初期値を設定できる)
var data_array = [...]int{0: 10, 1: 20, 2: 30}

スライス型

可変長の配列を作成する。

// 定義1
var data_array []int

// 定義2(初期値を指定できる)
var data_array = []int{10, 20}

// 追加方法はappend()
var data_array = []int{10, 20}
data_array = append(data_array, 30)
println(data_array[2])
// => 30

マップ型

phpでいう連想配列かな

var data_array = map[string]int{"aaa": 10, "bbb": 20}
data_array["ccc"] = 30 // 代入は簡単

型宣言(type)

独自の型を作成できる。

reflectパッケージの TypeOf() で指定のものがなんの型がわかる。

package main

import "fmt"
import "reflect"

type TestType int

var param1 TestType = 100
var param2 int = 200

func main() {
    fmt.Println(param1)
    fmt.Println(param2)
    fmt.Println(reflect.TypeOf(param1))
    fmt.Println(reflect.TypeOf(param2))
}
$ go run type.go
100
200
main.TestType
int

学生時代C言語やってたときにこんな事やった記憶があるような気がする。

構造体(Struct)

Goはクラスがないから構造体。めちゃ懐かしい。

  1. math/randパッケージを使用して乱数を生成している
  2. strconvパッケージを使用して、数値から文字列へのキャストをこなっている
package main

import (
    "fmt"
    "math/rand"
    "strconv"
    "time"
)

type Player struct {
    life   int
    attack int
    luck   int
}

func (p *Player) is_die() bool {
    return p.life == 0
}
func (p *Player) damege(point int) {
    p.life -= point
    if p.life < 0 {
        p.life = 0
    }
}

type Enemy struct {
    life   int
    attack int
    luck   int
}

func (e *Enemy) is_die() bool {
    return e.life == 0
}
func (e *Enemy) damege(point int) {
    e.life -= point
    if e.life < 0 {
        e.life = 0
    }
}

func main() {
    var player Player
    var enemy Enemy

    player.life = 10
    player.attack = 1
    player.luck = 5

    enemy.life = 10
    enemy.attack = 1
    enemy.luck = 5

    // 無限ループはこの書き方
    for {
        player_attack_offet := 0
        enemy_attack_offet := 0
        rand.Seed(time.Now().UnixNano())
        if player.luck >= rand.Intn(10) {
            // 会心の一撃出す
            player_attack_offet++
        }

        if enemy.luck >= rand.Intn(10) {
            // 痛恨の一撃出す
            enemy_attack_offet++
        }

        enemy.damege(player.attack + player_attack_offet)
        fmt.Println("プレイヤーの攻撃!敵に" + strconv.Itoa(player.attack+player_attack_offet) + "のダメージを与えた!")

        player.damege(enemy.attack + enemy_attack_offet)
        fmt.Println("敵の攻撃!プレイヤーに" + strconv.Itoa(enemy.attack+enemy_attack_offet) + "のダメージを与えた!")

        if player.is_die() == true {
            fmt.Println("敵の勝ち!")
            break
        } else if enemy.is_die() == true {
            fmt.Println("プレイヤーの勝ち!")
            break
        }
    }
}
$ go run struct.go
プレイヤーの攻撃!敵に2のダメージを与えた!
敵の攻撃!プレイヤーに1のダメージを与えた!
プレイヤーの攻撃!敵に2のダメージを与えた!
敵の攻撃!プレイヤーに1のダメージを与えた!
プレイヤーの攻撃!敵に1のダメージを与えた!
敵の攻撃!プレイヤーに1のダメージを与えた!
プレイヤーの攻撃!敵に1のダメージを与えた!
敵の攻撃!プレイヤーに2のダメージを与えた!
プレイヤーの攻撃!敵に2のダメージを与えた!
敵の攻撃!プレイヤーに1のダメージを与えた!
プレイヤーの攻撃!敵に2のダメージを与えた!
敵の攻撃!プレイヤーに1のダメージを与えた!
プレイヤーの勝ち!

ダラダラと長くなってしまったけど、こんな感じなのかな?

ポインタ型

C言語でお馴染みのポインタ。確か僕が挫折したのがポインタ。

ただ、ロジックの見通しよくするためには、必要だし、苦手意識は克服しなければ。

func main() {
    var player Player
    var enemy Enemy

    player.life = 10
    player.attack = 1
    player.luck = 5

    enemy.life = 10
    enemy.attack = 1
    enemy.luck = 5

    // 無限ループはこの書き方
    for {
        battle(&player, &enemy)
        if player.is_die() == true {
            fmt.Println("敵の勝ち!")
            break
        } else if enemy.is_die() == true {
            fmt.Println("プレイヤーの勝ち!")
            break
        }
    }
}

func battle(player *Player, enemy *Enemy) {
    player_attack_offet := 0
    enemy_attack_offet := 0
    rand.Seed(time.Now().UnixNano())
    if player.luck >= rand.Intn(10) {
        // 会心の一撃出す
        player_attack_offet++
    }

    if enemy.luck >= rand.Intn(10) {
        // 痛恨の一撃出す
        enemy_attack_offet++
    }

    enemy.damege(player.attack + player_attack_offet)
    fmt.Println("プレイヤーの攻撃!敵に" + strconv.Itoa(player.attack+player_attack_offet) + "のダメージを与えた!")

    player.damege(enemy.attack + enemy_attack_offet)
    fmt.Println("敵の攻撃!プレイヤーに" + strconv.Itoa(enemy.attack+enemy_attack_offet) + "のダメージを与えた!")
}
$ go run struct.go
プレイヤーの攻撃!敵に1のダメージを与えた!
敵の攻撃!プレイヤーに1のダメージを与えた!
プレイヤーの攻撃!敵に1のダメージを与えた!
敵の攻撃!プレイヤーに2のダメージを与えた!
プレイヤーの攻撃!敵に2のダメージを与えた!
敵の攻撃!プレイヤーに2のダメージを与えた!
プレイヤーの攻撃!敵に1のダメージを与えた!
敵の攻撃!プレイヤーに2のダメージを与えた!
プレイヤーの攻撃!敵に2のダメージを与えた!
敵の攻撃!プレイヤーに2のダメージを与えた!
プレイヤーの攻撃!敵に1のダメージを与えた!
敵の攻撃!プレイヤーに2のダメージを与えた!
敵の勝ち!

おわりに

とりあえず今日はここまでです。

やってみて思ったけど、すごくシンプルで覚えることも少なそうだけど、シンプルだからこそ、設計力が求められそうな感じだと思いました。

でも、Go言語でガッツリwebサービス作るっていうよりはAPI的な立ち位置で使ってるケースが多いからそこまで大規模に使うものでもないのかもしれません。

でもGoのフレームワークもあるみたいだし、せっかくだからwebサービス作る方向で勉強してみようとおもいます。

おすすめの本などあれば教えてください。

そういえば、最近やっとsublimeからvscodeに乗り換えたんですけど、すごくいいですね。同じElectronで作られてるAtomはえらい違いです。

php or jsはphpstorm

pythonはspyder

それ意外のものに関してはvscode

って感じで定着しそうです(もはやpythonvscodeになりそう)

おやすみなさい。

Ruby勉強その2(基礎の基礎)

はじめに

前回の続きです。

CODEPREPでRubyやってます。

やってみた

空Hashの作成方法

hash_data = Hash.new()

new Hash() かとおもったら逆だった。

配列からのHash化

array_data = ['AAA', 'aaa', 'BBB', 'bbb', 'CCC', 'ccc']
hash_data = Hash[*array_data]
p hash_data

=> {"AAA"=>"aaa", "BBB"=>"bbb", "CCC"=>"ccc"}

これ結構便利だなぁ。

phpではこの関数なさそう。たぶん。

array_combineとかならあるけど。

Hash(key, valueの組み合わせ)の配列化

hash_data = {'AAA' => 'aaa', 'BBB' => 'bbb', 'CCC' => 'ccc'}
p hash_data
=> {"AAA"=>"aaa", "BBB"=>"bbb", "CCC"=>"ccc"}

p array_data.to_a
=> [['AAA', 'aaa'], ['BBB', 'bbb'], ['CCC', 'ccc']]

簡単に変換できるのは楽だ。

こういうところも人気なひとつなのかな。

Hashのkeyやvalueをすべて取得

key

hash_data = {'AAA' => 'aaa', 'BBB' => 'bbb', 'CCC' => 'ccc'}
hash_data.each_key do |key|
  p key
end
=> 'AAA'
=> 'BBB'
=> 'CCC'

p hash_data.keys
=> ['AAA', 'BBB', 'CCC']

value

hash_data = {'AAA' => 'aaa', 'BBB' => 'bbb', 'CCC' => 'ccc'}
hash_data.each_value do |val|
  p val
end
=> 'aaa'
=> 'bbb'
=> 'ccc'

p hash_data.values
=> ['aaa', 'bbb', 'ccc']

keysvalues は、 array_keys()array_values() なんかと一緒だけど、一個ずつ取り出せるってのもあるようだ。

Hashの各存在確認

Hashが空かどうか

hash_data = {}
if hash_data.empty?
  p "empty!"
end
=> empty!

Hashに指定のkeyが存在するか

hash_data = {'AAA' => 'aaa', 'BBB' => 'bbb', 'CCC' => 'ccc'}
if hash_data.key?('AAA')
  p "exists!"
end
=> exists!

phpではisset() とか empty() を使う感じだけど、rubyでは ? を使うのか。

なんかかわいい。

終わりに

細かいところが違うけど、続けてやるとある程度わかってきた。

次もRubyやろう。Goの勉強はある程度Rubyやってからだな。。。

続けてやらないとすぐ忘れる。

あ、シュタゲゼロ始まる。

ではでは。

勢いで作ったslackグループで書いていない人にメンション送るようにした。

先日作ったslackグループにどんどん人が入ってきてくれて、若干引いています。

それはそれとして、書いてない人にリマインド飛ばせるようにしたいなと思ったので、lambda + cloudwatchで定期的に通知飛ばせるようにしました。

SlackにRSS登録してその通知を受け取って、通知がきていない = 書いていない人とみなして連絡するようにしました。

この手法のデメリットは毎回RSSを登録してもらう必要があるんですよね。。。

書いたコードが実際に動くのが来週なので、もしかしたらバグってる可能性ありますが、一旦公開しておきます。

実際動いてバグってたら修正します。

# coding: UTF-8
import os
import sys
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor'))

import urllib
import requests
import json
import datetime
import yaml


def getMessageApi():
    u"""
    Slackの特定チャンネルからメッセージを取得する 
    特定の投稿のみを抜き取るのが難しいので、1週間単位で取得するようにした
    @return array
    """
    config_data = getConfigData();
    latest      = datetime.datetime.now()
    oldest      = latest + datetime.timedelta(weeks=-1)
    params      = urllib.parse.urlencode({
        'token'   : config_data['slack_token'],
        'channel' : config_data['slack_get_message_channel_id'],
        'oldest'  : oldest.timestamp(),
        'latest'  : latest.timestamp(),
        'count'   : 1000
    })

    results  = []
    response = requests.get(config_data['slack_get_message_api_url'] + '?' + params).json()
    for message_data in response['messages']:
        if 'username' in message_data:
            results.append(message_data['username'])

    return results



def sendMessage(message_data):
    u"""
    Slackの特定チャンネルにメッセージを投稿する
    @param array message_data
    @return none
    """
    config_data = getConfigData();
    requests.post(config_data['slack_send_message_api_url'], data = json.dumps({
        'text'       : makeSendText(message_data),
        'channel'    : config_data['slack_send_message_channel_name'],
        'link_names' : 1
    }))



def makeSendText(message_data):
    u"""
    Slackへ送信するようのメッセージを作成する
    @param array message_data
    @return string
    """
    config_data  = getConfigData()
    target_users = []
    for config in config_data['mapping_table']:
        if not config['blog_name'] in message_data:
             target_users.append("<@" + config['user_name'] + ">さん")

    text_data = "<!channel>\n今週ブログを書けていないユーザーがいます!\n今週中に書けるようみんなで煽りましょう!\n書けていないユーザー\n================\n" + "\n".join(target_users)

    return text_data



def getConfigData():
    u"""
    コンフィグデータを取得する
    @return array
    """
    with open('./config.yml', 'r') as yml:
        config_data = yaml.load(yml)

    return config_data


def lambda_handler(event, context):
    message_data = getMessageApi()
    sendMessage(message_data)

勢いで週一ブログ書くslackグループを作った

例に漏れず、@kakakakakkuさんの影響でブログ書き始めて9ヶ月くらい立ったけど、書いているときとか結構孤独で、時々週イチで書けなくなるときがあります。

同じような人がもし入れば、お互いに応援というかいい意味で煽りあって、プレッシャーを与えあい、書き終えたらみんなでいいねと言い合う感じの健全な場があればいいなと思いました。

そんなときtwitterで以下のサイトをしりました。

shu-1blog.com

需要があるかどうかわかんないし、完全見切り発車ですけど、slackでもグループ作ってみるかなーとおもって作りました。

https://join.slack.com/t/write-blog-every-week/shared_invite/enQtNDQwMjM1MjU3NTU2LWMzOGJkMTcyODQ1YTU3NDE2M2E3ZDUwMDNmODFhMDhmMTdhZGM0ODU1NDFhOTIyODRlYzg0ZDU2YjExZmZkMGI

とりあえず細かいルールはなしで、

週イチでブログ書く。 書けてなければ煽る。 書いた際は褒める。

この3つだけできるグループでいいかなと。

全然需要なかったとしても一人slackとして活用しときますw

興味があれば入ってくださいー。