My Note

自己理解のためのブログ

金とプラチナの価格をGoとMackerelで可視化する

はじめに

今回は、Goでスクレイピングして金とプラチナの価格をMackerelで可視化します。

yhidetoshi.hatenablog.com こちらと同様にMackerelを使った可視化です。

前から金の積立をしているので価格が気になるのもあり、Mackerelでグラフ化しておきたいなー。という気持ちで。

前回、goqueryを使ってスクレイピングしたときの記事はこちらです。

yhidetoshi.hatenablog.com

アーキテクチャ

f:id:yhidetoshi:20190919182645p:plain

  1. CloudWatch EventでLambdaを定期実行
  2. Lmabda ( Go ) でサイトにスクレイピングでデータを取得
  3. Lambda ( Go ) でMackerelにメトリクスを投げる

Lambda ( Go )について

LambdaにはサイトをスクレイピングしてMackerelにメトリクスをPostする処理を定義して CloudWatch Eventを連携させます。Lambda関数やCloudwatchEventなどはServerlessFrameworkを利用してデプロイしました。

スクレイピングgoquery を利用しました。

godoc.org

*) Go: 1.12.4

  • main.go
package main

import (
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/PuerkitoBio/goquery"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/mackerelio/mackerel-client-go"
)

var (
    targetUrl = "https://gold.tanaka.co.jp/commodity/souba/index.php"
    mkrKey    = os.Getenv("MKRKEY")
    client    = mackerel.NewClient(mkrKey)
)

const (
    serviceName = "Metal"
    timezone    = "Asia/Tokyo"
    offset      = 9 * 60 * 60
)

func main() {
    lambda.Start(Handler)
}

// func main() {
func Handler() {
    var goldRetailTax, goldPurchaseTax, platinumRetailTax, platinumPurchaseTax string

    doc, err := goquery.NewDocument(targetUrl)
    if err != nil {
        fmt.Println(err)
    }

    // Fetch gold and platinum price
    doc.Find("#metal_price_sp").Each(func(_ int, s *goquery.Selection) {
        // Gold
        goldRetailTax = s.Children().Find("td.retail_tax").First().Text()
        goldPurchaseTax = s.Children().Find("td.purchase_tax").First().Text()
        // Platinum
        platinumRetailTax = s.Children().Find("td.retail_tax").Eq(1).Text()
        platinumPurchaseTax = s.Children().Find("td.purchase_tax").Eq(1).Text()
    })

    // Format
    strGoldRetailTax := strings.Replace(goldRetailTax[0:5], ",", "", -1)
    strGoldPurchaseTax := strings.Replace(goldPurchaseTax[0:5], ",", "", -1)
    strPlatinumRetailTax := strings.Replace(platinumRetailTax[0:5], ",", "", -1)
    strPlatinumPurchaseTax := strings.Replace(platinumPurchaseTax[0:5], ",", "", -1)

    // Convert string to int
    intGoldRetailTax, _ := strconv.Atoi(strGoldRetailTax)
    intGoldPurchaseTax, _ := strconv.Atoi(strGoldPurchaseTax)
    intPlatinumRetailTax, _ := strconv.Atoi(strPlatinumRetailTax)
    intPlatinumPurchaseTax, _ := strconv.Atoi(strPlatinumPurchaseTax)

    jst := time.FixedZone(timezone, offset)
    nowTime := time.Now().In(jst)

    mkrErr := PostValuesToMackerel(intGoldRetailTax, intGoldPurchaseTax, intPlatinumRetailTax, intPlatinumPurchaseTax, nowTime)
    if mkrErr != nil {
        fmt.Println(mkrErr)
    }
}

// PostValuesToMackerel Post Metrics to Mackerel
func PostValuesToMackerel(goldRetailTax int, goldPurchaseTax int, platinumRetailTax int, platinumPurchaseTax int, nowTime time.Time) error {
    // Post Gold metrics
    errGold := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{
        &mackerel.MetricValue{
            Name:  "Gold.retail_tax",
            Time:  nowTime.Unix(),
            Value: goldRetailTax,
        },
        {
            Name:  "Gold.purchase_tax",
            Time:  nowTime.Unix(),
            Value: goldPurchaseTax,
        },
    })
    if errGold != nil {
        fmt.Println(errGold)
    }

    // Post Platinum metrics
    errPlatinum := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{
        &mackerel.MetricValue{
            Name:  "Platinum.retail_tax",
            Time:  nowTime.Unix(),
            Value: platinumRetailTax,
        },
        {
            Name:  "Platinum.purchase_tax",
            Time:  nowTime.Unix(),
            Value: platinumPurchaseTax,
        },
    })
    if errPlatinum != nil {
        fmt.Println(errPlatinum)
    }

    return nil
}

デプロイ

  • Mackerelにサービスを登録する
export MKRKEY=XXX

curl -X POST https://api.mackerelio.com/api/v0/services \
    -H "X-Api-Key: ${MKRKEY}" \
    -H "Content-Type: application/json" \
    -d '{"name": "Metal", "memo": "metal"}'
make build
  • ServerlessFrameworkでデプロイする
sls deploy --aws-profile <PROFILE> --mkrkey ${MKRKEY}

ソースコード等はこちらにおいています。 ( Makefileや ServelessFrameworkのyamlファイルなども置いています )

github.com

結果 ( Mackerelの画面 )

f:id:yhidetoshi:20190919183157p:plain

さいごに

今回はGoで貴金属の価格が掲載されているサイトをgoqueryを利用してスクレイピングし、MackerelにメトリクスをPostすることで可視化しました。 次はこの貴金属の価格をjsonで返すAPIをGoで作りたいと思います。

GoogleAnalyticsのPV数をGoとMackerelで可視化する

はじめに

今回はGoとMackerelを使ってGoogleAnalyticsのデータ(PV数)をMackerelにカスタムメトリクスとして投稿・可視化について書いていきます。

アーキテクチャ

f:id:yhidetoshi:20190919101124p:plain

  1. CloudWatch EventでLambdaを定期実行
  2. Lmabda ( Go ) でGoogleAnalyticsにAPIでデータを取得
  3. Lambda ( Go ) でMackerelにメトリクスを投げる

Google Cloud Platformについて

GoogleAnalyticsのAPIを有効にして、認証キーを作成します。

f:id:yhidetoshi:20190919084346p:plain

f:id:yhidetoshi:20190919084517p:plain

f:id:yhidetoshi:20190919084559p:plain

f:id:yhidetoshi:20190919084610p:plain

f:id:yhidetoshi:20190919084625p:plain

