20200915のGoに関する記事は7件です。

GoでQiitaのトレンドを取得するAPI①叩いてみた

はじめに

前回、GoでQiitaのトレンドを取得するAPI①作ってみた という記事を書きました。

今回はGoでそのAPIを叩いてみたいと思います。

リポジトリはこちら

前回(API)のリポジトリはこちら

コード

まずは構造体を定義します。
返ってくるjsonと同じ構造になるように定義します。

type Qiita struct {
    Trend `json:"trend"`
    Scope string `json:"scope"`
}

type Trend struct {
    Edges `json:"edges"`
}

type Edges []struct {
    FollowingLikers []string `json:"followingLikers"`
    IsLikeByViewer  bool     `json:"isLikeByViewer"`
    IsNewArrival    bool     `json:"isNewArrival"`
    HasCodeBlock    bool     `json:"hasCodeBlock"`
    Node            `json:"node"`
}

type Node struct {
    CreatedAt  time.Time `json:"createdAt"`
    LikesCount int       `json:"likesCount"`
    Title      string    `json:"title"`
    Uuid       string    `json:"uuid"`
    Author     `json:"author"`
}

type Author struct {
    ProfileImageURL string `json:"profileImageUrl"`
    UrlName         string `json:"urlName"`
}

次は、実際にGETリクエストを送ります。
返ってきたjsonをデコードして表示させます。

var trend Qiita

func main() {
    resp, err := http.Get("http://localhost:8080/trend")
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "http request err: ", err)
    }
    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "read err: ", err)
    }

    if err := json.Unmarshal(bytes, &trend); err != nil {
        fmt.Fprintln(os.Stderr, "unmarshal err:", err)
    }

    fmt.Println(trend)
}

実行結果はこんな感じです。

{{[{[] false false true {2020-09-13 08:48:24 +0000 UTC 229 Dockerを体系的に学べる公式チュートリアル和訳 5778e0d9e9c04038903c {https://qiita-image-store.s3.amazonaws.com/0/227791/profile-images/1550453410 Michinosuke}}} {[] false false true {2020-09-13 11:10:37 +0000 UTC 191 kubernetesでもぷよぷよがしたいので同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った c8ff9c824b65ca371cd9 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38448/profile-images/1600092601 yoitomakenouta}}} {[] false false true {2020-09-12 10:57:35 +0000 UTC 136 スプラトゥーン2のプレイ動画から、やられたシーンだけをディープラーニングで自動抽出する acbc0906046bb7b2b1db {https://avatars3.githubusercontent.com/u/16898831?v=4 tfandkusu}}} {[] false false true {2020-09-12 13:59:38 +0000 UTC 111 Kubernetesの負荷試験で絶対に担保したい13のチェックリスト 56a5442c1fc4d714c941 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45875/profile-images/1595854294 taisho6339}}} {[] false false true {2020-09-12 03:08:08 +0000 UTC 148 間違えて違うブランチで作業を始めてしまったときの対処法【Git】 dc21de47c617b0d0eebf {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675596/profile-images/1598685340 paleo_engineer}}} {[] false false false {2020-09-12 00:49:28 +0000 UTC 64 【翻訳】wordpressのプラグインで毎月5万収益を得ている話 ab1b2b6d5676dc55144b {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705937/profile-images/1599573315 kirishima_app}}} {[] false false true {2020-09-13 05:42:09 +0000 UTC 48 論文と公共データベースを使って無料で始めるAI創薬 a12f7bf594a56a2b22ba {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/329816/ba6cbc8b71c92411f763dc9334212db0a2d0f08f/x_large.png?1589641830 kimisyo}}} {[] false false true {2020-09-13 04:57:19 +0000 UTC 37 FlutterにFirebase(Firestore)を導入したら、iOSのビルドが遅くなった 290d61a1731e50201a61 {https://avatars1.githubusercontent.com/u/38914807?v=4 sakaihiroaki}}} {[] false false true {2020-09-13 18:56:58 +0000 UTC 39 モナリザの写真を自分の顔とシンクロさせて動かしてみる、流行りの深層学習モデルFOMMについて解説&実装 e0ce1701e25e315713c4 {https://qiita-image-store.s3.amazonaws.com/0/56557/profile-images/1473694040 tommy19970714}}} {[] false true false {2020-09-14 07:59:56 +0000 UTC 25 VRChat向けマッチングサービス「sucrALrose(スクラロース)」でサイバー攻撃。何が起こった? 6136be8b48080554fead {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/535270/3e59522dfca791fd0ce7f98d863a9a796ca95a16/large.png?1574159780 kkent030315}}} {[] false false false {2020-09-13 01:57:50 +0000 UTC 37 音声分析におけるフーリエ変換とスペクトログラムを理解する 6af2cc4c4be0c57bef06 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651198/profile-images/1591512068 shirowanisan}}} {[] false false true {2020-09-12 23:07:03 +0000 UTC 25 C++上でPythonのコードを実行する(Boost.Pythonの利用) 1e2c24e80212a4c4c584 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/profile-images/1556565825 m4saka}}} {[] false false true {2020-09-13 10:34:26 +0000 UTC 23 React Live Conference 2020 に参加した話 f1c2e9c6fde046aed05c {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/profile-images/1591193480 nitaking}}} {[] false false true {2020-09-12 13:32:05 +0000 UTC 29 SwiftUIとCombineの練習がてらmacOS向けのピアノアプリ作ってみた 0230a14ec6c3a8174568 {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/67153/e88c7b2790238820700627c60482ade9228609f7/x_large.png?1575435001 Kyome}}} {[] false false false {2020-09-13 09:55:28 +0000 UTC 18 micro:bitプログラミング入門講座テキスト df4b502b43fde7eba35b {https://qiita-image-store.s3.amazonaws.com/0/26536/profile-images/1473684616 noanoa07}}} {[] false false false {2020-09-13 17:21:00 +0000 UTC 13 ThreeDPoseTracker Ver.0.3.0アップデート 82b1e50de8d81e554721 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374813/profile-images/1598234502 yukihiko_a}}} {[] false false true {2020-09-12 11:29:02 +0000 UTC 26 production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集 1fd6077327f99245b807 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348791/profile-images/1565355993 eihigh}}} {[] false true false {2020-09-13 23:56:21 +0000 UTC 18 【翻訳】自動投稿ツールが300万で売れた話(契約内容も) 58d77a7b91e013f22429 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705937/profile-images/1599573315 kirishima_app}}} {[] false false true {2020-09-11 23:36:29 +0000 UTC 25 Docker × Laravel 8 Jetstream でログイン、ユーザー登録、2要素認証を実装する 7824d1293fef4698c212 {https://qiita-image-store.s3.amazonaws.com/0/52879/profile-images/1540380605 ucan-lab}}} {[] false false true {2020-09-12 10:57:47 +0000 UTC 44 データ構造 Fenwick tree (binary indexed tree, BIT) にどっぷりと入門し、その美しき構造に心を洗われたい方のための紹介記事です! 7d50ff180a4e5c294cb7 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352395/profile-images/1557632578 ngtkana}}} {[] false false true {2020-09-12 09:04:26 +0000 UTC 30 Ractor超入門 f9d1917fda770bcdbe2a {https://qiita-image-store.s3.amazonaws.com/0/191521/profile-images/1531325180 S_H_}}} {[] false false true {2020-09-13 15:03:42 +0000 UTC 12 Docker環境でRails停止時にExit code 1が発生する b18eb09d016a6bf33568 {https://qiita-image-store.s3.amazonaws.com/0/27454/profile-images/1473685014 naomichi-y}}} {[] false false true {2020-09-12 08:48:31 +0000 UTC 17 Prism の Prism Full App (.NET Core) テンプレートを見てみよう cfbf5c9eaea6c5aed4e1 {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/70288/edf16d4d4fdd1abcb3e61b06730b0609cdc77adb/x_large.png?1585128349 okazuki}}} {[] false true true {2020-09-14 12:41:15 +0000 UTC 13 TypeScriptの型で遊ぶ時、再帰制限を(合法的に)突破する 44c1b012d66aae1dc2c0 {https://qiita-image-store.s3.amazonaws.com/0/71477/profile-images/1487741973 kazatsuyu}}} {[] false false true {2020-09-12 01:56:00 +0000 UTC 18 PythonでDICOM画像を扱う f145675055796c69e228 {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/680528/4c5642d036920f85daf6640335027d9a0387cecc/x_large.png?1595678226 asparagasu}}} {[] false false true {2020-09-12 08:11:54 +0000 UTC 27 自分のQiita記事をインクリメンタル検索するAlfred Workflowを作ってみた 6909b534a59d3550093e {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/profile-images/1596162182 ryo2132}}} {[] false true true {2020-09-14 00:14:21 +0000 UTC 4 ISUCON10 予選でやったこと、失敗したこと 40f77453f8c91d49014e {https://avatars1.githubusercontent.com/u/5134941?v=4 lostfind}}} {[] false true true {2020-09-14 08:34:10 +0000 UTC 4 WebCodecs + WebRTC Insertable Streams で Scalable live w/ P2P を実現してみた 177401804c69bd6cfd64 {https://qiita-image-store.s3.amazonaws.com/0/83466/profile-images/1473702785 komasshu}}} {[] false true true {2020-09-13 20:37:40 +0000 UTC 6 Azure Cosmos DB の無償枠 (Free Tier) の注意点 ffc3a44eef75bda5a502 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/profile-images/1580139929 ymasaoka}}} {[] false true true {2020-09-14 06:51:39 +0000 UTC 7 Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition) e48a011074bc65d57d1a {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55937/profile-images/1592202863 sh1ch}}}]} daily}

見にくい!!

欲しい値だけを取得してみます。
今回は、記事タイトルと記事URLを表示させたいと思います。
URLはユーザー名/ + items/ + Uuid で表示されるので、AuthorのUrlNameとNodeのUuidを取得してみます。

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
)

type Qiita struct {
    Trend `json:"trend"`
}

type Trend struct {
    Edges `json:"edges"`
}

type Edges []struct {
    Node `json:"node"`
}

type Node struct {
    Title  string `json:"title"`
    Uuid   string `json:"uuid"`
    Author `json:"author"`
}

type Author struct {
    UrlName string `json:"urlName"`
}

var trend Qiita

func main() {
    resp, err := http.Get("http://localhost:8080/trend")
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "http request err: ", err)
    }
    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "read err: ", err)
    }

    if err := json.Unmarshal(bytes, &trend); err != nil {
        fmt.Fprintln(os.Stderr, "unmarshal err:", err)
    }

    for i, v := range trend.Trend.Edges {
        fmt.Println("No.", i+1)
        fmt.Printf("\t%s\n\t%s\n", v.Node.Title, "https://qiita.com/"+v.Node.Author.UrlName+"/items/"+v.Node.Uuid)
    }
}

取得しない値については構造体を定義しなくても大丈夫です。

実行するとこんな感じ。

No. 1
    Dockerを体系的に学べる公式チュートリアル和訳
    https://qiita.com/Michinosuke/items/5778e0d9e9c04038903c
No. 2
    kubernetesでもぷよぷよがしたいので同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った
    https://qiita.com/yoitomakenouta/items/c8ff9c824b65ca371cd9
No. 3
    スプラトゥーン2のプレイ動画から、やられたシーンだけをディープラーニングで自動抽出する
    https://qiita.com/tfandkusu/items/acbc0906046bb7b2b1db
No. 4
    モナリザの写真を自分の顔とシンクロさせて動かしてみる、流行りの深層学習モデルFOMMについて解説&実装
    https://qiita.com/tommy19970714/items/e0ce1701e25e315713c4
No. 5
    Kubernetesの負荷試験で絶対に担保したい13のチェックリスト
    https://qiita.com/taisho6339/items/56a5442c1fc4d714c941
No. 6
    【翻訳】自動投稿ツールが300万で売れた話(契約内容も)
    https://qiita.com/kirishima_app/items/58d77a7b91e013f22429
No. 7
    論文と公共データベースを使って無料で始めるAI創薬
    https://qiita.com/kimisyo/items/a12f7bf594a56a2b22ba
No. 8
    VRChat向けマッチングサービス「sucrALrose(スクラロース)」でサイバー攻撃。何が起こった?
    https://qiita.com/kkent030315/items/6136be8b48080554fead
No. 9
    TypeScriptの型で遊ぶ時、再帰制限を(合法的に)突破する
    https://qiita.com/kazatsuyu/items/44c1b012d66aae1dc2c0
No. 10
    FlutterにFirebase(Firestore)を導入したら、iOSのビルドが遅くなった
    https://qiita.com/sakaihiroaki/items/290d61a1731e50201a61
No. 11
    React Live Conference 2020 に参加した話
    https://qiita.com/nitaking/items/f1c2e9c6fde046aed05c
No. 12
    SwiftUIとCombineの練習がてらmacOS向けのピアノアプリ作ってみた
    https://qiita.com/Kyome/items/0230a14ec6c3a8174568
No. 13
    Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)
    https://qiita.com/sh1ch/items/e48a011074bc65d57d1a
