go言語reflectメモ
reflect パッケージについてメモ
他記事:
reflectとは
- reflectはGoでReflection機能を提供するためのパッケージ
- このパッケージの関数を使うことで、引数の型や値を動的に取得できる
使い方1. (reflectで参照した変数の値を変更しないとき)
package main import ( "fmt" "reflect" ) type Point struct { X int Y int } func main() { p := Point{10, 5} // 変数を定義 rv := reflect.ValueOf(p) fmt.Printf("rv.Type = %v\n", rv.Type()) // 名前空間付きの型名 fmt.Printf("rv.Kind= %v\n", rv.Kind()) // 格納リソース種別 fmt.Printf("rv.Interface= %v\n", rv.Interface()) // interface{}としての実際の値 xv := rv.Field(0) // rv内のX要素を取り出し fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示 }
実行結果
rv.Type = main.Point rv.Kind= struct rv.Interface= {10 5} xv: 10
- 変数の値を変更しないのであれば下のように変数(p)を直接reflect.ValueOfに入れると、reflect.Valueが取得できる
Typeでreflect.Valueの名前空間付きの型の名前(例ではmain.Point)を取得できる
- https://golang.org/pkg/reflect/#Value.Type
Type returns v's type.
- https://golang.org/pkg/reflect/#Value.Type
Kindでint, struct, mapなどの型種別が取得できる
- https://golang.org/pkg/reflect/#Value.Kind
Kind returns v's Kind. If v is the zero Value (IsValid returns false), Kind returns Invalid.
- https://golang.org/pkg/reflect/#Value.Kind
Fieldでstructのフィールドを取得できる
- https://golang.org/pkg/reflect/#Value.Field
Field returns the i'th field of the struct v. It panics if v's Kind is not Struct or i is out of range.
- https://golang.org/pkg/reflect/#Value.Field
使い方2. (reflectで参照した変数の値を変更するとき)
- 変数の値を変更するときは、SetInt(SetFloat, SetStringなど他にもいろいろ)を使う
- ただし、変更するためには以下のようにValueOfにポインタを渡して、そのElemを取る必要がある
Elemはinterfaceの値もしくはポインタの参照先を返す
- reflect - The Go Programming Language
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.
- reflect - The Go Programming Language
package main import ( "fmt" "reflect" ) type Point struct { X int Y int } func main() { p := &Point{X: 10, Y: 5} // ポインタにすることによって編集可能にする rv := reflect.ValueOf(p).Elem() // Elemによってポインタが指している先の値を取得する fmt.Printf("rv.Type = %v\n", rv.Type()) // 名前空間付きの型名 fmt.Printf("rv.Kind= %v\n", rv.Kind()) // 格納リソース種別 fmt.Printf("rv.Interface= %v\n", rv.Interface()) // interface{}としての実際の値 xv := rv.Field(0) // rv内のX要素を取り出し fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示 xv.SetInt(100) // intの100をX要素にセット fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示 }
実行結果
rv.Type = main.Point rv.Kind= struct rv.Interface= {10 5} xv: 10 after SetInt xv: 100
【値を変更しようとしてpanicになる場合1】ポインタを渡さなかった場合
- ポインタではなく値を変更しようとするとpanicになる
func main() { p := Point{X: 10, Y: 5} rv := reflect.ValueOf(p) xv := rv.Field(0) // rv内のX要素を取り出し fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示 xv.SetInt(100) // intの100をX要素にセット => panic: reflect: reflect.Value.SetInt using unaddressable value fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示 }
実行結果
xv: 10 panic: reflect: reflect.Value.SetInt using unaddressable value goroutine 1 [running]: reflect.flag.mustBeAssignable(0x82, 0xf0a20) /usr/local/go/src/reflect/value.go:234 +0x180 reflect.Value.SetInt(0xf0460, 0x414020, 0x82, 0x7, 0x64, 0x0) /usr/local/go/src/reflect/value.go:1542 +0x40 main.main() /tmp/sandbox268672126/prog.go:18 +0x200
【値を変更しようとしてpanicになる場合2】Elemを使わなかった場合
- Elemでreflect.Valueの指し示す先の値を取得していないとFieldメソッドを使った時にpanicになる
- Field (再掲)
- reflect - The Go Programming Language
Field returns the i'th field of the struct v. It panics if v's Kind is not Struct or i is out of range.
- reflect - The Go Programming Language
func main() { p := &Point{X: 10, Y: 5} rv := reflect.ValueOf(p) xv := rv.Field(0) // rv内のX要素を取り出し => panic: reflect: call of reflect.Value.Field on ptr Value fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示 xv.SetInt(100) // intの100をX要素にセット fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示 }
実行結果
panic: reflect: call of reflect.Value.Field on ptr Value goroutine 1 [running]: reflect.Value.Field(0xe83c0, 0x414020, 0x16, 0x0, 0x40c120, 0x0, 0x90b80, 0x38f1) /usr/local/go/src/reflect/value.go:813 +0x160 main.main() /tmp/sandbox389676022/prog.go:16 +0xe0
値を変更できるか(SetXXXを使えるか)はCanSetを使うと判定できる
package main import ( "fmt" "reflect" ) type Point struct { X int Y int } func main() { p := Point{X: 10, Y: 5} rv := reflect.ValueOf(p) if rv.CanSet() { fmt.Println("p CanSet: true") xv := rv.Field(0) // rv内のX要素を取り出し fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示 xv.SetInt(100) fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示 } else { fmt.Println("p CanSet: false") } pPtr := &Point{X: 10, Y: 5} rv = reflect.ValueOf(pPtr).Elem() if rv.CanSet() { fmt.Println("pPtr CanSet: true") xv := rv.Field(0) // rv内のX要素を取り出し fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示 xv.SetInt(100) fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示 } else { fmt.Println("pPtr CanSet: true") } }
実行結果
p CanSet: false pPtr CanSet: true xv: 10 after SetInt xv: 100
使えそうな使用例
1. 構造体のフィールドを順に出力
package main import ( "fmt" "reflect" ) type Point struct { X int Y int Z int } func (p Point) PrintFiled() { v := reflect.ValueOf(p) t := v.Type() for i := 0; i < t.NumField(); i++ { // 構造体のフィールド名、フィールドの値、フィールドの型を出力 fmt.Printf("%s %d %T\n", t.Field(i).Name, v.Field(i).Interface(), v.Field(i).Interface()) } } func main() { p1 := Point{X: 10, Y: 5} p1.PrintFiled() fmt.Println("----") p2 := Point{X: 100, Y: 50, Z: 7} p2.PrintFiled() }
実行結果
X 10 int Y 5 int Z 0 int ---- X 100 int Y 50 int Z 7 int
2. 構造体の型が何であってもinterface型のSliceにまとめて返す
- どんな型であってもinterfaceのスライスにしたい場面があったので
- 以下のように、toInterfaceSlice関数を定義して、PointとUserのInterfaceSliceメソッドの中でこの関数を呼ぶだけにすることで共通化できた
- toInterfaceSlice関数の引数はinterface{}なのでどの構造体でも受け付けられる
package main import ( "fmt" "reflect" ) func toInterfaceSlice(v interface{}) []interface{} { var vs []interface{} rv := reflect.ValueOf(v) rt := rv.Type() for i := 0; i < rt.NumField(); i++ { vs = append(vs, rv.Field(i).Interface()) } return vs } type Point struct { X int Y int Z int } func (p Point) InterfaceSlice() []interface{} { return toInterfaceSlice(p) } type User struct { ID int Name string Height float64 Weight float64 } func (u User) InterfaceSlice() []interface{} { return toInterfaceSlice(u) } func main() { p := Point{X: 10, Y: 5, Z: 1} for _, v := range p.InterfaceSlice() { fmt.Printf("%v %T\n", v, v) } fmt.Println("----") u := User{ID: 1001, Name: "hoge", Height: 170.1, Weight: 60.45} for _, v := range u.InterfaceSlice() { fmt.Printf("%v %T\n", v, v) } }
実行結果
10 int 5 int 1 int ---- 1001 int hoge string 170.1 float64 60.45 float64
2-2. 関数に渡すパラメータが構造体のポインタの場合でも対応できるようにする
- 上のtoInterfaceSlice関数で引数が構造体のポインタの場合には以下のようにElemを使って実体を取得するようにすれば、 引数が構造体の場合も、構造体のポインタの場合も両方対応できる
func toInterfaceSlice(v interface{}) []interface{} { var vs []interface{} rv := reflect.ValueOf(v) fmt.Printf("rv.Kind %v\n", rv.Kind()) // パラメータvが構造体のポインタのときはElemでポインタの指している先の値を取得する if rv.Kind() == reflect.Ptr { // vが構造体のポインタの時はここを通る rv = reflect.ValueOf(v).Elem() } rt := rv.Type() for i := 0; i < rt.NumField(); i++ { vs = append(vs, rv.Field(i).Interface()) } return vs } type Point struct { X int Y int Z int } func (p Point) InterfaceSlice() []interface{} { // toInterfaceSliceには構造体を渡す return toInterfaceSlice(p) } type User struct { ID int Name string Height float64 Weight float64 } func (u *User) InterfaceSlice() []interface{} { // toInterfaceSliceには構造体のポインタを渡す return toInterfaceSlice(u) }
全体のコードは以下
実行結果
rv.Kind struct 10 int 5 int 1 int ---- rv.Kind ptr 1001 int hoge string 170.1 float64 60.45 float64
2-3. 構造体のフィールドに構造体を含んでいる場合にも対応できるようにする
- 以下のように、構造体のフィールドに構造体を持つ場合(ネストしている場合)にも対応できるようにしたい
type GamePlayerInfo struct { User Point }
- 以下のように条件分岐して再帰を使ってみる
func toInterfaceSlice(v interface{}) []interface{} { var vs []interface{} rv := reflect.ValueOf(v) if rv.Kind() == reflect.Ptr { rv = reflect.ValueOf(v).Elem() } rt := rv.Type() for i := 0; i < rt.NumField(); i++ { if rv.Field(i).Kind() == reflect.Struct { // フィールドがstructの場合は再帰でinterfaceのSliceを取得して後ろにつなげる sl := toInterfaceSlice(rv.Field(i).Interface()) vs = append(vs, sl...) } else { vs = append(vs, rv.Field(i).Interface()) } } return vs }
全体のコードは以下
実行結果
10 int 5 int 1 int ---- 1001 int hoge string 170.1 float64 60.45 float64 ---- 1001 int hoge string 170.1 float64 60.45 float64 10 int 5 int 1 int
2-4. 構造体のフィールドで〇〇メソッドを持つ場合はそれを使うようにする(おまけ)
- 以下のように、Attribute変数(Enum)のように、Stringメソッドを持つ場合はそれで出力させる
- MethodByName を使う
type Attribute int const ( Fire Attribute = iota Water Earth Air ) // constのString変換メソッド func (a Attribute) String() string { return []string{"Fire", "Water", "Earth", "Air"}[a] } type SpecialGamePlayerInfo struct { User Point Attribute }
参考
Go言語におけるinterface{}とリフレクションを使ったパターン - Qiita
reflect-examples/README.md at master · a8m/reflect-examples · GitHub
CloudSQLを安くするために考えたこと
無料トライアル期間が終わったのでCloudSQLのマイグレーションをした話
安さを追求したわけではなく、自分なりの妥協点を探っただけ
事象
2019/5/22 CloudSQLに接続できなくなった
$ gcloud sql connect myfinance --user=root ERROR: (gcloud.sql.connect) HTTPError 409: The instance or operation is not in an appropriate state to handle the request.
GoogleAppEngineのログには以下が出力されていた
failed to insert table: daily, err: driver: bad connection, query:
原因
Google Cloud PlatformのUIの一番上を見ると無料トライアル期間が終わったとを通知していた
上のUIの右の「アップグレード」ボタンを押すと以下のように表示された
これを承諾するまえに、そもそも無料トライアル期間中はいくらくらい使っていたのか確認してみた
これからかかるはずの料金の確認
レポートを見てみる
レポートの見方 - 請求レポートによる費用傾向の表示 | Cloud Billing のドキュメント | Google Cloud
「お支払い」の「レポート」を見てみた
レポートの、先月分をプロダクト単位で見てみる - グループ条件を変更
SKU単位で見てみると使用しているサービスの内訳がわかる
SKU | プロダクト | SKU ID | 使用 | クレジット適用前の費用 | プロモーション | 割引 | クレジット適用後の費用 |
---|---|---|---|---|---|---|---|
DB standard Intel N1 1 VCPU running in Japan (with 30% promotional discount) | Cloud SQL | 5356-0253-5FBD | 720 hour | ¥6,996 | ¥-6,996 | — | ¥0 |
Storage PD SSD for DB in Japan | Cloud SQL | 9D66-0506-E274 | 10 gibibyte month | ¥244 | ¥-244 | — | ¥0 |
特にこれ - DB standard Intel N1 1 VCPU running in Japan (with 30% promotional discount) - クレジット適用前の費用:¥6,996
・・・なにこの値段
SKUって?
SKUは最小管理単位 (Stock Keeping Unit) の略。
Google Cloud PlatformにはSKUの意味について説明がなかったのでたぶんこれのことだと思う
毎月の請求について | Cloud Billing のドキュメント | Google Cloud
SKU ID サービスが使用するリソースの ID。SKU の詳細な一覧については、GCP SKU をご覧ください。
GCP SKU一覧
この単位で料金を決めているらしい
この金額が正当なのか確認
上で表示したレポートのSKUの一覧を見ると、 DB standard Intel N1 1 VCPU running in Japan (with 30% promotional discount) に一番金がかかっているらしい
- ¥6,996
この「DB standard Intel N1 1 VCPU running in Japan (with 30% promotional discount)」を上の公式の表「GCP SKU一覧」から探すとあった
DB standard Intel N1 1 VCPU running in Japan (with 30% promotional discount) - 0.088 USD per hour
この0.088を4月中フル稼働した場合(720時間)で、現在の円ドルレート 約110円を当てはめると、 0.088720110=6969.6 となって上とほぼ等しい
with 30% promotional discount
この「with 30% promotional discount」というのは、 「継続利用割引」というサービスらしい
Google Compute Engine の料金 | Compute Engine ドキュメント | Google Cloud
継続利用割引は、使用する vCPU とメモリ量ごとに計算されます。Compute Engine では、1 か月の 25% を超える期間にわたり vCPU またはメモリ量がインスタンスで使用されると、そのリソースの使用が 1 秒延びるごとに割引が自動的に適用されます。 使用時間が増えるほど割引率は高くなり、1 か月ずっと稼働するインスタンスでは vCPU とメモリの費用の最大 30% の正味割引を受けることができます。
CloudSQLの料金体系について確認する
Cloud SQL の料金 | Cloud SQL ドキュメント | Google Cloud
自分が使っているのはCloudSQLのMysql 第 2 世代なのでそれを見る
MySQL 第 2 世代の料金 第 2 世代の料金は、次の料金で構成されます。 インスタンスの料金 ストレージの料金 ネットワークの料金
mysql 2ndの料金
東京(asia-northeast1)を見ると、
マシンタイプ | 仮想 CPU 数 | RAM(GB) | 最大ストレージ容量 | 最大接続数 | 料金(米ドル) | 継続利用価格(米ドル) |
---|---|---|---|---|---|---|
db-n1-standard-1 | 1 | 3.75 | 10,230 GB | 4,000 | $0.13 | $0.09 |
となっていた。
これは上のDB standard Intel N1 1 VCPU running in Japan (with 30% promotional discount) の 0.088という数字と一致する
なるほど・・・高い
各インスタンスの設定
インスタンスの設定 | Cloud SQL ドキュメント | Google Cloud
こちらに詳しく書いてある
どうして720時間(一月休みなく動かし続けるとこの時間)使っていることになっているのか?
これが一番疑問だった
自分のサービスはCronで一日せいぜい2時間くらいしか動いていないのにどうして休みなく稼働していることになっているのか?
調べてみると、以下の通り明示的にDBを停止させない限り、ずーっと稼働していて、その分料金もかかるらしい
Mysqlの第 2 世代の特徴
第 2 世代機能 | Cloud SQL ドキュメント | Google Cloud
料金の違い Cloud SQL 第 2 世代では、従量制の料金パッケージが提供されていません。インスタンスの料金は、マシンタイプによって決まります。分単位の課金と継続利用割引の導入により、Cloud SQL 第 2 世代では多くのワークロードで費用対効果を高めることができます。詳しくは、料金のページをご覧ください。
アクティベーションポリシー
インスタンスの設定 | Cloud SQL ドキュメント | Google Cloud
アクティベーション ポリシー 第 2 世代インスタンスの場合、アクティベーション ポリシーはインスタンスを起動または停止するためにのみ使用されます。アクティベーション ポリシーを [常にオン] に設定するとインスタンスが起動し、[オフ] に設定するとインスタンスが停止します。
つまり、オフにしない限り利用し続けている状態となるらしい
- 第1世代にはON DEMAND というポリシーがあって、リクエストが来たら起動する(立ち上がりに時間かかるけど)という仕組みがあったらしいが、それは第2世代にはない
インスタンスの起動、停止はどうすればいいのか
インスタンスの起動、停止、再起動 | Cloud SQL | Google Cloud
ブラウザまたはgcloudコマンドで起動・停止できるらしい
料金を安くできないか
以上より、料金を安く抑えてCloudSQLを使うためには以下の3つが考えられそう
3番目については自動でgcloudコマンドを実行できるようにそのうちしたい
一番安いリージョンの一番安いマシンを調べる
https://cloud.google.com/sql/pricing?hl=ja
をいろいろ見た結果、 アメリカなどのリージョンのdb-f1-microマシンが一番安かったのでこれを選択 なんでも良かったので、アイオワにする
リージョン | マシンタイプ | 仮想 CPU 数 | RAM(GB) | 最大ストレージ容量 | 最大接続数 | 料金(米ドル) | 継続利用価格(米ドル) |
---|---|---|---|---|---|---|---|
アイオワ | db-f1-micro* | 共有 | 0.6 | 3,062 GB | 250 | $0.0150 | $0.0105 |
東京 | db-n1-standard-1 | 1 | 3.75 | 10,230 GB | 4,000 | $0.1255 | $0.0878 |
アイオワのdb-f1-micro は東京のdb-n1-standard-1の1/8以下
継続利用価格(米ドル)では$0.0105 / hour
※この料金は一時間あたりの利用料金のことらしい
Google Cloud Platform 料金計算ツール | Google Cloud Platform | Google Cloud - このツールで見積もりもできる
自分のサービスはSLAがめちゃめちゃ低く、レイテンシーはほとんど気にしない。データサイズも数十GBなので、 db-f1-microで十分そう
想定費用を計算してみた
リージョン×マシンタイプ | 料金(24時間×31日×110円 ※ 110は現在のレート) |
---|---|
アイオワ db-f1-micro | 0.01052431*110=859.32円 |
東京 db-n1-standard-1o | 0.08782431*110=7185.552円 |
インスタンスの他にストレージとネットワークの料金が別にかかるが、一旦これだけ考える
もとの金額に比べれば、趣味で使う金額としてだいぶ現実的になった
ストレージについて
ストレージはSSDのほうが当然高い
でもこれくらいなら許容しようかな(あとで変えられるのでSSDを選ぶ) - $0.17 per GB/month for SSD storage capacity - $0.09 per GB/month for HDD storage capacity
SKUの表で料金を確認
https://cloud.google.com/skus/?currency=JPY&filter=micro
CloudSQLの表と名前が一対一対応していないのでわかりにくいけど、おそらくこれが対応するSKUとその金額だと思える
DB generic Micro instance with burstable CPU running in Americas (with 30% promotional discount) - 0.0105 USD per hour
新規DBの作成
アップグレード
最初のアップグレードボタンを押して使えるようにする
インスタンスの作成
https://console.cloud.google.com/sql/instances から、アイオワリージョンのdb-f1-microを新しく作る
で「インスタンスを作成」
Mysqlを選択
任意のインスタンスIDとパスワードを設定する
このときに下の「設定オプションを表示」を押すと、マシンタイプなどを細かく設定できる - リージョンはあとから変えられない
※あとでも変更できるけど、何も設定しないとdb-n1-standard-1になってしまうので超注意!!
インスタンスの編集
上の、「設定オプションを表示」をいじらなくても、あとからマシンタイプなどは変えられる
Google Cloud Platform のSQLでインスタンスが見られるので、その画面の中の「設定」→「設定を編集」から設定を変更できる
マシンタイプとストレージの設定
マシンタイプとストレージの設定を編集
変更前
変更後
- マシンタイプ:db-f1-micro
- ストレージの種類:SSD
- ストレージ容量 :最低の10GB
- ストレージの自動増量を有効化:チェックした
自動バックアップの有効化
しない - とりあえず、自分で定期的に頑張ることにする
新旧DBデータ移行汎用手順
0. 準備
Cloud SQL for MySQL の使用 | Go の App Engine スタンダード環境 | Google Cloud
Cloud SQL for MySQL のクイックスタート | Cloud SQL for MySQL | Google Cloud
これに従ってcloud_sql_proxyをローカルマシンに入れておく
事前に新旧DBのインスタンス接続名を調べておく
Google Cloud Platform の左上のボタンを押してからSQLを選んで、
対象のインスタンスIDを選択して、 「このインスタンスに接続」->「インスタンス接続名」から取得できる
1. 旧DBへのプロキシサーバ立ち上げ
ローカルの端末で以下を実施
$ cloud_sql_proxy -instances=<インスタンス接続名>=tcp:3307
2. 旧DBのテーブルをDump(Export)
別の端末を開いてTableデータをDump
$mysqldump -u root -p --host 127.0.0.1 --port 3307 <Database名> <テーブル名> > dumpdata.sql
- Database名: show databases; で出てくるなかの対象のDatabase
3. 新DBのテーブルへImport
新DBへのプロキシサーバ立ち上げ
先程までの旧DBに繋がっていたプロキシサーバは止めて、ローカルの端末で以下を実施
$ cloud_sql_proxy -instances=<新DBのインスタンス接続名>=tcp:3307
4. 新DBでDatabaseを作っておく
別の端末で新DBに接続
$mysql -u root -p --host 127.0.0.1 --port 3307 MySQL [(none)]> CREATE DATABASE <Database名>;
5. 新DBのテーブルへImport
別の端末を開いてTableデータをImport
$mysql -u root -p --host 127.0.0.1 --port 3307 <Database名> < dumpdata.sql
GoogleHomeまたはAndroidに話しかけてスプレッドシートにメモを取る
概要
GoogleHomeやAndroidに話しかけるだけでスプレッドシートにメモを取ることができる
ちょっとしたメモを取るのに便利
こちらとほぼ同じ方法で作った
手順
これだけ 1.スプレッドシートを用意する 2.スプレッドシートにGoogleAppScriptを追加する 3.IFTTTを登録する
1.スプレッドシートシートを用意する
新しいスプレッドシートを作成 → 空白を選ぶ
ここでは適当に「test」という名前のスプレッドシートにしたが何でもいい
2.スプレッドシートにGoogleAppScriptを追加する
GoogleAppScriptを作成
上のタブの「ツール」→ 「スクリプト エディタ」を選択
適当に「test-gas」というプロジェクト名にするが何でもいい
コード.gsに以下を記入する
function addDate() { var sheet = SpreadsheetApp.getActiveSheet(); var lastRow = sheet.getLastRow(); var date = new Date(); var unixTimestamp = Math.round( date.getTime() / 1000 ); sheet.getRange(lastRow, 1).setValue(date); }
「lastRow, 1」にすると縦一列目に入力時刻が記載される
トリガーを追加
コード.gsの上の部分の時計のアイコンを選択
「新しいトリガーを作成します。」をクリック
必要な部分を選択 - 実行する関数を選択: addDate - 実行するデプロイを選択:Head ←これしかなかった - イベントのソースを選択:スプレッドシートから - イベントの種類を選択:変更時 - エラー通知設定:1週間おきに通知を受け取る ← 好みで変えていい
記載したら保存
「アカウントの選択」でGmailIDを選択
「このアプリは確認されていません」と出るが、無視して詳細を選択して、 「~~(安全ではないページ)に移動」をクリックする
「test-gas が Google アカウントへのアクセスをリクエストしています」 の下の「許可」を押す
ここまでの動作確認
ここまでやったら最初のtestシートに戻る
Bより右の列に適当に入力してEnterキーを押すと以下のようにA列に日付が自動で出力されるはず
これだけでもいいが、時刻までシートに表示させたいので、表示形式を変える
A列目を全部選択して、「表示形式」→ 「数値」→「日時」を選択
またA列目を全部選択して右クリックから「条件付き書式」を選ぶと、以降A列目だけ色がつくので見やすい(やらなくてもいい)
3.IFTTTを登録する
IFTTTのトップページにいって「My Applets」を選択
New Appletを選択
this
Google Assistantを選択
Say a phrase with a text ingredient を選択
「テスト $」や「test $」など発音しやすい(かつGoogleAssistantが他の言葉と区別しやすい)言葉を選択
Create triggerを押す
that
Google Sheetsを選択
Add row to spreadsheet を選択して以下のようにする
もしGoogleAppScriptおよびトリガーを設定せずに、左の列にCreatedAtを選択するとGoogleHome経由で入力すると「May 14, 2019 at 08:48PM」のような形式でスプレッドシートに記載される この機能は昔は機能していなかった気がするので、GoogleAppScriptをわざわざ作ったんだけど。。 ただ、日付の形式は変えられない。 もし形式がこれでよければスプレッドシート側のスクリプトは不要そう
動作確認
GoogleHomeまたはAndroidで「OK Google テスト 123」
なぜかGoogleHome経由からだと時刻が表示されない。 でもセルを見れば時刻がわかるからいいかな。。
GoogleHome経由でスプレッドシートに記入されないとき
Google ドライブ - 1 か所であらゆるファイルを保管 Google Driveを見て、以下のthatで定義したGoogleHomeのフォルダに対象のシートが入っているか確認する
別のフォルダの同名のシートに書き込まれていることがあった
GoogleAppEngine(GAE)でgoroutineを使う
GoogleAppEngineでgoroutineを使う
以下に記載されている通り、GoogleAppEngineでは並列処理はサポートしていない。
Go Runtime Environment | App Engine standard environment for Go | Google Cloud
App Engine の Go ランタイム環境は、goroutine を完全にサポートしていますが、並列実行はサポートしていません。goroutine は単一のオペレーティング システム スレッドにスケジュールされます。このシングルスレッド制限は、今後のバージョンで解除される予定です。特定のインスタンスで複数のリクエストを同時に処理することができます。これは、1 つのリクエストで Cloud Datastore API の呼び出しを待っている間に、同じインスタンスで別のリクエストを処理できることを意味します。
しかし、並行処理はできる
これは、同時に複数の処理を実行すること(並列処理)はできないが、APIのレスポンスなどを待つ間に別の処理をすること(並行処理)はできるということを意味する
【脱線】並行性と並列性の違い
「Go言語による並行処理」ではこの違いを以下の一文で表している
並行性はコードの性質を指し、並列性は動作しているプログラムの性質を指します。
ピンと来ない表現・・・
検証
検証用のコード
以下のような3つのHandlerを作成して、それぞれの挙動を確認する - testHandler - testGoroutineHandler - testGoroutineHandler2
挙動の違いが分かりやすいように、time.Sleep(3 * time.Second)を入れている
このコードは以下の「複数のgoroutineの結果の取得」に書いたものを使っている go言語の並行処理 - ludwig125のブログ
package main import ( "fmt" "net/http" "sync" "time" "google.golang.org/appengine" // Required external App Engine library "google.golang.org/appengine/log" "google.golang.org/appengine/urlfetch" // 外部にhttpするため(http.GetはGAEでは使えない) ) func main() { http.HandleFunc("/test", testHandler) http.HandleFunc("/test_goroutine", testGoroutineHandler) http.HandleFunc("/test_goroutine2", testGoroutineHandler2) appengine.Main() // Starts the server to receive requests } func testHandler(w http.ResponseWriter, r *http.Request) { // GAE log ctx := appengine.NewContext(r) client := urlfetch.Client(ctx) urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} checkStatus := func(urls ...string) []*http.Response { var response []*http.Response for _, url := range urls { log.Infof(ctx, "client.Get '%s'", url) resp, err := client.Get(url) if err != nil { log.Errorf(ctx, "%v", err) continue } response = append(response, resp) // 関数の実行にかかる時間を遅らせる log.Infof(ctx, "sleep 3 sec") time.Sleep(3 * time.Second) } return response } for _, response := range checkStatus(urls...) { log.Infof(ctx, "checkStatus: status: %s", response.Status) } } func testGoroutineHandler(w http.ResponseWriter, r *http.Request) { // GAE log ctx := appengine.NewContext(r) client := urlfetch.Client(ctx) type Result struct { Error error Response *http.Response } checkStatus := func( done <-chan interface{}, urls ...string, ) <-chan Result { resultChan := make(chan Result) go func() { defer close(resultChan) for _, url := range urls { log.Infof(ctx, "client.Get '%s'", url) resp, err := client.Get(url) // 関数の実行にかかる時間を遅らせる log.Infof(ctx, "sleep 3 sec") time.Sleep(3 * time.Second) result := Result{Error: err, Response: resp} select { case <-done: return case resultChan <- result: } } }() return resultChan } done := make(chan interface{}) defer close(done) urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} for result := range checkStatus(done, urls...) { if result.Error != nil { log.Infof(ctx, "error: %v", result.Error) continue } log.Infof(ctx, "checkStatus: status: %s", result.Response.Status) } } urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} for result := range checkStatus(done, urls...) { if result.Error != nil { log.Infof(ctx, "error: %v", result.Error) continue } //log.Infof(ctx, "%s", formatStatus(result.Response.Status)) log.Infof(ctx, "checkStatus: status: %s", result.Response.Status) } } func testGoroutineHandler2(w http.ResponseWriter, r *http.Request) { // GAE log ctx := appengine.NewContext(r) client := urlfetch.Client(ctx) type Result struct { Error error Response *http.Response } checkStatus := func( done <-chan interface{}, urls []string, ) <-chan Result { resultChan := make(chan Result, len(urls)) wg := new(sync.WaitGroup) defer close(resultChan) for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() log.Infof(ctx, "client.Get '%s'", url) resp, err := client.Get(url) // 関数の実行にかかる時間を遅らせる log.Infof(ctx, "sleep 3 sec") time.Sleep(3 * time.Second) select { case <-done: return case resultChan <- Result{Error: err, Response: resp}: } }(url) } wg.Wait() return resultChan } done := make(chan interface{}) defer close(done) urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} for result := range checkStatus(done, urls) { if result.Error != nil { log.Infof(ctx, "error: %v", result.Error) continue } log.Infof(ctx, "checkStatus: status: %s", result.Response.Status) } }
実行して結果を確認
GoogleAppEngineの左側からトレースを選ぶと詳細がみられる
またトレース画面の右側のログの「表示」をクリックするとログがみられる
1.test(testHandler)
トレース - 3つのURLの処理を3秒おきに実行していることがわかる
ログ
2.test_goroutine(testGoroutineHandler)
- goroutineを使っているが、URLは3つを順番に処理している
- 3つそれぞれで3秒ごとのSleepを入れているため、全体の実行時間は9秒ちょっとになっている
トレース
ログ
3.test_goroutine2(testGoroutineHandler2)
- (ほぼ)同時に3つのgoroutineを実行している
- 「並列実行はサポートしていません」と公式に書いてあるので、おそらく全く同時ではないはずだが、これ見るとほぼ同時に見える。。
- ともかく、並行で3つの処理を行うようにしたことで、全体としての処理時間が3秒ちょっとになった
トレース
ログ
相関係数について
相関係数について
下記相関係数の導出までの説明
正の相関と負の相関
- データ列1のように、年月日に対して値が増加している場合、これを「正の相関がある」という
- データ列2のように、年月日に対して値が増加している場合、これを「負の相関がある」という
年月日 | 20140101 | 20140102 | 20140103 | 20140104 | 20140105 | 20140106 | 20140107 | 20140108 | 20140109 | 20140110 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
データ列1 | 正の相関 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | |
データ列2 | 負の相関 | 110 | 109 | 108 | 107 | 106 | 105 | 104 | 103 | 102 | 101 |
相関を数値で表す
参考:http://mcn-www.jwu.ac.jp/~yokamoto/ccc/stat/p22covcor/
年月日を変数x、相関関係を考えたい数値を変数yとする 変数xの平均をm_x、変数yの平均をm_yとする 変数の数をNとする
- Σ(x-m_x)(y-m_y)/N を計算すると、xとyに正の相関があるか、負の相関があるか分かる ⇒ 上記をxとyの 共分散 という
- 例えばデータ列1の例では、年月日をx、データ列1の数値をyとすると下記のような計算になる
xの平均 = (20140101 + 20140102 + ・・・ + 20140110)/10 = 20140105.5 yの平均 = (101 + 102 + ・・・ + 110)/10 = 105.5 xとyの共分散 = Σ(x-m_x)(y-m_y)/N = { (20140101-20140105.5) × (101-105.5) + ・・・ +(20140110-20140105.5) × (110-105.5) }/10 = 8.25
データ列1とデータ列2の共分散を計算すると下記の通り(エクセルではCOVAR関数を使用)
テータ列 | 相関 | 共分散 |
---|---|---|
データ列1 | 正の相関 | 8.25 |
データ列2 | 負の相関 | -8.25 |
共分散で相関係数を考える場合の問題点
- 共分散は変数が等倍されるだけで値が変わってしまうため、
- 複数の単位が異なったデータ列について相関の大きさを比較するには向いていない
- データ列1やデータ列2の数値を100倍したものをそれぞれデータ列3、データ列4とする
年月日 | 20140101 | 20140102 | 20140103 | 20140104 | 20140105 | 20140106 | 20140107 | 20140108 | 20140109 | 20140110 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
データ列3 | 正の相関(数値が100倍) | 10100 | 10200 | 10300 | 10400 | 10500 | 10600 | 10700 | 10800 | 10900 | 11000 | |
データ列4 | 負の相関(数値が100倍) | 11000 | 10900 | 10800 | 10700 | 10600 | 10500 | 10400 | 10300 | 10200 | 10100 |
- データ列3とデータ列4の共分散
テータ列 | 相関 | 共分散 |
---|---|---|
データ列3 | 正の相関(数値が100倍) | 825 |
データ列4 | 負の相関(数値が100倍) | -825 |
⇒ 数値が100倍だと共分散も100倍になってしまう
共分散を相関の大きさだと考えた時の問題点として データ列1、2の数値の単位がメートルだとしたとき、 それをセンチメートル単位で見たものをデータ列3、4とすると、 同じ数字を見ているにもかかわらず、2つの場合で相関の大きさが変化するということになってしまう
相関係数
- 共分散を規格化して、単位をによらない無次元の量としたものを相関係数といい、下記の通り定義する
相関係数 = Σ(x-m_x)(y-m_y)/N ÷ (√(Σ(x-m_x)^2) ÷ (√(Σ(y-m_y)^2) ※相関係数はxとyの共分散をそれぞれの標準偏差で割ったもの
参考:http://mcn-www.jwu.ac.jp/~yokamoto/ccc/stat/p22covcor/
この相関係数でデータ列1~4を計算すると下記の通り(エクセルではCORREL関数を使用)
テータ列 | 相関 | 相関係数 |
---|---|---|
データ列1 | 正の相関 | 1 |
データ列2 | 負の相関 | -1 |
データ列3 | 正の相関(数値が100倍) | 1 |
データ列4 | 負の相関(数値が100倍) | -1 |
⇒ 相関係数は単位によらない値になっている。また、最大値が1、最小値が-1となるよう規格化されている
数関係数=Rとした時の表
範囲 | いえること |
---|---|
1.0≧|R|≧0.7 | 高い相関がある |
0.7≧|R|≧0.5 | かなり高い相関がある |
0.5≧|R|≧0.4 | 中程度の相関がある |
0.4≧|R|≧0.3 | ある程度の相関がある |
0.3≧|R|≧0.2 | 弱い相関がある |
0.2≧|R|≧0.0 | ほとんど相関がない |
相関がない場合
下記のようなデータ列5は年月日と全く関係のない値をとっているため、相関係数は0に近くなる
年月日 | 20140101 | 20140102 | 20140103 | 20140104 | 20140105 | 20140106 | 20140107 | 20140108 | 20140109 | 20140110 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
データ列5 | 相関なし | 101 | 12 | 390 | -236 | 1 | 12 | 342 | -80 | 100 | 120 |
テータ列 | 相関 | 相関係数 |
---|---|---|
データ列5 | 相関なし | 0.018052993 |
相関が発見できない場合(シンプソンのパラドックス)
相関係数は変数同士の比例、反比例の関係のみ判断できるので、 下記のデータ列6のように半分に区間を区切れば相関があるデータについて見過ごしてしまう危険性がある
年月日 | 20140101 | 20140102 | 20140103 | 20140104 | 20140105 | 20140106 | 20140107 | 20140108 | 20140109 | 20140110 | |
---|---|---|---|---|---|---|---|---|---|---|---|
データ列6 | 相関が発見できない | 101 | 102 | 103 | 104 | 105 | 105 | 104 | 103 | 102 | 101 |
テータ列 | 相関 | 相関係数 |
---|---|---|
データ列6 | 相関があるのに発見できない | 0 |
集団を2つに分けた場合にある仮説が成立しても、集団全体では正反対の仮説が成立することがある ⇒ シンプソンのパラドックス 単純にデータを切り出してその相関係数のみを考えることは危険!
【付録】相関係数はcosθを意味する 下記のようなベクトルを定義すると相関係数はcosθと同じことがわかる
- x = (x1-m_x),(x2-m_x)・・・(xN-m_x)
- y = (y1-m_y),(y2-m_y)・・・(yN-m_y) (以下略)
pythonアルゴリズムなどメモ
内容
自分がよく使うpython3の書き方のまとめ 大体はpython2でも使えるはず(?)
Tips
文字列を受け取って整数にする
N = int(input())
文字列を受け取って整数の配列にする
A = list(map(int, input().split()))
またはリスト内包表記を使って
A = [int(a) for a in input().split()]
文字列を受け取って整数の変数に個別に代入
以下のようにリストの左辺をカンマ区切りで別々の変数にすればそれぞれに格納される
N, A, B = [int(x) for x in input().split()] print(N) print(A) print(B)
3 53 94 ← 入力値 3 53 94
a ~ bまでfor
0~3をfor
for x in range(4): print(x)
0 1 2 3
1から始めて4まで出したい場合は 「1, 4+1」を指定する
for x in range(1, 5): print(x)
1 2 3 4
listに対してfor
l=[1, 2, 3] for i in l: print(i)
結果
1 2 3
数字の各桁をすべて足す
考えた方法1.
以下ではlist(map(int, list(str(n)))
で、
いったん数字nを文字列にしてからそれをlistで一文字ずつ区切ったリストにして、それらをintに戻している
n=12345 summary = 0 for i in list(map(int, list(str(n)))): summary += i
summary : 15
考えた方法2.こちらのほうがわかりやすい
s = 0 while n > 0: s += n % 10 n //= 10
s: 15
ファイル読み込み
以下をopen.pyの名前で保存
import sys args = sys.argv #print(args[1]) path = args[1] with open(path) as f: print(f.read())
python open.py <対象ファイル>
go言語の並行処理
概要
「Go言語による並行処理」を読んだのでメモ ※この本に書いてないことも以下では取り上げている
goroutineとsync
goroutineの書き方
一番単純なgoroutine(これは期待した通り動かない)
package main import "fmt" func main() { hello := func() { fmt.Println("hello") } go hello() }
無名関数helloを定義して、「go 関数()」で実行する しかしこれは実行しても何も出力されない
$go run ex1.go $
その理由は、main goroutineとhello goroutineはそれぞれ別個に動くので、 mainが終わる前に子の処理のhelloを待たないため。
そこで、意図したとおりにするためには、syncで待ち合わせをすることが一番簡単な実現方法となる
syncで待ち合わせをする(期待通りの挙動になる)
syncパッケージを使って待ち合わせをする場合は以下のようにする
- wg.Add(待ちたい処理の数)を定義
- hello goroutineが終わる前に、このgoroutineの終了を伝えるwg.Doneをdeferで実行
- hello goroutineが終わるまで、wg.Wait()で待つ
$cat ex2.go package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(1) hello := func() { defer wg.Done() fmt.Println("hello") } go hello() wg.Wait() }
これを実行すると以下の通り出力される
$go run ex2.go hello $
syncで待ち合わせをする(helloを定義しない)
上の例はより簡単に、以下のようにも書ける
- かっこの最後に 「 }()」がついていることに注意
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("hello") }() wg.Wait() }
syncで待ち合わせをする(helloを通常の関数として定義する)
helloを通常の関数として定義する場合には以下のように、WaitGroupをポインタとして渡すことが必要になる
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(1) go hello(&wg) wg.Wait() } func hello(wg *sync.WaitGroup) { defer wg.Done() fmt.Println("hello") }
syncで複数の待ち合わせをする
waitNum := 3 と定義して、その回数分だけ待つ
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup waitNum := 3 wg.Add(waitNum) hello := func() { defer wg.Done() fmt.Println("hello") } for i := 0; i < waitNum; i++ { go hello() } wg.Wait() }
結果
hello hello hello
goroutineに引数を渡す
通常の関数と同様に、goroutineにも引数を渡せる
$cat ex3-2.go package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup waitNum := 3 wg.Add(waitNum) hello := func(i int) { defer wg.Done() fmt.Printf("hello %d\n", i) } for i := 0; i < waitNum; i++ { go hello(i) } wg.Wait() }
結果
$go run ex3-2.go hello 2 hello 0 hello 1
channel
最も簡単なchannel
一番簡単な例
- channelは複数のgoroutine間でデータを共有するためにあるので、ふつうこんな使い方はしない
package main import "fmt" func main() { ch := make(chan int, 1) // channelに5を送信 ch <- 5 // channelから整数値を受信 i := <-ch fmt.Println(i) }
実行結果
$go run chan.go 5
goroutineでのchannel
goroutineでchanelをやり取りする例
package main import "fmt" func main() { ch := make(chan int) go func() { // channelに5を送信 ch <- 5 }() // channelから5を受信 fmt.Println(<-ch) }
実行結果
5
複数のchannelを送受信することもできる
package main import "fmt" func main() { ch := make(chan int) n := 3 go func() { for i := 0; i < n; i++ { ch <- i } }() for i := 0; i < n; i++ { fmt.Println(<-ch) } }
実行結果
0 1 2
for rangeを使ったchannelの受信
for rangeを使ってchannelを受信する書き方もできる
ただし、for rangeを使って受信する場合、channelの終了がわからず永遠に待ち続けるので、goroutine側がこれ以上channelに送信しなくなった時点でchannelをcloseすることが必要
package main import "fmt" func main() { ch := make(chan int) n := 3 go func() { defer close(ch) // channelの終了を送信 for i := 0; i < n; i++ { ch <- i } }() for i := range ch { fmt.Println(i) } }
実行結果
0 1 2
deferを忘れると、待ち続けてdeadlockになる
deferをつけない場合の実行結果
0 1 2 fatal error: all goroutines are asleep - deadlock!
channelのcloseを確認する
channelからは、2番目の返り値としてchannelがclose済みかそうでないかを表すbool値が取得できる 慣習的にこの2番目の返り値はokとする
上の例で言えば、chから得られた値がi、chがclose済みかどうかがok(closeされていればfalse)となる。このokはとっても取らなくてもいい
i, ok := <-ch
参考 - A Tour of Go
上の例を、okがfalseとなるまでchの中身を出力し続けるようにした場合、以下のようになる
- ※上のfor rangeを使った書き方より冗長だしミスると無限ループになるからめったに使う必要ないと思う
package main import "fmt" func main() { ch := make(chan int) n := 3 go func() { defer close(ch) // channelの終了を送信 for i := 0; i < n; i++ { ch <- i } }() for { i, ok := <-ch if !ok { fmt.Println("ch closed") return // このreturnを忘れると無限ループになる } fmt.Println(i) } }
実行結果
0 1 2 ch closed
bufferなしchannel
bufferなしchannelとbufferつきchannelの挙動の違いについてシンプルにr例示している資料が意外とすぐに見つからなかった
まずbufferなしchannelの例
package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { defer close(ch) defer fmt.Println("Producer Done") for i := 0; i < 5; i++ { ch <- i fmt.Printf("Sending: %d\n", i) } }() for i := range ch { fmt.Printf("Received %v\n", i) time.Sleep(1 * time.Second) } }
実行結果
Sending: 0 Received 0 Received 1 Sending: 1 Received 2 Sending: 2 Received 3 Sending: 3 Received 4 Sending: 4 Producer Done
- 1秒おきにSendingとReceivedが交互に出力されている
channelが満杯のときはchannelへの書き込みは(読み込みされるまで)ブロックされるので、bufferなしchannelは毎回読み込みをした後で追加の書き込みをしている
bufferつきchannel
bufferつきchannelは一度に送信できる件数を指定できる
package main import ( "fmt" "time" ) func main() { ch := make(chan int, 4) go func() { defer close(ch) defer fmt.Println("Producer Done") for i := 0; i < 5; i++ { ch <- i fmt.Printf("Sending: %d\n", i) } }() for i := range ch { fmt.Printf("Received %v\n", i) time.Sleep(1 * time.Second) } }
実行結果
Sending: 0 Sending: 1 Sending: 2 Sending: 3 Sending: 4 Producer Done Received 0 Received 1 Received 2 Received 3 Received 4
- buffer4に指定した場合、channelに対して先に4つ書き込みが終了し、あとからReceivedされていることがわかる
- このように、bufferつきchannelは読み込みを待つことなく何件まで書き込めるかを指定できる
select
selectは複数のcaseに対して、それぞれの条件が等しく選択されるように、疑似乱数を使って配分している。
例えば、以下のようにc1, c2という2つのチャネルから合計1000回読み込むとする。
- c1からのチャネルから読み込んだらc1Countを増やす
- c2からのチャネルから読み込んだらc2Countを増やす
package main import ( "fmt" ) func main() { c1 := make(chan interface{}) close(c1) c2 := make(chan interface{}) close(c2) var c1Count, c2Count int for i := 1000; i >= 0; i-- { select { case <-c1: c1Count++ case <-c2: c2Count++ } } fmt.Printf("c1Count: %d, c2Count: %d.\n", c1Count, c2Count) }
このコードを何回も実行した結果を見てみると、c1, c2がほぼ同じ回数ずつカウントされていることがわかる
$go run select2.go c1Count: 499, c2Count: 502. $go run select2.go c1Count: 498, c2Count: 503. $go run select2.go c1Count: 520, c2Count: 481. $go run select2.go c1Count: 513, c2Count: 488. $go run select2.go c1Count: 505, c2Count: 496. $go run select2.go c1Count: 502, c2Count: 499. $go run select2.go c1Count: 509, c2Count: 492. $
for-selectでdefault処理
以下のように、チャネルを待っている間やりたい処理をdefaultに書くことでgoroutineの結果を待っている間他の処理ができる
for { select { case <-チャネル: <チャネルを受け取ったあとの処理> case <-チャネル2: <チャネル2を受け取ったあとの処理> default: } <チャネル1とチャネル2を受け取る前に行いたい処理> }
for で無限ループをしながらselectでdoneが来るのを待ち続ける例
goroutineが5秒後にdoneにcloseを送るので、処理が終わる
それまではWaitを繰り返す
package main import ( "fmt" "time" ) func main() { done := make(chan interface{}) go func() { defer close(done) fmt.Println("Goroutine start") time.Sleep(5 * time.Second) fmt.Println("Goroutine end. send done") }() i := 0 for { select { case <-done: fmt.Println("Receive done") return default: } i++ fmt.Printf("Wait %d\n", i) time.Sleep(1 * time.Second) } }
これを実行すると以下のようになる
$go run sample.go Wait 1 Goroutine start Wait 2 Wait 3 Wait 4 Wait 5 Goroutine end. send done Receive done
ちなみに、以下のようにdefautlとselectの終端の}の間に処理を入れても同様の結果になるが、この処理が往々にして長くなることがあってselectの終端がわかりにくくなるので、自分はあまり好んで使う人はいないようだ
default: <チャネル1とチャネル2を受け取る前に行いたい処理> }
timeout
Go by Exampleに書いてあるtimeoutの方法
Go by Example: Timeouts に書いてある方法
time.After(タイムアウトする時間) を設定してselect文で受け取ることで、時間がかかるgoroutineのタイムアウト処理ができる
以下の例では、 - 2秒かかるgoroutineの結果c1が1秒のtimeout制限に引っかかってtimeout 1が出力され、 - 同じく2秒かかるgoroutineの結果c2が3秒のtimeout制限以内に終わるので、result 2が出力される
package main import ( "fmt" "time" ) func main() { c1 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout 1") } c2 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c2 <- "result 2" }() select { case res := <-c2: fmt.Println(res) case <-time.After(3 * time.Second): fmt.Println("timeout 2") } }
実行結果
timeout 1 result 2
上のtimeout方法の問題点
上の方法には一つ問題があって、timeout 1と出てもgoroutineに切り出した処理は自動で止まらないということがある
ためしに以下のようにc1で受け取っていた部分を以下のようにtaskというgoroutineにして、task関数の中では0から999999まで数字を出力し続けるようにしてみる - また、最初と最後の時間差をとって、処理時間を計測してみる (結果は長くなるので注意)
package main import ( "fmt" "log" "time" ) func main() { start := time.Now() select { case res := <-task(): fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout 1") } log.Printf("Total time: %fs", time.Since(start).Seconds()) } func task() <-chan string { res := make(chan string) go func() { defer close(res) for i := 0; i < 1000000; i++ { fmt.Println(i) time.Sleep(1 * time.Nanosecond) } res <- "done!" }() return res }
実行結果
0 1 2 (省略) 475135 475136 475137 timeout 1 2019/09/29 06:11:19 Total time: 12.296678s
おかしなことが起きた! 実行結果はマシンの性能にもよると思うが、「timeout 1」まで出した後でtimeoutが出力されて、「 Total time: 12.296678s」となっている。
しかも恐ろしいことに2回目に実行すると、今度は「timeout 1」すら出ずに12秒かかっている。
489587 489588 489589 2019/09/29 06:13:37 Total time: 12.209647s
3回目もまた結果が変わる
489490 489491 489492 2019/09/29 06:14:39 Total time: 12.120974s
timeoutとなっても、すでに起動してfmt.Println(i) を出力し続けるgoroutineは勝手に暴走して、それが終わるタイミングは全然予測できていない
これをtimeout時間の通り1秒で終了させたい場合は以下のようにcontextやdoneチャネルを使うといい。
timeoutでcontext cancelを行う場合
上のコードをcontextを用いて書き直したものが以下となる。
timeout時刻になるとcontextのcancel処理が飛んで、1秒で処理が終わっていることがわかる
package main import ( "context" "fmt" "log" "time" ) func main() { start := time.Now() ctx, cancel := context.WithCancel(context.Background()) defer cancel() res := task(ctx) select { case <-res: fmt.Println(res) case <-time.After(1 * time.Second): log.Println("timeout after 1s") cancel() log.Println(<-res) } log.Printf("Total time: %fs", time.Since(start).Seconds()) } func task(ctx context.Context) <-chan string { res := make(chan string) go func() { defer close(res) for i := 0; i < 1000000; i++ { select { case <-ctx.Done(): log.Printf("context cancelled. count: %d", i) res <- fmt.Sprintf("error: %s", ctx.Err()) return default: } fmt.Println(i) time.Sleep(1 * time.Nanosecond) } res <- "done!" }() return res }
実行結果
(省略) 38503 38504 38505 38506 38507 2019/09/29 06:30:35 timeout after 1s 2019/09/29 06:30:35 context cancelled. count: 37841 2019/09/29 06:30:35 error: context canceled 2019/09/29 06:30:35 Total time: 1.000472s
contextの使い方は公式は以下。ほかにもネット上にたくさんあるので、以下で簡単な説明だけ記載しておく。 Go Concurrency Patterns: Context - The Go Blog
簡単に説明
- コードの最初でcancel 機能付きのcontextを宣言する
- 「defer cancel()」によって、プログラム終了時に確実にすべてのcontextがキャンセルされるようにする
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
- task関数にこのcontextを渡して(第一引数として渡すのがセオリー)、for 文の1ループごとに、「ctx.Done()」が来ていないかチェックしている
- このプログラムでは 「ctx.Done()」を受け取ったらlogに「context cancelled. count」とその時のループ番号を出力している
- さらにresにエラー原因として「 ctx.Err()」を返してreturnでgoroutineを終える
- このreturnがないとDoneを受け取ってもgoroutineが終わらないので必要
for i := 0; i < 1000000; i++ { select { case <-ctx.Done(): log.Printf("context cancelled. count: %d", i) res <- fmt.Sprintf("error: %s", ctx.Err()) return default: } fmt.Println(i) time.Sleep(1 * time.Nanosecond) }
- 呼び出し側のmain内では、「res := task(ctx)」としてtaskの戻り値(channel)をいったんres変数に定義
- time.Afterの制限時間内に処理が終われば、「case <-res」に入る
- time.Afterの制限時間を過ぎたら、「timeout after 1s」をlogに表示させて、contextのcancel関数を呼び出す(重要)
- 「log.Println(<-res)」には、task関数内で詰めたctx.Err()の内容が出力される「error: context canceled」
- 「<-res」とすることで、cancelをした後のtask関数が返すエラーをきちんと受け取ってから終わるように待ち合わせをしている
res := task(ctx) select { case <-res: fmt.Println(res) case <-time.After(1 * time.Second): log.Println("timeout after 1s") cancel() log.Println(<-res) }
contextを使うことで、goroutineを安全に終了させることができる
pipeline、fan-in、fan-out
go言語のpipeline、fan-in、fan-out - ludwig125のブログ
複数のgoroutineのエラーハンドリングをする
同時並列数を制御する
シグナル処理
GoogleAppEngineでgoroutineを使う
GoogleAppEngineでgoroutineを使った例 ludwig125.hatenablog.com