20210121のGoに関する記事は4件です。

[追記]Go言語でチャンネル書込がブロックされるのを無理矢理解決した話

概要

Go言語のチャンネルは非常に便利なんですが、正直複雑すぎてあまり使いこなせていません。

この記事は、チャンネルの受信側のgoroutineが先に死んだ場合に、送信側のチャンネル書き込みが永久にブロックされるのを無理矢理解決した事例です。

正直こんな書き方がGo言語的に良いのかどうかわからないので、ご意見等いただけると嬉しいです。

バッドノウハウかもしれないのでご注意を!!
[追記]もしかしてBind呼ぶたびに終わらないgoroutineが増えるかも?

参考

Go言語のチャンネル書き込みの時に…

条件 結果
受信ルーチンが生きていてチャンネルが満杯でない ブロックしない
受信ルーチンが生きていてチャンネルが満杯 チャンネルが空くまでブロック
受信ルーチンが死んでいる 永久にブロック
チャンネルにnilが代入されている 永久にブロック
チャンネルがcloseされている panicする

panicするコード

Playgroundへのリンク

panic.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    fmt.Println("Hello, playground")
    ch := make(chan string)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Writer Finished")
        }()
        ch <- "AAA"
        time.Sleep(time.Second * 6)
        ch <- "BBB"
    }()
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Reader Finished")
        }()
        for i := 0; i < 5; i++ {
            select {
            case v := <-ch:
                fmt.Printf("OK:%v\n", v)
            default:
                fmt.Println("NG")
            }
            time.Sleep(time.Second)
        }
    }()
    wg.Wait()
}

受信側のルーチンが5秒で死んで、送信側が6秒後に新しい値を書こうとしてpanicします。

無理矢理解決したコード

Playgroundへのリンク

muriyari.go
package main

import (
    "fmt"
    "sync"
    "time"
)

var m sync.Map

func main() {
    fmt.Println("Hello, playground")
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Writer Finished")
        }()
        m.Store("AAA", "BBB")
        m.Store("CCC", "DDD")
        time.Sleep(time.Second * 6)
        m.Store("EEE", "FFF")
    }()
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Reader Finished")
        }()
        ch := make(chan interface{})
        Bind(ch, "CCC")
        for i := 0; i < 5; i++ {
            select {
            case v := <-ch:
                fmt.Printf("OK:%v\n", v)
            default:
                fmt.Println("NG")
            }
            time.Sleep(time.Second)
        }
    }()
    wg.Wait()
    fmt.Println("Main Finished")
}

func Bind(ch chan interface{}, key string) {
    go func() {
        for {
            if v, ok := m.LoadAndDelete(key); ok {
                ch <- v
            }
            time.Sleep(time.Millisecond * 200)
        }
    }()
}

チャンネルに直接書き込むのではなく、sync.Mapに書き込んでるのでブロックしません。sync.Mapに書き込むところをきちんと型を書いた新たなfuncを定義してやれば、型チェックは働きます。上で定義しているBind関数も同様です。

ちなみに最初はBind関数内でチャンネルを作ろうとしてたんですが、関数を抜けた時点で変数が解放されるのでpanic起こしてダメでした。

パフォーマンスは計測してませんが、生のチャンネルを扱うよりは確実に悪いでしょう。sleepの時間調整である程度パフォーマンス調整が出来るかもしれません。

余談

チャンネルの容量を増やすことで満杯になるまではブロックしない書き込みになるんですが、無限に増やすわけにもいかないのでこんな方法を考えてみました。

設計が悪いのは明らかなんですが、一応なぜこんな設計を考えているのかの事情は説明しておきます。

それはCtrl+CやSIGTERMなどOSのシステムコール時に全てのGoroutineが後始末をしながら終了をして欲しいからで、その際にcontextを使ってルーチンを終了させているのですがルーチンの終了の順番が不定だからです。なので送信側ルーチンが先に死んで正常終了するケースと、受信ルーチンが先に死んで永久にプロセスが動き続ける場合がありました。

これはあくまで本当に無理矢理やった例ですので、もっと洗練されたやり方があったり、ライブラリがあればご紹介いただければ嬉しいです。

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