No. 14
    【R】データ要約ガチ勢のためのgtsummaryで表を書こう
    https://qiita.com/yanami/items/117851de49024f5980d0
No. 15
    音声分析におけるフーリエ変換とスペクトログラムを理解する
    https://qiita.com/shirowanisan/items/6af2cc4c4be0c57bef06
No. 16
    C++上でPythonのコードを実行する(Boost.Pythonの利用)
    https://qiita.com/m4saka/items/1e2c24e80212a4c4c584
No. 17
    ThreeDPoseTracker Ver.0.3.0アップデート
    https://qiita.com/yukihiko_a/items/82b1e50de8d81e554721
No. 18
    JAWS SONIC 2020 & MIDNIGHT JAWS 2020 登壇資料まとめ
    https://qiita.com/tosiooooooo/items/6b2b9ba3fd7e7ad68604
No. 19
    「重複エラー」を伝える英語フレーズ
    https://qiita.com/suin/items/b51f1434723f4979e8c8
No. 20
    Docker環境でRails停止時にExit code 1が発生する
    https://qiita.com/naomichi-y/items/b18eb09d016a6bf33568
No. 21
    Azure Cosmos DB の無償枠 (Free Tier) の注意点
    https://qiita.com/ymasaoka/items/ffc3a44eef75bda5a502
No. 22
    画面をデザインするということ
    https://qiita.com/megumu-u/items/73b728ad1d381717d731
No. 23
    Linuxカーネル、その29年の歴史レポート
    https://qiita.com/rana_kualu/items/27c883922061c33af0a4
No. 24
    VSCodeをIDE化する方法
    https://qiita.com/Midoliy/items/d19dc8c1c97078b1e640
No. 25
    production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集
    https://qiita.com/eihigh/items/1fd6077327f99245b807
No. 26
    WebCodecs + WebRTC Insertable Streams で Scalable live w/ P2P を実現してみた
    https://qiita.com/komasshu/items/177401804c69bd6cfd64