f:id:yhidetoshi:20190919084639p:plain

f:id:yhidetoshi:20190919084705p:plain

この鍵ファイルを使ってAPIを実行します。

f:id:yhidetoshi:20190919084701p:plain

GoogleAnalyticsについて

今回はGoogleAnalyticsの初期設定は完了している状態で進めます。

GCPで作成したサービスアカウントをGoogleAnalyticsに登録します。

f:id:yhidetoshi:20190919092459p:plain

f:id:yhidetoshi:20190919092519p:plain

f:id:yhidetoshi:20190919092517p:plain

f:id:yhidetoshi:20190919092514p:plain

ビューIDを確認する。

f:id:yhidetoshi:20190919092731p:plain

Lambda ( Go )について

LambdaにはGoogleAnalyticsにAPIでデータを取得してMackerelにメトリクスをPostする処理を定義して CloudWatch Eventを連携させます。Lambda関数やCloudwatchEventなどはServerlessFrameworkを利用してデプロイしました。

GoでGoogleAnalyticsのデータ取得にはこちらのパッケージを利用しました。

godoc.org

そして、Mackerelに取得したデータをメトリクスとして投稿にするところはこちらを利用!

github.com

*) Go: 1.12.4

  • main.go
package main

import (
    "context"
    "fmt"
    "os"
    "strconv"
    "time"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/mackerelio/mackerel-client-go"
    "golang.org/x/oauth2/google"
    "google.golang.org/api/analytics/v3"
    "google.golang.org/api/option"
)

var (
    mkrKey = os.Getenv("MKRKEY")
    client = mackerel.NewClient(mkrKey)

    json   = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
    viewID = os.Getenv("VIEW_ID")
)

const (
    // GA
    startDate = "today"
    endDate   = "today"

    metricsUsers     = "users"
    metricsPVs       = "pagePath!=/preview"
    metricsPageViews = "pageviews"

    // Mackerel
    serviceName = "GoogleAnalytics"

    // Lambda
    timezone = "Asia/Tokyo"
    offset   = 9 * 60 * 60
)

func main() {
    lambda.Start(Handler)
}

// Handler Lambda
func Handler() {
    jst := time.FixedZone(timezone, offset)
    nowTime := time.Now().In(jst)

    ctx := context.Background()
    jwtConfig, err := google.JWTConfigFromJSON([]byte(json), analytics.AnalyticsReadonlyScope)
    if err != nil {
        fmt.Println(err)
    }

    ts := jwtConfig.TokenSource(ctx)
    client, err := analytics.NewService(ctx, option.WithTokenSource(ts))

    // Get Users
    resUsers, err := client.Data.Ga.Get("ga:"+viewID, startDate, endDate, "ga:"+metricsUsers).Do()
    if err != nil {
        fmt.Println(err)
    }

    // Get PVs
    resPVs, err := client.Data.Ga.Get(
        "ga:"+viewID, startDate, endDate, "ga:"+metricsPageViews).Filters("ga:" + metricsPVs).Do()
    if err != nil {
        fmt.Println(err)
    }

    intResultUsers, _ := strconv.Atoi(resUsers.TotalsForAllResults["ga:"+metricsUsers])
    intResultPVs, _ := strconv.Atoi(resPVs.TotalsForAllResults["ga:"+metricsPageViews])

    mkrErr := PostValuesToMackerel(intResultUsers, intResultPVs, nowTime)
    if err != nil {
        fmt.Println(mkrErr)
    }
}

// PostValuesToMackerel Post Metrics to Mackerel
func PostValuesToMackerel(intResultUsers int, intResultPVs int, nowTime time.Time) error {
    // Post users
    errUser := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{
        &mackerel.MetricValue{
            Name:  "Users.users",
            Time:  nowTime.Unix(),
            Value: intResultUsers,
        },
    })
    if errUser != nil {
        fmt.Println(errUser)
    }

    // Post PV
    errPV := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{
        &mackerel.MetricValue{
            Name:  "PV.PVs",
            Time:  nowTime.Unix(),
            Value: intResultPVs,
        },
    })
    if errPV != nil {
        fmt.Println(errPV)
    }

    return nil
}

認証ファイル(json)のセット方法

序盤でGoogleAnalyticsの認証に使うjsonファイルを生成しました。環境変数でFilePathを指定して実行してもいいみたいです。( 下記リンクに記載があります )

今回は認証ファイル(json)を環境変数に中身をセットして読み込む方法にしました。 ( Lambdaの環境変数にkey/valueでセットしたかったので )

以下、コードの一部抜粋です。

  • main.go
jwtConfig, err := google.JWTConfigFromJSON([]byte(json), analytics.AnalyticsReadonlyScope)
    if err != nil {
        fmt.Println(err)
    }

    ts := jwtConfig.TokenSource(ctx)
    client, err := analytics.NewService(ctx, option.WithTokenSource(ts))
func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
    var f credentialsFile
    if err := json.Unmarshal(jsonKey, &f); err != nil {
        return nil, err
    }
    if f.Type != serviceAccountKey {
        return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
    }
    scope = append([]string(nil), scope...) // copy
    return f.jwtConfig(scope), nil
}
// credentialsFile is the unmarshalled representation of a credentials file.
type credentialsFile struct {
    Type string `json:"type"` // serviceAccountKey or userCredentialsKey

    // Service Account fields
    ClientEmail  string `json:"client_email"`
    PrivateKeyID string `json:"private_key_id"`
    PrivateKey   string `json:"private_key"`
    TokenURL     string `json:"token_uri"`
    ProjectID    string `json:"project_id"`

    // User Credential fields
    // (These typically come from gcloud auth.)
    ClientSecret string `json:"client_secret"`
    ClientID     string `json:"client_id"`
    RefreshToken string `json:"refresh_token"`
}

→ 認証ファイル(json)をパースして利用しています。

認証ファイルを環境変数にセットできるように加工

そのままだとjsonの中身をbashで exportで環境変数にセットできないので以下のように修正しました。

  • 変更パターン

    • " --> \"
    • \n --> "'\n'"
  • 変更前 ( 一部抜粋 )

  "type": "service_account",
  "private_key": "-----BEGIN PRIVATE KEY-----\n"
  • 変更後 ( 一部抜粋 )
  \"type\": \"service_account\",
  \"private_key\": \"-----BEGIN PRIVATE KEY-----"'\n'"\"

GoogleAnalyticsから欲しい情報を取得する

今回取得するのは、今日のPV数です。クエリパラメータは以下のようにセットしました。 取得したデータに合わせて、クエリパラメータのページを参考に書けばいいかと。