Go言語でチャンネル書込がブロックされるのを無理矢理解決した話

概要

Go言語のチャンネルは非常に便利なんですが、正直複雑すぎてあまり使いこなせていません。

この記事は、チャンネルの受信側のgoroutineが先に死んだ場合に、送信側のチャンネル書き込みが永久にブロックされるのを無理矢理解決した事例です。

正直こんな書き方がGo言語的に良いのかどうかわからないので、ご意見等いただけると嬉しいです。

バッドノウハウかもしれないのでご注意を!!

参考

Go言語のチャンネル書き込みの時に…

条件 結果
受信ルーチンが生きていてチャンネルが満杯でない ブロックしない
受信ルーチンが生きていてチャンネルが満杯 チャンネルが空くまでブロック
受信ルーチンが死んでいる 永久にブロック
チャンネルにnilが代入されている 永久にブロック
チャンネルがcloseされている panicする

panicするコード

Playgroundへのリンク

panic.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    fmt.Println("Hello, playground")
    ch := make(chan string)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Writer Finished")
        }()
        ch <- "AAA"
        time.Sleep(time.Second * 6)
        ch <- "BBB"
    }()
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Reader Finished")
        }()
        for i := 0; i < 5; i++ {
            select {
            case v := <-ch:
                fmt.Printf("OK:%v\n", v)
            default:
                fmt.Println("NG")
            }
            time.Sleep(time.Second)
        }
    }()
    wg.Wait()
}

受信側のルーチンが5秒で死んで、送信側が6秒後に新しい値を書こうとしてpanicします。

無理矢理解決したコード

Playgroundへのリンク

muriyari.go
package main

import (
    "fmt"
    "sync"
    "time"
)

var m sync.Map

func main() {
    fmt.Println("Hello, playground")
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Writer Finished")
        }()
        m.Store("AAA", "BBB")
        m.Store("CCC", "DDD")
        time.Sleep(time.Second * 6)
        m.Store("EEE", "FFF")
    }()
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            fmt.Println("Reader Finished")
        }()
        ch := make(chan interface{})
        Bind(ch, "CCC")
        for i := 0; i < 5; i++ {
            select {
            case v := <-ch:
                fmt.Printf("OK:%v\n", v)
            default:
                fmt.Println("NG")
            }
            time.Sleep(time.Second)
        }
    }()
    wg.Wait()
    fmt.Println("Main Finished")
}

func Bind(ch chan interface{}, key string) {
    go func() {
        for {
            if v, ok := m.LoadAndDelete(key); ok {
                ch <- v
            }
            time.Sleep(time.Millisecond * 200)
        }
    }()
}

チャンネルに直接書き込むのではなく、sync.Mapに書き込んでるのでブロックしません。sync.Mapに書き込むところをきちんと型を書いた新たなfuncを定義してやれば、型チェックは働きます。上で定義しているBind関数も同様です。

ちなみに最初はBind関数内でチャンネルを作ろうとしてたんですが、関数を抜けた時点で変数が解放されるのでpanic起こしてダメでした。

パフォーマンスは計測してませんが、生のチャンネルを扱うよりは確実に悪いでしょう。sleepの時間調整である程度パフォーマンス調整が出来るかもしれません。

余談

チャンネルの容量を増やすことで満杯になるまではブロックしない書き込みになるんですが、無限に増やすわけにもいかないのでこんな方法を考えてみました。

設計が悪いのは明らかなんですが、一応なぜこんな設計を考えているのかの事情は説明しておきます。

それはCtrl+CやSIGTERMなどOSのシステムコール時に全てのGoroutineが後始末をしながら終了をして欲しいからで、その際にcontextを使ってルーチンを終了させているのですがルーチンの終了の順番が不定だからです。なので送信側ルーチンが先に死んで正常終了するケースと、受信ルーチンが先に死んで永久にプロセスが動き続ける場合がありました。

これはあくまで本当に無理矢理やった例ですので、もっと洗練されたやり方があったり、ライブラリがあればご紹介いただければ嬉しいです。

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

sdk-goでdynamoDBの複雑なstructの値をupdateする

やりたいこと

GoでのdynamoDBに対しての操作は割とめんどかったりします。単純なStringやIntのものであればまだ良いのですが、ListのMapのなかのListとか、値の構造が複雑になってくると気が滅入ります。