No. 27
    データ構造 Fenwick tree (binary indexed tree, BIT) にどっぷりと入門し、その美しき構造に心を洗われたい方のための紹介記事です!
    https://qiita.com/ngtkana/items/7d50ff180a4e5c294cb7
No. 28
    Ractor超入門
    https://qiita.com/S_H_/items/f9d1917fda770bcdbe2a
No. 29
    ISUCON10 予選でやったこと、失敗したこと
    https://qiita.com/lostfind/items/40f77453f8c91d49014e
No. 30
    pythonで極力簡単にGUIを作る【tkinter編】
    https://qiita.com/Ringa_hyj/items/a580f2d60eadcf4dbb2f

ちゃんと欲しい値だけ取得できました!

さいごに

qiitaのトレンド記事を取得したい方は是非使ってみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoでQiitaのトレンドを取得するAPI②叩いてみた

はじめに

前回、GoでQiitaのトレンドを取得するAPI①作ってみた という記事を書きました。

今回はGoでそのAPIを叩いてみたいと思います。

リポジトリはこちら

前回(API)のリポジトリはこちら

コード

まずは構造体を定義します。
返ってくるjsonと同じ構造になるように定義します。

type Qiita struct {
    Trend `json:"trend"`
    Scope string `json:"scope"`
}

type Trend struct {
    Edges `json:"edges"`
}

type Edges []struct {
    FollowingLikers []string `json:"followingLikers"`
    IsLikeByViewer  bool     `json:"isLikeByViewer"`
    IsNewArrival    bool     `json:"isNewArrival"`
    HasCodeBlock    bool     `json:"hasCodeBlock"`
    Node            `json:"node"`
}

type Node struct {
    CreatedAt  time.Time `json:"createdAt"`
    LikesCount int       `json:"likesCount"`
    Title      string    `json:"title"`
    Uuid       string    `json:"uuid"`
    Author     `json:"author"`
}

type Author struct {
    ProfileImageURL string `json:"profileImageUrl"`
    UrlName         string `json:"urlName"`
}

次は、実際にGETリクエストを送ります。
返ってきたjsonをデコードして表示させます。

var trend Qiita

func main() {
    resp, err := http.Get("http://localhost:8080/trend")
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "http request err: ", err)
    }
    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "read err: ", err)
    }

    if err := json.Unmarshal(bytes, &trend); err != nil {
        fmt.Fprintln(os.Stderr, "unmarshal err:", err)
    }

    fmt.Println(trend)
}

実行結果はこんな感じです。