( metricsPVs = "pagePath!=/preview"はてブロで執筆中に確認するプレビューはカウントしないようにしています )

  • main.go ( 一部抜粋 )
const (
    // GA
    startDate = "today"
    endDate   = "today"

    metricsUsers     = "users"
    metricsPVs       = "pagePath!=/preview"
    metricsPageViews = "pageviews"

    dimensionsTitle = "pageTitle"
    dimensionsPath  = "pagePath"

    // Mackerel
    serviceName = "GoogleAnalytics"

    // Lambda
    timezone = "Asia/Tokyo"
    offset   = 9 * 60 * 60
)

        // Get PVs
    resPVs, err := client.Data.Ga.Get(
        "ga:"+viewID, startDate, endDate, "ga:"+metricsPageViews).Filters("ga:" + metricsPVs).Do()

以下のページにクエリパラメータについて記載されています。

developers.google.com

Lambdaの定期実行

CloudWatch Eventを利用して定期実行させます。今回は15分ごとに実行するようにしました。 f:id:yhidetoshi:20190919150430p:plain

Mackerelにメトリクスを投稿する

GoでMackerelにメトリクスを投稿する方法は以前のブログに書いているのでよければ参照ください!

yhidetoshi.hatenablog.com

今回書いたMackerelにメトリクスをPostするコードは以下の通りです。

  • main.go ( 一部抜粋 )
// PostValuesToMackerel Post Metrics to Mackerel
func PostValuesToMackerel(intResultUsers int, intResultPVs int, nowTime time.Time) error {
    // Post users
    errUser := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{
        &mackerel.MetricValue{
            Name:  "Users.users",
            Time:  nowTime.Unix(),
            Value: intResultUsers,
        },
    })
    if errUser != nil {
        fmt.Println(errUser)
    }

    // Post PV
    errPV := client.PostServiceMetricValues(serviceName, []*mackerel.MetricValue{
        &mackerel.MetricValue{
            Name:  "PV.PVs",
            Time:  nowTime.Unix(),
            Value: intResultPVs,
        },
    })
    if errPV != nil {
        fmt.Println(errPV)
    }

    return nil
}
  • Mackerelの画面 ( 実行結果 )

f:id:yhidetoshi:20190919143643p:plain

デプロイ

export MKRKEY=XXX
export GOOGLE_APPLICATION_CREDENTIALS_JSON="{...}"
  • Mackerelにサービスを作成する
    • 名前は GoogleAnalytics で作成する
curl -X POST https://api.mackerelio.com/api/v0/services \
    -H "X-Api-Key: ${MKRKEY}" \
    -H "Content-Type: application/json" \
    -d '{"name": "GoogleAnalytics", "memo": "google analytics"}'
make build
  • ServerlessFrameworkでデプロイする
sls deploy --aws-profile <PROFILE> --viewid <VIEW-ID> --mkrkey ${MKRKEY} --google-apikey ${GOOGLE_APPLICATION_CREDENTIALS_JSON}

さいごに

Goで利用してGoogleAnalyticsのデータをMackerelのメトリクスとして投稿・可視化をしました。 今回はじめて Google Cloud Servicesに接続するためのライブラリを利用したので、これから活用していくいいきっかけになりました。 他にも個人的に可視化したいメトリクスがあり、最近ありがたいことにアンバサダプランになったとこもあるので、どんどん活用していこうと思います!

Mackerelアンバサダーを拝命しました!

この度、嬉しいことにMackerelアンバサダーを拝命しました!!

f:id:yhidetoshi:20190912080654p:plain

Mackerelアンバサダーとは?

mackerel.io

Mackerelとの出会いとツール等作成のきっかけ

 Mackerelとの出会いは、前職の先輩社員に "よさそうな監視サービスあるけど話聞いてみる??" というのがきっかけで、 はてな CREのa-knowさんに会社に来ていただいてMackerelを丁寧にアツくご紹介していただいたのがはじまりでした(ともて印象に残っています)!

 当時、私はGoの勉強をはじめていて、MackerelはGo製で管理画面は直感的に使いやすかったことや、エージェントインストールの容易さ、 mkrコマンドで監視設定を変更できるなど、すごく興味をもったことを覚えています。 幸運なことに、前職のシステム監視に導入することができました。 当時のMackerel利用は標準的に監視して、設定をCLIで変更したりする程度でした。

 ありがたいことに現職でもMackerelを利用しています。そのなかで、継続的にGoの勉強を進めているとGoとMackerelをテーマに何か作りたいという気持ちに。 運用をより楽にしたり、メトリクスをMackerelで可視化することができればGoの勉強になるし運用改善になると思いはじめてツールやプラグインを作成したりするようになりました。

つくったもの

OSSへのコミット

Mackerelツールを開発する中で mackerel-client-go を活用する機会が多くなり、欲しい機能が未実装だったので このOSSにPull Requestを出してコミットすることができました。

github.com

yhidetoshi.hatenablog.com

yhidetoshi.hatenablog.com

最後に

Mackerelを通じてGoを勉強しつつMackerelのプラグインやツール等を作成、最近は mackerel-client-go にコミットする事ができました。 そして、嬉しいことに先日にMackerelのアンバサダーを拝命することができました!!(ノベルティをいただけるとの事なのでとても楽しみしてます!!) 個人的には楽しくやっていた事がこのような結果となって返ってきたことがとても嬉しく良い経験になりました!(多謝)

GuardDutyの結果をLambda ( Go ) でSlackに通知する

はじめに

AWSのセキュリティ対策のひとつにGuardDutyというサービスがあります。

aws.amazon.com

  • GuardDuty はインテリジェントな脅威検出サービス。
  • AWS アカウントとワークロードを継続的にモニターし、保護
  • 実際には以下のイベントを分析
    • AWS CloudTrail (アカウント内の AWS ユーザーAPI アクティビティ)
    • VPC フローログ (ネットワークトラフィックデータ)
    • DNS ログ (名前のクエリパターン)
  • 脅威検出は、以下のアクティビティを特定
    • アカウントの侵害
    • インスタンスの侵害、
    • 悪意のある偵察に関連する可能性
  • 以下を検知
    • 異常なAPI コール
    • 既知の悪質な IP アドレスへの不審なアウトバウンド通信
    • DNSクエリを転送メカニズムとして使用の可能性のあるデータの窃盗

現時点では30日間の無料トライアルがあるので、一度有効にしてみて、1ヶ月でどれくらいコストがかかるのかを確認することができます。