// シンプルなパターン
&dynamodb.UpdateItemInput{
  TableName: aws.String("sampleTable"),
  Key: // 省略,
  ExpressionAttributeValues: map[string]*dynamodb.Attribute{
   ":ssv": {
      S: aws.String("sampleStringValue")
    },
   ":siv": {
      N: aws.String(strconv.Itoa(0)) // ここもintをstringに型変換してそれをaws.String()してkeyにはNを指定って感じでわかりづらい。
    },
  },
  UpdateExpression: aws.String("set sampleStringValue=:ssv and sampleIntValue=:siv")
}

// 複雑なパターン
&dynamodb.UpdateItemInput{
  TableName: aws.String("sampleTable"),
  Key: //省略,
  ExpressionAttributeValues: map[string]*dynamodb.Attribute{
    ":slv": {
      L: [
       {
         M: {
           ":smp": {
            aws.String("sample")
            }....
         }
       }
      ]
    },
  }
}

これを楽にやれるのがsdk-goのexpressionってやつです。

expressionの使い方

update := expression.UpdateBuilder{}.Set(
  expression.Name("sampleComplexValue"),
  expression.Value(sampleCompexValue)
)

expr, err := expression.NewBuilder().WithUpdate(update).Build()

&dynamodb.UpdateItemInput{
  TableName: aws.String("sampleTable"),
  Key: //省略,
  ExpressionAttributeNames: expr.Names(),
  ExpressionAttributeValues: expr.Values(),
  UpdateExpression: expr.Update()
}

上の例よりかなりスマートにかけますね。可読性も格段に上がります。
expression.Valuesの引数に複雑な(もちろん単純なものでもなんの問題もありませんが)値を渡すだけで、ここまでできるのは結構ありがたい。

もちろんexpressionは上記の例のような更新処理だけでなく、Query処理等にも使えるので積極的に使っていきたいところですね。

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

Azure FunctionsとGoでZoom会議参加者を取得する

はじめに

リモートワークによりZoom会議が増え、参加者を記録して残すことが増えてきました。
今までは会議にAcceptしたりした人をカウントしたり、会議参加者リストをスクリーンショットしたりして参加者を取得していましたが、書き漏らしたり、スクリーンショットを忘れていたりといったことが起きていました。

Zoom自体に会議参加者をエクスポートする機能はあるのですが、せっかくなのでAPIを叩いて取得できるツールを作ってみようと思い、作り始めました。

構成

ツールを作るに当たり、必要な/やってみたい技術、どんな形にしたいかをまとめてみました。

  • 完成形のツールイメージ
    → Meeting UUIDをチャットツールから送信するだけで参加者一覧の名前が返ってくる

  • 使用する技術と理由

    • Slack(チャットツールなら何でも良かったのですが、手元にSlackがあったので)
    • Azure Functions(会社でAzureを使っているため個人でも使ってみたかった)
    • Go(未体験の言語学習)
    • Zoom(参加者取得)

この技術を使ってイメージ図を作成しました。SlackからAzure Functionsへリクエストを飛ばし、カスタムハンドラーのGoへリクエストを渡します。カスタムハンドラー側でZoom APIにMeeting UUIDをつけたリクエストを飛ばし、レスポンスから参加者を取得してSlackに返します。

Azure FunctionsでGoを使う

去年の12月に、Azure Functionsがアップデートされ、 どんな言語でもCustom handlerで使用できるようになりました。これまではAzure Functionsで使用できる言語は限られていたので、このアップデートはすごく助かりました。
Azure Functions in Any Language with Custom Handlers

また、別ブログにて、このCustom HandlerにGoを使ったサンプルも投稿されていました。この記事が私のやりたいことと似ていたので、この記事を基にしてZoomを組み合わせてみることにしました。
How to build a serverless app using Go and Azure Functions

開発環境

Go: go1.15.2 darwin/amd64
Visual Studio Code: 1.52.1

ツールの制限事項

今回作成したツールでは、過去の会議参加者のAPI(GET
/past_meetings/{meetingUUID}/participants
)を使用しています。このAPIは有料プランのPro以上でしか呼べないので、本ツールを使う場合、Zoomの料金プラン(Choose a plan)で登録しておく必要があります。