{{[{[] false false true {2020-09-13 08:48:24 +0000 UTC 229 Dockerを体系的に学べる公式チュートリアル和訳 5778e0d9e9c04038903c {https://qiita-image-store.s3.amazonaws.com/0/227791/profile-images/1550453410 Michinosuke}}} {[] false false true {2020-09-13 11:10:37 +0000 UTC 191 kubernetesでもぷよぷよがしたいので同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った c8ff9c824b65ca371cd9 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38448/profile-images/1600092601 yoitomakenouta}}} {[] false false true {2020-09-12 10:57:35 +0000 UTC 136 スプラトゥーン2のプレイ動画から、やられたシーンだけをディープラーニングで自動抽出する acbc0906046bb7b2b1db {https://avatars3.githubusercontent.com/u/16898831?v=4 tfandkusu}}} {[] false false true {2020-09-12 13:59:38 +0000 UTC 111 Kubernetesの負荷試験で絶対に担保したい13のチェックリスト 56a5442c1fc4d714c941 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45875/profile-images/1595854294 taisho6339}}} {[] false false true {2020-09-12 03:08:08 +0000 UTC 148 間違えて違うブランチで作業を始めてしまったときの対処法【Git】 dc21de47c617b0d0eebf {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675596/profile-images/1598685340 paleo_engineer}}} {[] false false false {2020-09-12 00:49:28 +0000 UTC 64 【翻訳】wordpressのプラグインで毎月5万収益を得ている話 ab1b2b6d5676dc55144b {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705937/profile-images/1599573315 kirishima_app}}} {[] false false true {2020-09-13 05:42:09 +0000 UTC 48 論文と公共データベースを使って無料で始めるAI創薬 a12f7bf594a56a2b22ba {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/329816/ba6cbc8b71c92411f763dc9334212db0a2d0f08f/x_large.png?1589641830 kimisyo}}} {[] false false true {2020-09-13 04:57:19 +0000 UTC 37 FlutterにFirebase(Firestore)を導入したら、iOSのビルドが遅くなった 290d61a1731e50201a61 {https://avatars1.githubusercontent.com/u/38914807?v=4 sakaihiroaki}}} {[] false false true {2020-09-13 18:56:58 +0000 UTC 39 モナリザの写真を自分の顔とシンクロさせて動かしてみる、流行りの深層学習モデルFOMMについて解説&実装 e0ce1701e25e315713c4 {https://qiita-image-store.s3.amazonaws.com/0/56557/profile-images/1473694040 tommy19970714}}} {[] false true false {2020-09-14 07:59:56 +0000 UTC 25 VRChat向けマッチングサービス「sucrALrose(スクラロース)」でサイバー攻撃。何が起こった? 6136be8b48080554fead {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/535270/3e59522dfca791fd0ce7f98d863a9a796ca95a16/large.png?1574159780 kkent030315}}} {[] false false false {2020-09-13 01:57:50 +0000 UTC 37 音声分析におけるフーリエ変換とスペクトログラムを理解する 6af2cc4c4be0c57bef06 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651198/profile-images/1591512068 shirowanisan}}} {[] false false true {2020-09-12 23:07:03 +0000 UTC 25 C++上でPythonのコードを実行する(Boost.Pythonの利用) 1e2c24e80212a4c4c584 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/profile-images/1556565825 m4saka}}} {[] false false true {2020-09-13 10:34:26 +0000 UTC 23 React Live Conference 2020 に参加した話 f1c2e9c6fde046aed05c {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/profile-images/1591193480 nitaking}}} {[] false false true {2020-09-12 13:32:05 +0000 UTC 29 SwiftUIとCombineの練習がてらmacOS向けのピアノアプリ作ってみた 0230a14ec6c3a8174568 {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/67153/e88c7b2790238820700627c60482ade9228609f7/x_large.png?1575435001 Kyome}}} {[] false false false {2020-09-13 09:55:28 +0000 UTC 18 micro:bitプログラミング入門講座テキスト df4b502b43fde7eba35b {https://qiita-image-store.s3.amazonaws.com/0/26536/profile-images/1473684616 noanoa07}}} {[] false false false {2020-09-13 17:21:00 +0000 UTC 13 ThreeDPoseTracker Ver.0.3.0アップデート 82b1e50de8d81e554721 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374813/profile-images/1598234502 yukihiko_a}}} {[] false false true {2020-09-12 11:29:02 +0000 UTC 26 production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集 1fd6077327f99245b807 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348791/profile-images/1565355993 eihigh}}} {[] false true false {2020-09-13 23:56:21 +0000 UTC 18 【翻訳】自動投稿ツールが300万で売れた話(契約内容も) 58d77a7b91e013f22429 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705937/profile-images/1599573315 kirishima_app}}} {[] false false true {2020-09-11 23:36:29 +0000 UTC 25 Docker × Laravel 8 Jetstream でログイン、ユーザー登録、2要素認証を実装する 7824d1293fef4698c212 {https://qiita-image-store.s3.amazonaws.com/0/52879/profile-images/1540380605 ucan-lab}}} {[] false false true {2020-09-12 10:57:47 +0000 UTC 44 データ構造 Fenwick tree (binary indexed tree, BIT) にどっぷりと入門し、その美しき構造に心を洗われたい方のための紹介記事です! 7d50ff180a4e5c294cb7 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352395/profile-images/1557632578 ngtkana}}} {[] false false true {2020-09-12 09:04:26 +0000 UTC 30 Ractor超入門 f9d1917fda770bcdbe2a {https://qiita-image-store.s3.amazonaws.com/0/191521/profile-images/1531325180 S_H_}}} {[] false false true {2020-09-13 15:03:42 +0000 UTC 12 Docker環境でRails停止時にExit code 1が発生する b18eb09d016a6bf33568 {https://qiita-image-store.s3.amazonaws.com/0/27454/profile-images/1473685014 naomichi-y}}} {[] false false true {2020-09-12 08:48:31 +0000 UTC 17 Prism の Prism Full App (.NET Core) テンプレートを見てみよう cfbf5c9eaea6c5aed4e1 {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/70288/edf16d4d4fdd1abcb3e61b06730b0609cdc77adb/x_large.png?1585128349 okazuki}}} {[] false true true {2020-09-14 12:41:15 +0000 UTC 13 TypeScriptの型で遊ぶ時、再帰制限を(合法的に)突破する 44c1b012d66aae1dc2c0 {https://qiita-image-store.s3.amazonaws.com/0/71477/profile-images/1487741973 kazatsuyu}}} {[] false false true {2020-09-12 01:56:00 +0000 UTC 18 PythonでDICOM画像を扱う f145675055796c69e228 {https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/680528/4c5642d036920f85daf6640335027d9a0387cecc/x_large.png?1595678226 asparagasu}}} {[] false false true {2020-09-12 08:11:54 +0000 UTC 27 自分のQiita記事をインクリメンタル検索するAlfred Workflowを作ってみた 6909b534a59d3550093e {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/profile-images/1596162182 ryo2132}}} {[] false true true {2020-09-14 00:14:21 +0000 UTC 4 ISUCON10 予選でやったこと、失敗したこと 40f77453f8c91d49014e {https://avatars1.githubusercontent.com/u/5134941?v=4 lostfind}}} {[] false true true {2020-09-14 08:34:10 +0000 UTC 4 WebCodecs + WebRTC Insertable Streams で Scalable live w/ P2P を実現してみた 177401804c69bd6cfd64 {https://qiita-image-store.s3.amazonaws.com/0/83466/profile-images/1473702785 komasshu}}} {[] false true true {2020-09-13 20:37:40 +0000 UTC 6 Azure Cosmos DB の無償枠 (Free Tier) の注意点 ffc3a44eef75bda5a502 {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/profile-images/1580139929 ymasaoka}}} {[] false true true {2020-09-14 06:51:39 +0000 UTC 7 Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition) e48a011074bc65d57d1a {https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55937/profile-images/1592202863 sh1ch}}}]} daily}

見にくい!!

欲しい値だけを取得してみます。
今回は、記事タイトルと記事URLを表示させたいと思います。
URLはユーザー名/ + items/ + Uuid で表示されるので、AuthorのUrlNameとNodeのUuidを取得してみます。

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
)

type Qiita struct {
    Trend `json:"trend"`
}

type Trend struct {
    Edges `json:"edges"`
}

type Edges []struct {
    Node `json:"node"`
}

type Node struct {
    Title  string `json:"title"`
    Uuid   string `json:"uuid"`
    Author `json:"author"`
}

type Author struct {
    UrlName string `json:"urlName"`
}

var trend Qiita

func main() {
    resp, err := http.Get("http://localhost:8080/trend")
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "http request err: ", err)
    }
    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal()
        fmt.Fprintln(os.Stderr, "read err: ", err)
    }

    if err := json.Unmarshal(bytes, &trend); err != nil {
        fmt.Fprintln(os.Stderr, "unmarshal err:", err)
    }

    for i, v := range trend.Trend.Edges {
        fmt.Println("No.", i+1)
        fmt.Printf("\t%s\n\t%s\n", v.Node.Title, "https://qiita.com/"+v.Node.Author.UrlName+"/items/"+v.Node.Uuid)
    }
}

取得しない値については構造体を定義しなくても大丈夫です。

実行するとこんな感じ。

No. 1
    Dockerを体系的に学べる公式チュートリアル和訳
    https://qiita.com/Michinosuke/items/5778e0d9e9c04038903c
No. 2
    kubernetesでもぷよぷよがしたいので同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った
    https://qiita.com/yoitomakenouta/items/c8ff9c824b65ca371cd9
No. 3
    スプラトゥーン2のプレイ動画から、やられたシーンだけをディープラーニングで自動抽出する
    https://qiita.com/tfandkusu/items/acbc0906046bb7b2b1db
No. 4
    モナリザの写真を自分の顔とシンクロさせて動かしてみる、流行りの深層学習モデルFOMMについて解説&実装
    https://qiita.com/tommy19970714/items/e0ce1701e25e315713c4
No. 5
    Kubernetesの負荷試験で絶対に担保したい13のチェックリスト
    https://qiita.com/taisho6339/items/56a5442c1fc4d714c941
No. 6
    【翻訳】自動投稿ツールが300万で売れた話(契約内容も)
    https://qiita.com/kirishima_app/items/58d77a7b91e013f22429
No. 7
    論文と公共データベースを使って無料で始めるAI創薬
    https://qiita.com/kimisyo/items/a12f7bf594a56a2b22ba
No. 8
    VRChat向けマッチングサービス「sucrALrose(スクラロース)」でサイバー攻撃。何が起こった?
    https://qiita.com/kkent030315/items/6136be8b48080554fead
No. 9
    TypeScriptの型で遊ぶ時、再帰制限を(合法的に)突破する
    https://qiita.com/kazatsuyu/items/44c1b012d66aae1dc2c0
No. 10
    FlutterにFirebase(Firestore)を導入したら、iOSのビルドが遅くなった
    https://qiita.com/sakaihiroaki/items/290d61a1731e50201a61
No. 11
    React Live Conference 2020 に参加した話
    https://qiita.com/nitaking/items/f1c2e9c6fde046aed05c
No. 12
    SwiftUIとCombineの練習がてらmacOS向けのピアノアプリ作ってみた
    https://qiita.com/Kyome/items/0230a14ec6c3a8174568
No. 13
    Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)
    https://qiita.com/sh1ch/items/e48a011074bc65d57d1a
No. 14
    【R】データ要約ガチ勢のためのgtsummaryで表を書こう
    https://qiita.com/yanami/items/117851de49024f5980d0
No. 15
    音声分析におけるフーリエ変換とスペクトログラムを理解する
    https://qiita.com/shirowanisan/items/6af2cc4c4be0c57bef06
No. 16
    C++上でPythonのコードを実行する(Boost.Pythonの利用)
    https://qiita.com/m4saka/items/1e2c24e80212a4c4c584
No. 17
    ThreeDPoseTracker Ver.0.3.0アップデート
    https://qiita.com/yukihiko_a/items/82b1e50de8d81e554721
No. 18
    JAWS SONIC 2020 & MIDNIGHT JAWS 2020 登壇資料まとめ
    https://qiita.com/tosiooooooo/items/6b2b9ba3fd7e7ad68604
No. 19
    「重複エラー」を伝える英語フレーズ
    https://qiita.com/suin/items/b51f1434723f4979e8c8
No. 20
    Docker環境でRails停止時にExit code 1が発生する
    https://qiita.com/naomichi-y/items/b18eb09d016a6bf33568
No. 21
    Azure Cosmos DB の無償枠 (Free Tier) の注意点
    https://qiita.com/ymasaoka/items/ffc3a44eef75bda5a502
No. 22
    画面をデザインするということ
    https://qiita.com/megumu-u/items/73b728ad1d381717d731
No. 23
    Linuxカーネル、その29年の歴史レポート
    https://qiita.com/rana_kualu/items/27c883922061c33af0a4
No. 24
    VSCodeをIDE化する方法
    https://qiita.com/Midoliy/items/d19dc8c1c97078b1e640
No. 25
    production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集
    https://qiita.com/eihigh/items/1fd6077327f99245b807
No. 26
    WebCodecs + WebRTC Insertable Streams で Scalable live w/ P2P を実現してみた
    https://qiita.com/komasshu/items/177401804c69bd6cfd64
No. 27
    データ構造 Fenwick tree (binary indexed tree, BIT) にどっぷりと入門し、その美しき構造に心を洗われたい方のための紹介記事です!
    https://qiita.com/ngtkana/items/7d50ff180a4e5c294cb7
No. 28
    Ractor超入門
    https://qiita.com/S_H_/items/f9d1917fda770bcdbe2a
No. 29
    ISUCON10 予選でやったこと、失敗したこと
    https://qiita.com/lostfind/items/40f77453f8c91d49014e
No. 30
    pythonで極力簡単にGUIを作る【tkinter編】
    https://qiita.com/Ringa_hyj/items/a580f2d60eadcf4dbb2f

ちゃんと欲しい値だけ取得できました!

さいごに

qiitaのトレンド記事を取得したい方は是非使ってみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語でTODOアプリwithクリーンアーキテクチャ

この記事ではクリーンアーキテクチャを学びながらGoで簡単なTODOアプリを作成する記事です。勉強のまとめをした物なので間違っている点があればご指摘をお願いします。

クリーンアーキテクチャ

クリーンアーキテクチャとは

ソフトウェアをレイターごとに役割を切り分けることにより関心を分離させることができるという考え方です。
翻訳版はこちら

採用する目的

  1. フレームワーク独立
    フレームワークを道具として使うことでアプリケーションがフレームワークに依存せず、制約を受けない

  2. テスト可能
    レイヤー同士が独立しているのでテストがしやすい。

  3. UI独立
    ビジネスルールを変えずにUIを変更できる。

  4. データベース独立
    postgresやMySqlなどのデータベースの変更が容易である。

  5. 外部機能独立
    ビジネスルールは外部レイヤーに依存しない。

レイヤー

説明するのは4つだが4つでなくても良いです。

  1. Entities

ビジネスルールのためのデータ構造や関数

  1. Use Case

アプリケーション固有のビジネスルールを定義する。エンティティとのデータをやり取りする。

  1. Interfce Adapters

アダプターの集合。内側にある層から外側の層にデータを変換して受け渡す。逆に外側から内側にデータを受け渡す。

  1. Frameworks & Drivers

フレームワークやツールから構成される。

レイヤー間の規則

依存ルール。各レイヤーは内側のレイヤーのことしか知らず、外側のレイヤーについては知らない。

内側の円は、外側の円についてなにも知ることはない。とくに、外側の円で宣言されたものの名前を、内側の円から言及してはならない。これは、関数、クラス、変数、あるいはその他、名前が付けられたソフトウェアのエンティティすべてに言える。
同様に、外側の円で使われているデータフォーマットを内側の円で使うべきではない。とくに、それらのフォーマットが、外側の円でフレームワークによって生成されているのであれば。外側の円のどんなものも、内側の円に影響を与えるべきではないのだ。

実装するアプリケーション

作成したアプリケーションのコードはこちらです。
TODOとユーザーを使った単純なアプリケーションです。WebAPIを提供します。
ユーザーのCRUD, TODOのCRUDを行います。

開発環境

Docker 19.03.12
docker-compose 1.26.2
Go 1.13
PostgreSQL
github.com/lib/pq 1.8.0

ディレクトリ構成

├── domain
│   └── model
│       ├── todo.go
│       └── user.go
├── infrastructure
│   ├── router.go
│   ├── sqlhandler.go
│   └── sqlhandler_test.go
├── interfaces
│   ├── controllers
│   │   ├── todoController.go
│   │   └── userController.go
│   └── database
│       ├── sqlhandler.go
│       ├── todoRepository.go
│       └── userRepository.go
├── main.go
└── usecase
    ├── todoInteractor.go
    ├── todoRepository.go
    ├── userInteractor.go
    └── userRepository.go

とします。アーキテクチャ図とディレクト構成の関係は次のようになっています。

依存の方向 レイヤー ディレクトリ名(パッケージ名)
- Entities domain(model)
Use cases usecase(usecase)
interfaces interfaces(controllers, database)
Frameworks & Drivers infrastracture(infrastructure)

データの流れ

  • DB操作
    Use case(Interactor)interface(Repository)infrastructure(Sqlhandler)

  • リクエスト処理
    アクセス→infrastructure(handler)interfaces(controller)→httpリスポンス

Entitiesレイヤ

User, Todoはビジネスルールのためのデータ構造なのでここで定義します。また、今回は追加していないユーザーの名前の文字数制限などはここで定義する必要があります。UserTodoは次のようになります。

/domain/model/user.go
package model

type User struct {
    ID        int    `json:"id"`
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
}

type Users []User
/domain/model/todo.go
package model

import "time"

type Todo struct {
    ID      int       `json:"id"`
    Title   string    `json:"title"`
    Note    string    `json:"note"`
    DueDate time.Time `json:"due_date"`
    UserID  int       `json:"user_id"`
}

type Todos []Todo

infrastructureレイヤ

このレイヤでは外部のツールであるPostgresとの接続を行います。

/infrastructure/sqlhandler.go
package infrastructure

import (
    "database/sql"
    "fmt"

    "github.com/kikils/golang-todo/interfaces/database"
    _ "github.com/lib/pq" // postres driver
)

type Sqlhandler struct {
    DB *sql.DB
}

func NewSqlhandler() *Sqlhandler {
    connStr := "postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil
    }

    err = db.Ping()
    if err != nil {
        fmt.Println("here")
        return nil
    }

    return &Sqlhandler{db}
}