今回はこの脅威検出サービスで検知した結果を少しカスタマイズしてSlackに通知させます。

Slack通知を設定

アーキテクチャ

f:id:yhidetoshi:20190829074848p:plain

通知結果

今回は、GuardDutyにサンプル生成した結果をSlack通知させました。

docs.aws.amazon.com

  • 高 ( GetFindingsの結果 severity パラメータの値は 7.0〜8.9 の範囲 )
  • 中 ( GetFindingsの結果 severity パラメータの値は 4.0〜6.9 の範囲 )
  • 低 ( GetFindingsの結果 severity パラメータの値は 0.1〜3.9 )
  • ※ ) 値 0 と 9.0 から 10.0 が将来使用するために現在予約されています。

このseverity の値 ( 高・中・低 ) によってSlack通知の色分けをしました。

api.slack.com

今回、以下のように脅威度によって配色しました。

  • color

    • 高: danger
    • 中: warning
    • 低: #0000ff
  • slack通知結果

f:id:yhidetoshi:20190905080248p:plain

CloudWatchの設定

  1. イベントバターン
  2. サービス名: GuardDuty
  3. イベントタイプ: GuardDuty Finding
  4. ターゲット: Lambda関数を選択して関数名を指定

serverlessFrameworkでデプロイしていますが、コンソール画面での設定ものせておきます。

f:id:yhidetoshi:20190831141418p:plain

Lambda関数

serverlessFrameworkでデプロイします。ソースコードはのちほど記載します。 通知のjsonをパースしてSlackに通知するGoのコードは以下の通りです。

cloudwatchに送信されたGuardDutyのjsonをパースするために調べて必要なところだけ加工したjsonが以下です。

  • json全体をログ出力。
    err := json.Unmarshal([]byte(event.Detail), gd)
    fmt.Println(string([]byte(event.Detail)))
    if err != nil {
        fmt.Println(err)
    }
  • CloudWatchLogsでログを確認 (必要なところだけ抽出)
{
    "schemaVersion": "2.0",
    "accountId": "XXX",
    "region": "ap-northeast-1",
    "type": "UnauthorizedAccess:EC2/TorRelay",
    "resource": {
        "resourceType": "Instance",
        "instanceDetails": {
            "instanceId": "i-99999999",
            "instanceType": "m3.xlarge"
        }
    },
    "title": "EC2 instance i-99999999 is communicating with Tor Exit node.",
    "description": "EC2 instance i-99999999 is communicating with IP address 198.51.100.0 on the Tor Anonymizing Proxy network."
}
{
    "schemaVersion": "2.0",
    "accountId": "XXX",
    "region": "ap-northeast-1",
    "type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration",
    "resource": {
        "resourceType": "AccessKey",
        "accessKeyDetails": {
            "accessKeyId": "GeneratedFindingAccessKeyId",
            "principalId": "GeneratedFindingPrincipalId",
            "userType": "IAMUser",
            "userName": "GeneratedFindingUserName"
        }
    },
    "severity": 8,
    "title": "Credentials for instance role GeneratedFindingUserName used from external IP address.",
    "description": "Credentials created exclusively for an EC2 instance using instance role GeneratedFindingUserName have been used from external IP address 198.51.100.0."
}

これをもとにgoのコードを作成。

  • main.go
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "strconv"
    "strings"

    "github.com/ashwanthkumar/slack-go-webhook"
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/iam"
)

const (
    version = "0.0.1"
    region  = "ap-northeast-1"
)

var (
    // USERNAME username of slack
    USERNAME = "GuardDutyAlert"

    // SLACKURL webhookurl of slack
    SLACKURL = os.Getenv("SLACKURL")

    notPostThreshold = os.Getenv("THRESHOLD")

    // add excluded account name: []string{"dev"}
    excludeAccountList = []string{"dev"}
    config             = aws.Config{Region: aws.String(region)}
    svcIAM             = iam.New(session.New(&config))
)

// GuardDutyFindings set guardduty GuardDutyFindingsValue
type GuardDutyFindings struct {
    AccountID   string      `json:"accountId"`
    Region      string      `json:"region"`
    Type        string      `json:"type"`
    Severity    json.Number `json:"severity"`
    Title       string      `json:"title"`
    Description string      `json:"description"`
    Resource    Resource    `json:"resource"`
}

// Resource set guardduty ResourceValue
type Resource struct {
    ResourceType     string           `json:"resourceType,omitempty"`
    UserName         string           `json:"userName,omitempty"`
    InstanceDetails  InstanceDetails  `json:"instanceDetails,omitempty"`
    AccessKeyDetails AccessKeyDetails `json:"accessKeyDetails,omitempty"`
}

// InstanceDetails set guardduty value
type InstanceDetails struct {
    InstanceID   string `json:"instanceId,omitempty"`
    InstanceType string `json:"instanceType,"`
}

// AccessKeyDetails set guardduty AccessKeyDetailsValue
type AccessKeyDetails struct {
    UserName string `json:"userName,omitempty"`
}

func main() {
    lambda.Start(Handler)
}

// Handler get value from cloudwatch event
func Handler(event events.CloudWatchEvent) (events.CloudWatchEvent, error) {
    var resource string
    postFlag := true
    gd := &GuardDutyFindings{}

    err := json.Unmarshal([]byte(event.Detail), gd)
    if err != nil {
        fmt.Println(err)
    }

    // cast to float64
    float64Severity, err := gd.Severity.Float64()
    slackColor := CheckSeverityLevel(float64Severity)
    float64NotPostThreshold, err := strconv.ParseFloat(notPostThreshold, 64)

    // get aws account name
    accountAliasName := FetchAccountAlias()

    // Check excluded List
    for i := range excludeAccountList {
        if strings.Contains(accountAliasName, excludeAccountList[i]) {
            postFlag = false
        }
    }

    // Set affected resource
    if gd.Resource.InstanceDetails.InstanceID != "" {
        resource = gd.Resource.InstanceDetails.InstanceID
    } else if gd.Resource.AccessKeyDetails.UserName != "" {
        resource = gd.Resource.AccessKeyDetails.UserName
    } else {
        resource = "unknown"
    }

    // Post slack
    if postFlag == false && float64Severity < float64NotPostThreshold {
        fmt.Println("Do not post slack")
    } else {
        PostSlack(slackColor, gd.Title, accountAliasName, string(gd.Severity), resource, gd.Type, gd.Description)
    }
    return event, err
}