作成手順

ZoomのAPI使用設定

ZoomのApp Marketplaceで、JWT認証によるアプリを作成します。このアプリ作成には以下記事を基に作成しました。
Zoom App Marketplace
Zoom APIの使い方:OAuth・JWT による認証方法・Postman での実行方法
Zoom APIを試してみる JWT編

作成した結果がこちらです。今回はここに記載されているAPI KeyとAPI Secretを、後述する環境変数にセットして使用します。

コード

コードを書く準備として、ローカル環境構築をしておきます。この構築では、MicrosoftがGoのクイックスタートガイドを公開しているので、それに沿って構築してください。
Quickstart: Create a Go or Rust function in Azure using Visual Studio Code

今回作成したコードはMeetingParticipantsExporterです。以降、このコードについて説明します。

HTTPServerの起動

mainでHTTP Serverを起動させておきます。これでFunctions HostからCustom Handlerに対して、/api/zoomでリクエストを送信できるようになります。

func main() {
    port, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
    if !exists {
        port = "7071"
    }
    http.HandleFunc("/api/zoom", function.ExportParticipants)
    fmt.Println("Go server listening on port", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Slackからのリクエストを検証

Verifying requests from Slackにて、Slackからのリクエスト検証方法が記載されています。この検証法と、参考にしたHow to build a serverless app using Go and Azure Functionsを基に、ヘッダのシグネチャやタイムスタンプを使用して、Slackからのリクエストかどうかを判定します。

Zoom APIの使用

JWT認証用のトークン作成

Zoom APIを使用するにはJWT認証用のトークンを作成する必要があります。そのトークンをを作成するために、goで書かれているjwtを使用することにしました。このjwtでは、API KeyやSecret Keyが必要だったので、
「ZoomのAPI使用設定」項目で取得したAPI Key、API Secretを、それぞれAPIKEY、SECRETKEYとして環境変数にセットして使用しています。
以下、トークン作成コードです。

import (
・・・
    "github.com/cristalhq/jwt"
・・・
)

type Env struct {
    SecretKey string `required:"true"`
    ApiKey    string `required:"true"`
}

・・・

func ExportParticipants(w http.ResponseWriter, r *http.Request) {

・・・

    key := []byte(goenv.SecretKey)
    signer, _ := jwt.NewSignerHS(jwt.HS256, key)

    // tokenは作成後1分間有効
    now := time.Now()
    after := now.Add(time.Minute)
    claims := jwt.RegisteredClaims{
        Issuer:    goenv.ApiKey,
        ExpiresAt: jwt.NewNumericDate(after),
    }

    builder := jwt.NewBuilder(signer)
    token, _ := builder.Build(claims)

これでZoom APIに使用するトークンを作成できました。

会議参加者のAPIへリクエスト・レスポンス

会議参加者のAPIへリクエストを送ります。使用するAPIの仕様(GET /past_meetings/{meetingUUID}/participants)を見ると、
URL: https://api.zoom.us/v2/past_meetings/{meetingUUID}/participants
Header: Authorization: "Bearer " + JWTアクセストークン
で送る必要があります。
また、レスポンスは以下のような形式となっているため、participants内のnameを取得するようにします。

{
  "page_count": 1,
  "page_size": 30,
  "total_records": 1,
  "next_page_token": "aliqua",
  "participants": [
    {
      "id": "8b29rgg4bb",
      "name": "Ram Shekhar",
      "user_email": "ram.shekhar.123@fkdngfjg.fdghdfgj"
    }
  ]
}

このURL生成、リクエスト、レスポンスからの参加者取得は以下のとおりです。

    var tokenStr string
    tokenStr = token.String()

    meetingParticipantsUrl := "https://api.zoom.us/v2/past_meetings/"
    url := meetingParticipantsUrl + meetingUUID + "/participants"
    req, _ := http.NewRequest("GET", url, nil)
    bearerAccessToken := "Bearer " + tokenStr
    req.Header.Set("Authorization", bearerAccessToken)

    client := new(http.Client)
    resp, _ := client.Do(req)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Printf("Response body: %s", body)
    if err != nil {
        fmt.Printf("Error ocurred while executing ReadAll. %s", err)
        log.Fatal(err)
        return
    }

    //Unmarshall json
    var meetingParticipantsResponseBody MeetingParticipantsResponseBody
    if err := json.Unmarshal(body, &meetingParticipantsResponseBody); err != nil {
        fmt.Printf("Error ocurred while unmarshal body. %s", err)
        log.Fatal(err)
        return
    }

    fmt.Println("Participant")
    participantNames := ""
    for _, participant := range meetingParticipantsResponseBody.Participants {
        fmt.Printf("%s\n", participant.Name)
        participantNames += participant.Name
        participantNames += "\n"
    }

Slackへレスポンス

Slackへは参加者のテキストのみを返すようにします。

type SlackResponse struct {
    Text string `json:"text"`
}

・・・

    slackResponse := SlackResponse{Text: participantNames}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(slackResponse)

Azure上での動作

Azure Functionsでアプリ作成

Azure Portal上で関数アプリを選択し作成していきます。

リソースグループ、関数アプリ名には適当な名前をセットします。ランタイムスタックやバージョンでは、Custom Handlerを使うようにしておきます。

OSはLinuxを使うようにしておき、プランにはサーバーレスを選択します。

その後の監視やタグは必要に応じて設定してください。

ここまでの設定に問題がなければ、エラーメッセージが出ず、作成ボタンを押すことができます。

作成ボタンを押して数分すると作成完了の画面となります。

Azure Functionsにコードをアップロード

ローカル環境のコードをLinux/x64環境用にビルドしておきます。

> GOOS=linux GOARCH=amd64 go build -o go_main cmd/main.go

このコードをVSCodeからアップする方法もありますが、ここではコマンドでアップする方法を実行します。MicrosoftのAzure Functions Core Tools の操作を参照して、Azure Functions Core Toolsをインストールします。
インストール完了後、func azure functionapp publish アプリ名 --customを実行することでAzure Functionsにコードをアップロードできます。
ここでのアプリ名はAzure Functionsの関数アプリで作成したアプリ名となります。

❯ func azure functionapp publish MeetingParticipantsExporter --custom
Getting site publishing info...
Uploading package...
Uploading 7.72 MB [###########################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions in MeetingParticipantsExporter:
    zoom - [httpTrigger]
        Invoke url: https://meetingparticipantsexporter.azurewebsites.net/api/zoom?code=xxxxx

これでAzure Functionsへのコードアップロードは完了です。
アップロード後のInvoke url: https://meetingparticipantsexporter.azurewebsites.net/api/zoom?code=xxxxx
は、後述するSlackのAPI使用設定で使用するため、控えておいてください。

SlackのAPI使用設定

slack apiのサイトから、Create New Appsを選択し、任意のアプリ名を入力します。

アプリ作成後、Slash Commandsを選択し、新しいコマンドを作成します。

コマンドの作成ではSlackからどのように呼び出すかを入力していきます。
/zoom meetinguuidと呼び出したいので、コマンドには/zoomとします。またRequest URLは、先程控えていたURLを入力します。

コマンドの作成が終わったら、Install to Workspaceボタンを押して、作成したアプリをワークスペースにインストールします。

インストールが終わったら、App Credentialsにある、Sigining Secretを控えておきます。

環境変数設定

Azure Functionsの構成項目で、ZoomのAPI Key、Secret Key、SlackのSigining SecretをそれぞれAPIKEY、SECRETKEY、SLACK_SIGNING_SECRETの環境変数としてセットします。

環境変数セット後、Azure Functionsのアプリを再起動しておきます。
これで準備は整いました。

結果

Slackから/zoom meetinguuidを送ると、参加者のリストが返されます。

おわりに

各技術を使って、Zoomの参加者を取得するツールを作成しました。触ったことのない技術を独学で使っていくのは大変でしたが、一歩ずつ前に進める経験は結構楽しいですね。
ツールを作るにあたり、本記事に掲載した記事の作者の方々、Twitterでアドバイスを頂いた、@nthonyChuさん、@u_phyさん、ありがとうございました。

この記事が誰かのお役に立てれば幸いです。

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