func (handler *Sqlhandler) Execute(statement string, args ...interface{}) (database.Result, error) {
    res := SqlResult{}
    result, err := handler.DB.Exec(statement, args...)
    if err != nil {
        return res, err
    }
    res.Result = result
    return res, nil
}

func (handler *Sqlhandler) Query(statement string, args ...interface{}) (database.Row, error) {
    rows, err := handler.DB.Query(statement, args...)
    if err != nil {
        return new(SqlRow), err
    }
    row := new(SqlRow)
    row.Rows = rows
    return row, nil
}

type SqlResult struct {
    Result sql.Result
}

func (r SqlResult) LastInsertId() (int64, error) {
    return r.Result.LastInsertId()
}

func (r SqlResult) RowsAffected() (int64, error) {
    return r.Result.RowsAffected()
}

type SqlRow struct {
    Rows *sql.Rows
}

func (r SqlRow) Scan(dest ...interface{}) error {
    return r.Rows.Scan(dest...)
}

func (r SqlRow) Next() bool {
    return r.Rows.Next()
}

func (r SqlRow) Close() error {
    return r.Rows.Close()
}

次に実際にSQlとのデータのやり取りを行う処理をinterfaces/databaseに書きます。

interfaces/databaseレイヤ

このレイヤにはUserRepositoryTodoRepositoryを宣言します。
例としてuserRepository.goを示します。SQLを叩いていることがわかります。