// CheckSeverityLevel fix the color
func CheckSeverityLevel(severity float64) string {
    var color string

    if severity == 0.0 {
        color = "good"
    } else if (0.1 <= severity) && (severity <= 3.9) {
        color = "#0000ff"
    } else if (4.0 <= severity) && (severity <= 6.9) {
        color = "warning"
    } else {
        color = "danger"
    }
    return color
}

// FetchAccountAlias get account alias name
func FetchAccountAlias() string {
    var accountAlias string

    params := &iam.ListAccountAliasesInput{}
    res, err := svcIAM.ListAccountAliases(params)
    if err != nil {
        fmt.Println(err)
    }
    if res.AccountAliases == nil {
        accountAlias = "None"
    } else {
        accountAlias = *res.AccountAliases[0]
    }
    return accountAlias
}

// PostSlack post slack result
func PostSlack(slackColor string, title string, accountAliasName string, severity string, resource string, reason string, description string) {
    field0 := slack.Field{Title: "Title", Value: "_" + title + "_"}
    field1 := slack.Field{Title: "Account", Value: accountAliasName}
    field2 := slack.Field{Title: "Severity", Value: severity}
    field3 := slack.Field{Title: "Affected Resource", Value: resource}
    field4 := slack.Field{Title: "Type", Value: reason}
    field5 := slack.Field{Title: "Description", Value: "```" + description + "```"}

    attachment := slack.Attachment{}
    attachment.AddField(field0).AddField(field1).AddField(field2).AddField(field3).AddField(field4).AddField(field5)
    color := slackColor
    attachment.Color = &color
    payload := slack.Payload{
        Username:    USERNAME,
        Attachments: []slack.Attachment{attachment},
    }
    err := slack.Send(SLACKURL, "", payload)
    if err != nil {
        os.Exit(1)
    }
}

ServerlessFrameworkを利用したデプロイ

Lambda (Go) + IAMロール + CloudwatchEventをseverlessFrameworkを利用してデプロイしました。

■ デプロイコマンド

- Goコンパイル
  - $ make build

- ServerlessFrameworkでデプロイ
  - $ sls deploy --aws-profile <PROFILE> --slackurl <SLACKURL>
export GO111MODULE=on


## Install dependencies
.PHONY: deps
deps:
  go get -v -d


## Setup development
.PHONY: deps
devel-deps: deps
  GO111MODULE=off
  go get -u golang.org/x/lint/golint
  go get -u github.com/motemen/gobump/cmd/gobump
  go get -u github.com/Songmu/make2help/cmd/make2help


## Setup build
.PHONY: pre-build
build-deps:
  go get -u github.com/mitchellh/gox


## Build binaries
.PHONY: build
build: build-deps
  rm -rf ./main
  gox -os=linux -arch=amd64 -output=./main -ldflags "-s -w"
  gobump show


## Lint
.PHONY: lint
lint: devel-deps
  go vet ./...
  golint -set_exit_status ./...


## Show help
.PHONY: help
help:
   @make2help $(MAKEFILE_LIST)
  • $ make help
build:             Build binaries
build-deps:        Setup build
deps:              Install dependencies
devel-deps:        Setup development
help:              Show help
lint:              Lint
  • serverless.yml
service: guardduby
frameworkVersion: ">=1.48.0"

provider:
  name: aws
  stage: dev
  runtime: go1.x
  region: ap-northeast-1


functions:
  slack:
    handler: main
    role: GuardDutyLambda
    timeout: 30
    description: GuardDutyLambda slack notice
    memorySize: 128
    environment:
      SLACKURL: ${opt:slackurl}
      THRESHOLD: 2.0
    events:
      - cloudwatchEvent:
          event:
            source:
              - 'aws.guardduty'
            detail-type:
              - 'GuardDuty Finding'


resources:
  Resources:
    GuardDutyLambda:
      Type: AWS::IAM::Role
      Properties:
        RoleName: GuardDutyLambda
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/IAMReadOnlyAccess
        Policies:
          - PolicyName: GuardDutyLambda
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - "logs:CreateLogGroup"
                    - "logs:CreateLogStream"
                    - "logs:PutLogEvents"
                  Resource: "*"

まとめ

AWS GuardDutyのアラート通知を CloudwatchEvent + Lambda (Go)を使ってSlackに通知する方法を記載しました。 デプロイについては、ServerlessFrameworkを利用しています。

GuardDutyの通知は、最近サービスリリースされた ChatBotが対応しているので利用してみてもいいかもしれません。 jp.techcrunch.com

AWS アベイラビリティゾーン名とAZ-IDのマッピングを確認する

AWSの大障害がありアベイラビリティーゾーン名とAZ-IDのマッピングを確認した事があったので備忘録的に書きます。

aws-cli

docs.aws.amazon.com

aws-cliを利用します。バージョンが1.15系だと ZoneId が取得できなかったので、アップデートしました。

$ aws --version

aws-cli/1.16.106

以下、実行コマンドと結果です。 <PROFILE_NAME> の部分は各自の実行環境に合わせてください。

■ 整形せずに出力する

$ aws ec2 describe-availability-zones --region ap-northeast-1 --profile <PROFILE_NAME>

{
    "AvailabilityZones": [
        {
            "State": "available",
            "Messages": [],
            "RegionName": "ap-northeast-1",
            "ZoneName": "ap-northeast-1a",
            "ZoneId": "apne1-az4"
        },
        {
            "State": "available",
            "Messages": [],
            "RegionName": "ap-northeast-1",
            "ZoneName": "ap-northeast-1c",
            "ZoneId": "apne1-az1"
        },
        {
            "State": "available",
            "Messages": [],
            "RegionName": "ap-northeast-1",
            "ZoneName": "ap-northeast-1d",
            "ZoneId": "apne1-az2"
        }
    ]
}

jqで整形する

  • コマンド
aws ec2 describe-availability-zones \
  --region ap-northeast-1 \
  --profile <PROFILE_NAME> \
  | jq -c '.AvailabilityZones[] | {RegionName: .RegionName, ZoneName: .ZoneName, ZoneId: .ZoneId}'
  • 出力
{"RegionName":"ap-northeast-1","ZoneName":"ap-northeast-1a","ZoneId":"apne1-az4"}
{"RegionName":"ap-northeast-1","ZoneName":"ap-northeast-1c","ZoneId":"apne1-az1"}
{"RegionName":"ap-northeast-1","ZoneName":"ap-northeast-1d","ZoneId":"apne1-az2"}

■ テーブル表示にする

  • コマンド