/interfaces/database/userRepository.go
package database

import (
    "github.com/kikils/golang-todo/domain/model"
)

type UserRepository struct {
    Sqlhandler
}

func (repo *UserRepository) Store(u model.User) (id int, err error) {
    row, err := repo.Sqlhandler.Query(
        "INSERT INTO users (FirstName, LastName) VALUES ($1,$2) RETURNING id;", u.FirstName, u.LastName,
    )

    if err != nil {
        return
    }
    for row.Next() {
        if err := row.Scan(&id); err != nil {
            return -1, err
        }
    }
    return
}
... 

UserRepositoryinfrastructureレイヤで定義したSqlhandlerを埋め込んでいます。
しかし、先ほど説明したように内側の層は外側の層の物を使用してはいけないので新しくSqlhandlerinterfaceレイヤに定義します。

/interfaces/database/sqlhandler.go
package database

type Sqlhandler interface {
    Execute(string, ...interface{}) (Result, error)
    Query(string, ...interface{}) (Row, error)
}

type Result interface {
    LastInsertId() (int64, error)
    RowsAffected() (int64, error)
}

type Row interface {
    Scan(...interface{}) error
    Next() bool
    Close() error
}

infrastructureレイヤのSqlhandlerExecute(string, ...interface{}) (Result, error) Query(string, ...interface{}) (Row, error)をもっているのでこれを満たすことができます。
interfaceレイヤにSqlhandler Result Rowを定義することでinterfaceレイヤはinfrastructureレイヤに依存することはなくなるのです。(Goのインターフェースについてはここ)

Usecaseレイヤ

このレイヤではinterfaceレイヤからのInput portの役割、interfaces/controllersへのGatewayの役割をしている。

/usecase/userInteractor.go
package usecase

import "github.com/kikils/golang-todo/domain/model"

type UserInteractor struct {
    UserRepository UserRepository
}

func (interactor *UserInteractor) Add(u model.User) (id int, err error) {
    id, err = interactor.UserRepository.Store(u)
    return
}
...

このようにRepositoryをもつInteractorを定義する。Repositoryinterfaces/databaseレイヤに存在するので、この層でも先ほどと同じようにインターフェースを定義することで依存関係を解決する。そのためUsecaseレイヤでもRepositoryを定義する。

/usecase/userRepository.go
package usecase

import "github.com/kikils/golang-todo/domain/model"

type UserRepository interface {
    Store(model.User) (int, error)
    Update(user model.User) (id int, err error)
    Delete(userID int) (err error)
    FindById(int) (model.User, error)
    FindAll() (model.Users, error)
}

interfaces/controllersレイヤ

ここのレイヤではリクエストに対するルーティングを実装します。

/interfaces/controllers/userController.go
package controllers

import (
    "encoding/json"
    "io/ioutil"
    "net/http"

    "github.com/kikils/golang-todo/domain/model"
    "github.com/kikils/golang-todo/interfaces/database"
    "github.com/kikils/golang-todo/usecase"
)

type UserController struct {
    Interactor usecase.UserInteractor
}

func NewUserController(sqlHandler database.Sqlhandler) *UserController {
    return &UserController{
        Interactor: usecase.UserInteractor{
            UserRepository: &database.UserRepository{
                Sqlhandler: sqlHandler,
            },
        },
    }
}

func (controller *UserController) Create(w http.ResponseWriter, r *http.Request) {
    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
        ResponseError(w, http.StatusInternalServerError, err.Error())
        return
    }

    var user model.User
    if err := json.Unmarshal(b, &user); err != nil {
        ResponseError(w, http.StatusBadRequest, err.Error())
        return
    }
    id, err := controller.Interactor.Add(user)
    if err != nil {
        ResponseError(w, http.StatusBadRequest, err.Error())
        return
    }
    ResponseOk(w, id)
}
...

NewUserControllerdatabase.Sqlhandlerを引数に持つのでInteractorを返してUsecaseレイヤとinfrastructure/databaseを紐づけられます。
次にルーティングを実装します。

infrastructureレイヤ

リクエストは外部から来るので最外殻のinfrastructureレイヤにルーティングを実装します。

/infrastructure/router.go
package infrastructure

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/kikils/golang-todo/interfaces/controllers"
)

func SetUpRouting() *http.ServeMux {
    mux := http.NewServeMux()

    sqlhandler := NewSqlhandler()
    userController := controllers.NewUserController(sqlhandler)
    todoController := controllers.NewTodoController(sqlhandler)

    mux.HandleFunc("/user/create", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodPost:
            userController.Create(w, r)
        default:
            ResponseError(w, http.StatusNotFound, "")
        }
    })

    mux.HandleFunc("/user/get", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodPost:
            userController.Show(w, r)
        default:
            ResponseError(w, http.StatusNotFound, "")
        }
    })
...

SetUpRouting()Sqlhandlerを定義しControllerに渡してあげることで各レイヤの依存関係を保つことができます。

その後、main.go

/main.go
package main

import (
    "log"
    "net/http"

    "github.com/kikils/golang-todo/infrastructure"
)

func main() {
    mux := infrastructure.SetUpRouting()
    log.Fatal(http.ListenAndServe(":8080", mux))
}

としてSetUpRouting()を呼び出して実装は終了です。

最後に

今回はクリーンアーキテクチャを用いてTODOアプリを作成してみました。最初はクリーンアーキテクチャがとっつきにくかったです。実装してみて思ったのは、クリーンアーキテクチャにすることで同じようなコードを複数書く必要があることや、コード補完がきかないので実装のしにくさに気づきました。けれども、DBなどから独立しているので簡単に変更可能だと実感できました。

参考にしたもの

https://qiita.com/ogady/items/34aae1b2af3080e0fec4
https://qiita.com/hirotakan/items/698c1f5773a3cca6193e
https://github.com/eminetto/clean-architecture-go
https://github.com/cohhei/go-to-the-handson/tree/master/04
https://yyh-gl.github.io/tech-blog/blog/go_web_api/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google の protobuf で gRPC Server に REST のエンドポイントをマッピング

Google が公開している googleapis1 のソースを使うと REST と gRPC の両プロトコルをサポートできるのでご紹介。

どうしても、HTTP/1.1 で通信したいという局面で知っておくと便利。わたしの場合、ALBで gRPC Server にフォワーディングするとき、ターゲットグループのヘルスチェック用のエンドポイントとして実装した。

そのときは envoy を使って Proxy 立てたのだけれど、今回は grpc-gateway を使ってくことにする。

とりあえず gRPCサーバーの準備

すぐ動くのが欲しかったので grpc-go の helloworld を拝借。

git clone https://github.com/grpc/grpc-go

# 以降も基本的にこのディレクトリで作業する
cd grpc-go/examples/helloworld/

# gRPCサーバー起動
go run greeter_server/main.go

# クライアントコード実行で動作確認
go run greeter_client/main.go
2020/09/14 23:02:48 Greeting: Hello world

.proto ファイルの中身はこんな感じになっている。

grpc-go/examples/helloworld/helloworld.proto
syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

REST API マッピング

helloworld.proto 修正

annotations.proto を import して rpc の定義に option (google.api.http) {...} を追記。

grpc-go/examples/helloworld/helloworld.proto
syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

// 追記
import "google/api/annotations.proto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
      // 追記
      option (google.api.http) = {
        get: "/say_hello"
      };
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

googleapis のソースを取ってくる