aws ec2 describe-availability-zones \
  --region ap-northeast-1 \
  --query 'AvailabilityZones[].{RegionName:RegionName, ZoneName:ZoneName, ZoneId:ZoneId}' \
  --output table \
  --profile <PROFILE_NAME>
  • 出力
----------------------------------------------------
|             DescribeAvailabilityZones            |
+----------------+-------------+-------------------+
|   RegionName   |   ZoneId    |     ZoneName      |
+----------------+-------------+-------------------+
|  ap-northeast-1|  apne1-az4  |  ap-northeast-1a  |
|  ap-northeast-1|  apne1-az1  |  ap-northeast-1c  |
|  ap-northeast-1|  apne1-az2  |  ap-northeast-1d  |
+----------------+-------------+-------------------+

参考

AWS CLIで自アカウントのAZ名とAZ IDのマッピングを確認する | DevelopersIO

Goの文法学習 ( 自分用Memoで適宜更新 )

先人たちのブログや技術書を活用して自分用の備忘録です。 参照した記事は参照に一覧で表示させていただきます。

( 適宜更新 )

Map

blog.golang.org

mapを作成

  • make
    • make(map[<キーの型>]<値の型>) で mapを生成
  • mapから値を取得
package main

import (
    "fmt"
)

func main() {
    m := make(map[string]int)
    m["test1"] = 100
    m["test2"] = 90

    fmt.Println(m["test1"])
    fmt.Println(m)
}

/* 結果
100
map[test1:100 test2:90]
*/

mapリテラルをつくる

※)リテラル

数値や文字列を直接に記述した定数のこと

package main

import (
    "fmt"
)

func main() {
    m := map[string]int{
        "test1": 100,
        "test2": 95,
        "test3": 50,
    }
    fmt.Println(m)
}

/* 結果
map[test1:100 test2:95 test3:50]
*/

値チェック

valueが存在するかを確認する。結果は true, false が返る

package main

import (
    "fmt"
)

func main() {
    m := map[string]int{
        "test1": 100,
        "test2": 95,
        "test3": 50,
    }

    val, chk := m["test1"]
    fmt.Println(val, chk)

    val, chk = m["test100"]
    fmt.Println(val, chk)
}

/* 結果
100 true
0 false
*/

key/valueを削除

package main

import (
    "fmt"
)

func main() {
    m := map[string]int{
        "test1": 100,
        "test2": 95,
        "test3": 50,
    }

    fmt.Println(m)
    delete(m, "test1")
    fmt.Println(m)
}

/* 結果
map[test1:100 test2:95 test3:50]
map[test2:95 test3:50]
*/

key/valueの全取得と要素数の取得

package main

import (
    "fmt"
)

func main() {
    m := map[string]int{
        "test1": 100,
        "test2": 95,
        "test3": 50,
    }

    for key, val := range m {
        fmt.Println(key, val)
    }
    fmt.Println(len(m))
}

/* 結果
test1 100
test2 95
test3 50
3
*/

error

package main

import (
    "fmt"
)

func main() {
    if err := doError();err != nil {
        fmt.Println(err)
        //return
    }
    fmt.Println("end of code")
}
func doError() error {
    message := "error occurred!!!"
    return fmt.Errorf(message)

}

/* 結果 ( return無し )
error occurred!!!
end of code
*/

/* 結果 ( return有り )
error occurred!!!
*/

Slice (スライス)

  • 特徴
    • 可変長配列
    • 配列全体のポインタ(ptr) / 配列の長さ(len) / 配列の容量(cap)からなるデータ構造

make()関数

  • make(型, 要素数, 容量) で定義する。
    • スライスが初期化され、デフォルトでは0が格納される
    • 容量は省略可能。省略すると 要素数容量 は同じになる
    • 組み込み関数
package main

import (
    "fmt"
)

func main() {
    test1 := make([]int, 5, 10)
    fmt.Println(test1)
    fmt.Println(len(test1))
    fmt.Println(cap(test1))
 
    fmt.Println()
 
    test2 := make([]int, 10)
    fmt.Println(test2)
    fmt.Println(len(test2))
    fmt.Println(cap(test2))    
}

/* 結果
[0 0 0 0 0]
5
10

[0 0 0 0 0 0 0 0 0 0]
10
10
*/

append()関数

  • append(slice, val1, val2, val3, ...) と使う
    • slice の末尾に val1, val2 ... と追加されていく
    • sliceの要素数は自動的に増える
    • 容量に達すると、容量の2倍を確保してくれる
    • 配列のメモリアドレスは容量が確保されると変わる

copy関数

  • copy(コピー先, コピー元) として使う
package main

import (
    "fmt"
)

func main() {
    test1 := []int{1, 2, 3, 4, 5}
    test2 := make([]int, 5)
 
    copy(test2, test1) 
    fmt.Println(test2)
}

/* 結果
[1 2 3 4 5]
*/

構造体/JSON

構造体の初期化

package main

import (
    "fmt"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    var p1 Person
    p1.Name = "yhidetoshi"
    p1.Age = 20
    fmt.Println("p1: "+p1.Name, p1.Age)

    p2 := Person{"yhidetoshi", 21}
    fmt.Println("p2: "+p2.Name, p2.Age)

    p3 := Person{Name: "yhidetoshi", Age: 22}
    fmt.Println("p3: "+p3.Name, p3.Age)

    p4 := &Person{Name: "yhidetoshi", Age: 23}
    fmt.Println("p4: "+p4.Name, p4.Age)

    p5 := new(Person)
    p5.Name = "yhidetoshi"
    p5.Age = 24
    fmt.Println("p5: "+p5.Name, p5.Age)

    p6 := &Person{}
    p6.Name = "yhidetoshi"
    p6.Age = 25
    fmt.Println("p6: "+p6.Name, p6.Age)

}

/* 結果
p1: yhidetoshi 20
p2: yhidetoshi 21
p3: yhidetoshi 22
p4: yhidetoshi 23
p5: yhidetoshi 24
p6: yhidetoshi 25
 */

構造体配列

フェレットを飼っている人には見やすいかもしれません。 フェレットの情報を保持した構造体を配列にして、3匹のフェレット情報を格納した例です。

package main

import "fmt"

type Ferret struct {
    Age   int
    Color string
    Farm  string
}

type Ferrets []Ferret

func main() {

    maru := Ferret{
        Age:   4,
        Color: "blackself",
        Farm:  "furFarm",
    }

    hana := Ferret{
        Age:   6,
        Color: "sable",
        Farm:  "ruby",
    }

    natu := Ferret{
        Age:   3,
        Color: "butterscotch",
        Farm:  "mountainDew",
    }

    var ferrets Ferrets
    ferrets = append(ferrets, maru)
    ferrets = append(ferrets, hana)
    ferrets = append(ferrets, natu)

    fmt.Println(ferrets)
}