go get -u github.com/googleapis/googleapis

コンパイル時に参照したいだけなので git clone とかでもOK

変更した protobuf を元に自動生成

コンパイラプラグインインストール

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/golang/protobuf/protoc-gen-go

gRPC Server 向け

このとき google/api/annotations.proto を参照できるように protoc の引数に-Iのオプションを追加。 また、既存の helloworld_grpc.pb.go と生成されたクライアントコードが重複するので申し訳ないが今回は消してしまう。

protoc \
  -I. -I$GOPATH/src/github.com/googleapis/googleapis \
  --go_out=plugins=grpc:. --go_opt=paths=source_relative \
  helloworld/helloworld.proto

// 申し訳ないが消す
rm helloworld/helloworld_grpc.pb.go

Proxy Server 向け

grpc-gateway のコンパイラプラグインを使って同様に自動生成。

protoc -I. -I$GOPATH/src/github.com/googleapis/googleapis \
  --grpc-gateway_out=logtostderr=true,paths=source_relative:. \
  helloworld/helloworld.proto

gRPC Server の実装を修正

生成された helloworld.pb.go に合わせてサーバーの実装を修正。

grpc-go/examples/helloworld/greeter_server/main.go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
    port = ":50051"
)

// 追記
type server struct {}

// 修正: ポインタレシーバに修正
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()

    // 修正: server を代入
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Proxy Server 実装

grpc-go/examples/helloworld/greeter_proxy/main.go
package main

import (
    "flag"
    "net/http"

    "github.com/golang/glog"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    gw "google.golang.org/grpc/examples/helloworld/helloworld"
)

var (
    tasklistEndpoint = flag.String("tasklist_endpoint", "localhost:50051", "endpoint of YourService")
)

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    err := gw.RegisterGreeterHandlerFromEndpoint(ctx, mux, *tasklistEndpoint, opts)
    if err != nil {
        return err
    }

    return http.ListenAndServe(":8080", mux)
}

func main() {
    flag.Parse()
    defer glog.Flush()

    if err := run(); err != nil {
        glog.Fatal(err)
    }
}

動作検証

左:サーバーとプロキシ起動、右:リクエスト
スクリーンショット 2020-09-15 15.00.18.png

POSTやリクエストヘッダーの設定もできるみたいなので気になる方はどうぞ。
https://grpc-ecosystem.github.io/grpc-gateway/

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fargateのスケールアウトを高速化する

はじめに

ALB+Fargateという構成で動くWebサービスのスケールアウトが遅く、メディア露出や
DM配信によるスパイクアクセスに耐えられない、という問題があり改善策を検討していた
ところ下記記事を見つけました。

爆速でFargateをスケールさせる「aws-fargate-fast-autoscaler」を試してみた

記事抜粋
簡単に動作原理を説明すると、Fargateをオートスケールさせる場合、通常はECSのターゲット追跡
スケーリングポリシーなどを利用しECSサービスにおけるCloudWatchメトリクスとアラームを利用して
タスク数などを制御します。

ただ、この場合、CloudWatchメトリクスからアラーム発報までにどうしてもタイムラグが有り数秒単位
でのスケーリングが難しく基本は分単位でのスケーリングとなっていました。

そこを、この「aws-fargate-fast-autoscaler」では、Step Functionsを利用して3秒毎に
Fargateタスクへのコネクション数を取得し、その結果に応じて即ecs service updateでタスク数の
上限を引き上げることで、ターゲット追跡スケーリングポリシーでは実現できない高速なスケーリングを
実現しています。

これを導入するか検討した結果、後述する理由により自作ツールを作ることにしました。
(nginxのstub_statusで検知するというアイデアは面白いのでそのまま活用しています!)
この記事はそのツール紹介です。

自作する理由

  • 最大実行時間の制限が1年
    • Step Functions版は、1年を超えて連続稼働できない
  • コスト抑えたい
    • Step Functions版は、月あたり約65USDかかる
    • 自作ツールだと月額11USD程度
  • 複数サービスに対応したい
    • Step Functions版は、対象URLが1つ
    • 弊社ではALB+Fargate構成の複数Webサービスを運用しているため

fast-autoscaler紹介

概要

/nginx-status(stub_status)に定期的にアクセスし、Active Connectionsの値を取得し
閾値を超えていた場合にスケールアウトルールに従いタスク数を変更し、Slack通知します。

対象となるURL/閾値/Slack通知要WebhookURLは、起動時にパラメータストアから取得します。

コードはこちら

処理フロー

  1. コンテナ起動時にパラメータストアから設定情報(json形式,詳細後述)を取得
  2. 指定された/nginx-statusURLにリクエスト送信し、Active Connectionsの値を確認
  3. 閾値を超えていた場合)
    ->スケールアウト実行->/nginx-statusチェック停止->猶予期間経過後チェック再開
  4. 閾値を超えていない場合)
    ->一定周期での/nginx-statusチェックを継続

処理イメージ

fastAutoScaler.png

使い方

セットアップ手順

CloudFormationテンプレートを使用して、パラメータストア、IAMロール、ECSタスク定義、
ECSクラスタ、ECSサービスを作ります。

  • CloudFormationテンプレートをダウンロード

    curl -sLO https://raw.githubusercontent.com/senbazuru/fast-autoscaler/master/cfn-template.yml
    
  • CloudFormationコンソールで上記テンプレートを使用してスタックを作成

    • スタック名(=ECSクラスタ/サービス名)を指定(任意)
      • 以降、fast-autoscalerを指定した前提で記載
    • あとは、コンテナが動くVPC、Subnet、SGを環境に合わせて指定して作成する
      cfn.png

作成した時点ではパラメータストアの設定が空なので、CloudWatchLogsにはエラーが
出ている状態ですが問題ありません。

  • パラメータストアの設定を編集

    • /ecs/fast-autoscaler/config.jsonという名前のパラメータを編集
    • 後述する設定情報の説明を参照して書き換えて保存
  • 設定変更を反映

    • awscliでタスクを停止(その後自動的に起動されることで設定が反映される)
    export AWS_PROFILE={AWSクレデンシャルのプロファイル名}
    TASK_ID=$(aws ecs list-tasks --cluster fast-autoscaler | jq -r '.taskArns[]' | awk -F/ '{print $NF}')
    aws ecs stop-task --cluster fast-autoscaler --task ${TASK_ID}
    

設定情報

  • json形式でパラメータストアに保存
    • タイプはString or SecureStringどちらでもOK
    • 上述CFnで作成した場合はStringになります
設定例
{
  "Services": [
    {
      "StatusUrl":"https://example-a.com/nginx-status",
      "StatusAuthName":"Status-Auth-Key",
      "StatusAuthValue":"hogefuga",
      "ScaleoutThreshold":100,
      "EcsClusterName":"example-app",
      "EcsServiceName":"example-app",
      "SlackWebhookUrl":"https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxx"
    },
    {
      "StatusUrl":"https://example-b-1234567890.ap-northeast-1.elb.amazonaws.com/nginx-status",
      "ScaleoutThreshold":150,
      "MinDesiredCount":2,
      "CheckInterval":10,
      "EcsClusterName":"example-app-b",
      "EcsServiceName":"example-app-b"
    }
  ]
}
フィールド名 説明 必須 デフォルト
StatusUrl nginx-statusのURL https://example-a.com/nginx-status
StatusAuthName 認証用HTTPヘッダ名 Status-Auth-Key
StatusAuthValue 認証用HTTPヘッダ値 hogefuga
ScaleoutThreshold スケールアウト発動するActiveConnectionsの閾値 150 150
MinDesiredCount スケールアウト時に現在のタスク数とみなす最小数 5 5
CheckInterval nginx-statusをチェックする間隔(秒) 3 3
EcsClusterName ECSクラスタ名 example-app
EcsServiceName ECSサービス名 example-app
SlackWebhookUrl Slack通知用WebhookURL https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxx

補足

  • StatusUrlにはnginxに到達できるドメイン+stub_statusパスを指定してください
    • verifyをスキップするので、SSL証明書のコモンネームとの不一致は許容されます
      (つまり、ALBドメイン名を直接指定可能)
  • スケールアウトは現在のタスク数を2倍にします
    • ただし、現在のタスク数が1の場合に2倍にしても2にしかならずスケール不足となる可能性があります
      その場合はMinDesiredCountを指定してください。この値をタスク数の下限値として動作します
      • 例)現在タスク数=3、MinDesiredCount=5の場合、3<5なので現在のタスク数を5とみなし、その2倍の10にDesiredCountを変更します。
  • StatusAuthName/StatusAuthValueで指定した値が/nginx-statusへのリクエストにヘッダとして追加されます
    • fast-autoscaler以外から/nginx-statusへのリクエストを防ぐため
      ALBルーティング設定のヘッダ認証で使用します
  • SlackWebhookUrlに指定があればスケールアウト時にSlackに通知を行います
    • 下記イメージ
      notice.png

効果(例)

リクエスト数が急増し負荷がかかったために応答速度が遅延し始めていますが、その直後に
fast-autoscalerがスケールアウト(タスク数2->10)したことですぐに正常な応答速度に
戻っていることがわかります!
その後は、ターゲット追跡スケーリングポリシーでタスク数が徐々に減っています。

graph.png
上がCPU/Mem使用率(左軸)タスク数(右軸)
下が応答時間 のグラフ

まとめ

これで安心して眠れます!
もしよければお使いください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A Tour of Go メモ 【5】 三日目

A Tour of Go

Methods

関数

引数 func ScaleFunc(v Vertex, i int) func ScaleFunc(v *Vertex, i int)
変数(n) ×
ポインタ(*n) ×
アドレス(&p) ×

メソッド

レシーバ  func (v Vertex))  func (v *Vertex)
変数(n)
アドレス(&p)

struct と type について
Goを学びたての人が誤解しがちなtypeと構造体について

type Vertex struct {
    X, Y float64
}

type Name struct {
    first, last string
}

func(n Name) Greet() string {
    //変数.値でメソッド内で構造体の値を使うことができる
    return "I'm " + n.first + " " + n.last
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    n := Name{"first", "last"}
    fmt.Println(n.Greed())
}

> 5
> I'm first last

#通常の関数としてもかける
type Name struct {
    first, last string
}

func Greet(n Name) string {
    return "I'm " + n.first + " " + n.last
}

func main() {
   n := Name{"first", "last"}
   fmt.Println(Greet(n))
}

> I'm first last

*任意の型にメソッドを宣言できる

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

ポインタレシーバ【1】

構造体も引数にされた関数内での値はコピーされたものなので、関数内の変更は関数外の元の構造体に反映されない。ポインタを引数に指定すれば、関数内の変更も元の構造体に反映される

https://www.yoheim.net/blog.php?q=20170902
ポインタについて学ぶ(基本的なところ)

関数の引数に値を渡した場合には、関数が実行される際に値がコピーされ、参照先のアドレスが変わります。
そのため、以下のように関数内で値を変更したとしても、呼び出し元の変数には値が反映されません。

ポインタレシーバを使う理由

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

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

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.X)
    v.Scale(10)
    fmt.Println(v.X)
    fmt.Println(v.Abs())
}

> 50

*ポインタを使わないと関数内の変更が反映されない

func (v Vertex) Scale(f float64) {
    // そもそもここのvは元の構造体と違う別物、値はコピーされる
    // コピーした値を変更し使用しているため、元の構造体の値は変更されない
    // この関数内でのみ、変更された値を使うことができる
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    //関数外なので、XとYの値は最初に宣言した3,4のままである
    fmt.Println(v.Abs())
}

> 5

ポインタレシーバ【2】

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

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

func main() {
    v := Vertex{3, 4}
     //  &vはvのアドレスであるため、エラーなる
     // Scale(v, 10)ならエラーにならないが、vの値の変更は関数内のみ適用される
    Scale(&v, 10)
    fmt.Println(Abs(v))

    //  例えば、 n := &として引数に*nとするとエラーにならないが、
    //しかし同様にポインタレシーバでないので、Vertexの値は変わらない
   Scale(*n, 10)
    fmt.Println(Abs(v))
}
// Scaleメソッドはポインタレシーバではない
// つまりScaleメソッドは、ポインタを引数に取らないためエラーになる
> cannot use &v (type *Vertex) as type Vertex in argument to Scale

//Scale関数内では値は変わるが、関数外では元のVertexの値は変わらない
>5
  • ポインタレシーバであった場合は、レシーバ(呼び出し元)が変数でもポインタでもok(変数の場合は自動で"&変数"として解釈してくれる)
type Vertex struct {
    X, Y float64
}
//*Vertex型(Vertexへのポインタである *Vertex型)
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
}

func main() {
    v := Vertex{3, 4}
    // 値のアドレスでなくでもエラーにならない(vを"&v"として自動で解釈してくれている)
    v.Scale(2)
    fmt.Println(v)
    ScaleFunc(&v, 10)
    fmt.Println(v)
    // もちろん、こちらはアドレスを渡しているので、ok
    p := &Vertex{4, 3}
    p.Scale(3)
    fmt.Println(v)
    ScaleFunc(p, 8)
    fmt.Println(v)
    fmt.Println(v, p)
}

ポインタレシーバ【3】

・復習
"&変数"でアドレスを取得
"*変数"でアドレスを元に参照先の値を取得

//関数1
// 引数に変数を指定しているのに、ポインタをいれるとエラー
func Scale(v Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v:= Vertex{4, 3}
    fmt.Println(Scale(&v))
}

//関数2
//関数の引数にポインタを指定したら、関数の引数は変数でもポインタでもOK
func Scale(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    n = Vertex{5, 4}
    fmt.Println(Scale(n))

    v:= &Vertex{4, 3}
    fmt.Println(Scale(v))
}

//メソッド

//メソッドを設定時、レシーバに変数を指定したら、メソッド実行時のレシーバは変数はもちろん、ポインタ変数でもOK
//・・・レシーバ(メソッドの呼び出し元、今回はVertex)
func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    //レシーバ(呼び出し元)が変数vなのでOK
    fmt.Println(v.Abs())
    fmt.Println(AbsFunc(v))

    p := &Vertex{4, 3}
    // レシーバがポインタ変数(p=&Vertex)でもOK
    fmt.Println(p.Abs())
    fmt.Println(AbsFunc(*p))
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A Tour of Go メモ 【4】

A Tour of Go

Methods

type Vertex struct {
    X, Y float64
}

type Name struct {
    first, last string
}

func(n Name) Greet() string {
    //変数.値でメソッド内で構造体の値を使うことができる
    return "I'm " + n.first + " " + n.last
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    n := Name{"first", "last"}
    fmt.Println(n.Greed())
}

> 5
> I'm first last

#通常の関数としてもかける

type Name struct {
first, last string
}

func Greet(n Name) string {
return "I'm " + n.first + " " + n.last
}

func main() {
n := Name{"first", "last"}
fmt.Println(Greet(n))
}

I'm first last

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む