/*
[{4 blackself furFarm} {6 sable ruby} {3 butterscotch mountainDew}]
*/

ネスト構造体

package main

import (
    "fmt"
)

type Ferret struct {
    Name string
    Food Food
}

type Food struct {
    Name   string
    Volume int
}

func main() {

    ferret := Ferret{
        Name: "maru",
        Food: Food{
            Name:   "zupurimeGrainFreeDiet",
            Volume: 5,
        },
    }

    fmt.Println(ferret.Name)
    fmt.Println(ferret.Food.Name)
    fmt.Println(ferret.Food.Volume)

}

/* 実行結果
maru
zupurimeGrainFreeDiet
5
*/

ネスト構造の実用例

  • 別ブログでこのネスト構造体を利用しました。

yhidetoshi.hatenablog.com

GuardDutyのjson形式が以下で、 Resourceがネストし、さらに InstanceDetails がネストしています。

{
    "schemaVersion": "2.0",
    "accountId": "XXX",
    "region": "ap-northeast-1",
    "type": "UnauthorizedAccess:EC2/TorRelay",
    "resource": {
        "resourceType": "Instance",
        "instanceDetails": {
            "instanceId": "i-99999999",
            "instanceType": "m3.xlarge"
        }
    },
    "title": "EC2 instance i-99999999 is communicating with Tor Exit node.",
    "description": "EC2 instance i-99999999 is communicating with IP address 198.51.100.0 on the Tor Anonymizing Proxy network."
}

なので、構造体の宣言は以下のようにしました。

  • go( 一部抜粋 )
// GuardDutyFindings set guardduty GuardDutyFindingsValue
type GuardDutyFindings struct {
    AccountID   string      `json:"accountId"`
    Region      string      `json:"region"`
    Type        string      `json:"type"`
    Severity    json.Number `json:"severity"`
    Title       string      `json:"title"`
    Description string      `json:"description"`
    Resource    Resource    `json:"resource"`
}

// Resource set guardduty ResourceValue
type Resource struct {
    ResourceType     string           `json:"resourceType,omitempty"`
    UserName         string           `json:"userName,omitempty"`
    InstanceDetails  InstanceDetails  `json:"instanceDetails,,omitempty"`
    AccessKeyDetails AccessKeyDetails `json:"accessKeyDetails,,omitempty"`
}

// InstanceDetails set guardduty value
type InstanceDetails struct {
    InstanceID   string `json:"instanceId,omitempty"`
    InstanceType string `json:"instanceType,"`
}

Marshalを使う

  • JSONを出力する
    • 構造体をJSON文字列に変換で Marshal を利用
    • インデントを付きで出力するために MarshalIndentを利用
    b, _ := json.Marshal(&ferret)
    fmt.Println(string(b))

    fmt.Println()

    bmi, _ := json.MarshalIndent(&ferret, "", "   ")
    fmt.Println(string(bmi))
  • 実行結果
{"Name":"maru","Food":{"Name":"zupurimeGrainFreeDiet","Volume":5}}

{
    "Name": "maru",
    "Food": {
        "Name": "zupurimeGrainFreeDiet",
        "Volume": 5
    }
}

Unmarshalを使う

  • JSONから構造体にパースする
  • JSON(in bytes)をGo Objectに変換する

  • ferret.json

[
  {
    "Name": "maru",
    "Food": {
      "Name": "zupurimeGrainFreeDiet",
      "Volume": 5
    }
  },
  {
    "Name": "hana",
    "Food": {
      "Name": "ferretSelectionSenior",
      "Volume": 3
    }
  },
  {
    "Name": "natu",
    "Food": {
      "Name": "totalyComplete",
      "Volume": 7
    }
  }
]
  • main.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
)

type Ferret struct {
    Name string
    Food Food
}

type Food struct {
    Name   string
    Volume int
}

func main() {
    var ferrets []Ferret

    bytes, err := ioutil.ReadFile("ferret.json")
    if err != nil {
        fmt.Println(err)
    }

    errJson := json.Unmarshal(bytes, &ferrets)
    if errJson != nil {
        fmt.Println(errJson)
    }

    for _, value := range ferrets {
        fmt.Printf("%s\t%s\t%d\n", value.Name, value.Food.Name, value.Food.Volume)
    }
}

/* 実行結果
maru   zupurimeGrainFreeDiet   5
hana   ferretSelectionSenior   3
natu   totalyComplete  7
*/

NewDecoderを使う

  • ストリームから情報を読み込んだ時はjson.NewDecoderを使う
    • http.Getで返ったresp.Bodyなどの io.Reader型に対して使う
req, err := http.NewRequest("GET", "https://api.nature.global/1/devices", nil)
    if err != nil {
        fmt.Println("Error")
    }
    req.Header.Set("Authorization", "Bearer "+token)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error request")
    }

    err = json.NewDecoder(resp.Body).Decode(&devices)
    if err != nil {
        fmt.Println("Error decode")
    }

詳細はこちら。 yhidetoshi.hatenablog.com

JSON-to-Go ( Convert JSON to Go struct )

JSON --> 構造体 してくれるサービスがる。

JSON-to-Go: Convert JSON to Go instantly

ポインタ

参考・引用について

このポインタのところは、以下の2つのブログで大変わかりやすく解説されています。自分が理解するために必要なところを引用・参照して記載しています。(多謝)

例え話をしないC言語のポインタの説明 | 右や左の旦那様

Goのポインタ - はじめてのGo言語

ポインタとは

  • 説明

    • int型変数のポインタの場合は *int と記述する
    • 変数のアドレス取得するときは & をつけ、アドレスから変数の中身へアクセスする時は * を使う
    • 変数 とは、メモリのアドレスに付けられた名前 のこと
    • 変数c があるのではなくて、 0x7fffffffe494 という場所 アドレス変数c という名前を付けている
    • コンパイラ変数cという場所 が必要なようだから、そのぶんメモリを用意しておこう。 0x7fffffffe494変数c と名付けよう」と決めている
    • コンパイラがこういったことを考えてくれるおかげで、われわれプログラマーは「変数c」というものの正体を深く考えずにプログラミングできる
    • ポインタは メモリ上の他のアドレスを指す変数
  • Pointerを使う理由

    • サイズの大きな変数を効率よく扱う
      • 関数の引数として値を渡す際に、「引数用のアドレス」に変数をコピーします。int型やchar型のような小さい変数であれば問題ないですが、大きなサイズの構造体を引数に取るような関数の場合、メモリコピーにCPUを使ってしまって性能が落ちてしまう。どれだけ大きな構造体でも、先頭のアドレスだけ渡してしまえば効率よくやり取りできる
void warizan(int warareru, int waru, int* syou, int* amari)
{
  *syou = warareru / waru;
  *amari = warareru % waru;
}

void main ()
{
  int a;
  int b;
  int c;
  int d;
  a = 17;
  b = 5;
  warizan(a, b, &c, &d);
  // c = 3, d = 2になる
}
  • 値渡し
    • ある変数を関数の引数として渡す場合、値のコピーが渡されます。そのため呼び出された関数内で変数の値を変更しても、元の値には影響がありません。これを「値渡し」と呼びます。
  • ポインタ渡し(参照渡し)
    • ポインタ変数を関数に渡した場合は、ポインタが指し示す値が、呼び出し元も、呼び出された方も同じものであるため、呼び出された関数内で元の値を変更することができます。これを「ポインタ渡し(または参照渡し)」と呼びます。

値渡しとポインタ渡しの簡単な例

package main

import "fmt"

func call(a int, b *int){
  a = a + 1
  *b = *b + 1
}

func main(){
  a,b := 5,5

  call(a, &b)
  fmt.Println("値渡し", a) // ---> 値渡し 5
  fmt.Println("ポインタ渡し", b) // ---> ポインタ渡し 6
}
  • ポインタ配列
    • Go 言語の配列名は配列の先頭アドレスを表していません。また、配列にアドレス演算子を適用すると、配列へのポインタが生成されますが、それは配列の先頭アドレスを表しているわけではありません。Go 言語の配列はひとつの「値」なので、配列へのポインタは配列そのものを指し示すことになります。

メソッドの ポインタレシーバ変数レシーバ

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}
  • ポイントレシーバ
    • 呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取得できる
var v Vertex
ScaleFunc(v)  // Compile error!
ScaleFunc(&v) // OK

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK
  • 変数レシーバ
    • メソッドが変数レシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができる
var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
  • ポインタレシーバを使う理由
    • メソッドがレシーバが指す先の変数を変更するため
    • メソッドの呼び出し毎に変数のコピーを避けるため。 例えば、レシーバが大きな構造体である場合に効率的

reflect

  • アサーション

    • interface{}型から具体的な型に落とし込む必要がある
    • アサーションする型が実体と合わない場合はpanicが発生する
  • reflectパッケージを利用する

    • アサーションする型が事前にわからない場合
      • mapは key/valueの両方の型を合わせるために難しい場合
package main

import (
    "fmt"
    "reflect"
)

func p(v ...interface{}) {
    fmt.Println(v...)
}

func main() {

    // 型チェック
    p(reflect.TypeOf(0)) // int

    //型比較
    p(reflect.TypeOf(0).Kind() == reflect.Int)     // true
    p(reflect.TypeOf(0) == reflect.TypeOf("hoge")) // false
    p(reflect.TypeOf(0) == reflect.TypeOf(1))      // true

    //reflectで値をセット

    /* Elem
   https://golang.org/pkg/reflect/#Value.Elem

   Elem returns the value that the interface v contains or that the pointer v points to.
   It panics if v's Kind is not Interface or Ptr. It returns the zero Value if v is nil.
   */

    var n int
    v := reflect.ValueOf(&n).Elem()

    v.SetInt(99)
    p(n) // 99

    v.Set(reflect.ValueOf(999))
    p(n) // 999

    /* Slice */

    // 型チェック
    p(reflect.TypeOf([]int{})) // []int

    // 型比較
    p(reflect.TypeOf([]int{}).Kind() == reflect.Slice)        // true
    p(reflect.TypeOf([]int{}) == reflect.TypeOf([]int{1, 2})) //true
}

テスト

書き方

  • 書き方のルール
    • テストコードは同一のディレクトリに配置する
    • テストコードのファイル名は _test.go とする --> calc_test.go
    • Testから始まる名前にする --> func TestSum(t *testing.T)
    • 引数は *testing.Tを渡す --> func TestSum(t *testing.T)

シンプルなコードで確認する。

.
├── calc
│   ├── calc.go
│   └── calc_test.go
└── main.go
  • main.go
package main

func main() {}
  • calc.go
package calc

func Sum(a, b int) int {
    return a + b
}
  • calc_test.go
package calc

import "testing"

func TestSum(t *testing.T) {
    if Sum(1 ,2) != 3{
        t.Fatal("Sum vaule should be 3, but not match" )
    }
}

実行方法

  • 実行のルール

    • go testを実行するとカレントディレクトリ以下になる *_test.goコンパイルされてテストが実行される
    • go test -v の(-v)オプションをつけるとテストケースの詳細を表示してくれる
    • _. からはじまるファイルは無視される
    • go test -run TestXXX のように -run正規表現によりテストケースを指定することができる
    • テスト結果はキャッシュされる
    • テスト結果のキャッシュを削除する --> go clean -testcache
  • $ go test -v ./calc

=== RUN   TestSum
--- PASS: TestSum (0.00s)
PASS
ok      _/Users/hidetoshi/TestGoStudy/calc  (cached)
  • $ go test -run TestSum ./calc
ok   _/Users/hidetoshi/TestGoStudy/calc  (cached)
  • $ go clean -testcache (キャッシュをクリア)

  • $ go test -run TestSum ./calc

ok   _/Users/hidetoshi/TestGoStudy/calc  0.008s

Tips

Goで開発するときに利用すべきツール

  • gofmt
  • goimports
    • パッケージを読み込むimport文の挿入ど削除を自動でおこなってくれる
  • govet
    • バグの原因になりそうなコードを検出してくれる
  • golint
    • Goらいしくない書き方を検出してくれる
    • -set_exit_status
      • 警告が発生したらエラーで終了させる場合に使う
    • -min_confidence
      • 厳しさを変更できる
        • デフォルト値は0.8
        • -min_confidence=0.5 のように指定する

Go Module

  • Go1.13から正式導入予定
  • 環境変数 export GO111MODULE=on
  • go mod init
    • go.modファイルが生成される
      • 依存関係の定義と管理のためのファイル
    • go: cannot determine module path for source directory... ( outside GOPATH, no import comments )
      • gitのリポジトリ設定がされていなければエラーになる ? と思う。
    • go getを実行するとgo.modが自動更新される。go.sumファイルが生成される
      • go.sumファイル
        • バージョンロックのためのファイル

表示形式

参考