2022/09/11

[android] ブート後に intent を受け取る

AlarmManager の章にデバイスの再起動後にアラームを開始したい場合について書かれている。

デバイスの再起動時にアラームを開始する
https://developer.android.com/training/scheduling/alarms#boot

デバイスの再起動をアプリは検知することは通常できないので、 intent を受け取るようにするようにしておくのだ。
つまり、アラームとは関係が無いのだが、アラームを設定したアプリを起動していたのに、いつの間にか再起動されてアラームが動作しなかったということがあるからここで説明しているのだろう。

 

書いてあるとおりに実装した。
一度アプリを起動しておけば、再起動すると notification が表示される。

https://github.com/hirokuma/AndroidAlarmTest/compare/20488ad5377f82a2d529b2654dbb6262a50985e6..4f69554d7f177e051d3f57b33e59c7701986507d

(関係ないけど、Windows で作っていたからか AndroidManifest.xml だけ CR/LF になっていた。 Android Studio の設定だけでは足りないのか?)

 

また、このブート時に受け取るような設定は永続的なものではないようだ。
このアプリを「アプリ情報」から強制停止してからデバイスを再起動すると notification が出てこなかったからだ。一度設定しておけば良いものでも無く、アプリがブート前に立ち上がって設定しておくだけで良いものでも無く、アプリがまったく死んでいない状態にしておかなければならないようだ。

 

では、ブート時の notification が表示されただけで setComponentEnabledSettings() を実行していない状態で再起動したらどうなるだろうか?
これはブート時に notification が表示された。 AlarmTest アプリは Activity の onCreate() で setComponentEnabledSettings() を呼び出しているのでおそらく呼ばれていないだろう。3分間隔の notification も出ないのでそのはずだ。
プロセスとしては立ち上がっているけれども、 onCreate() から始まる経路は使われていないということかな。

[android] アラームの復習

さて、少し期間が空いてしまったため Android の Alarm についてかなり忘れてしまった。
ほんの少し・・・それですら命取りになるのが加齢よ。。。


前回は AlarmManager を使って通知を出すようなしくみを考えていたが、Android というシステムからするとデバイスの起動状態を管理するという分野になるそうだ。宇宙の成り立ちみたいな広い話ですな。

デバイスの起動状態を管理する  |  Android デベロッパー  |  Android Developers
https://developer.android.com/training/scheduling

定期的に intent を発行する、というだけでなく、もっとデバイスの中で限られたリソースをうまいこと使いたいとなると電源やネットワークの状態によってアプリの動作を制限するというのも理解できる。
アプリを作っている人たちはみんな「私のアプリを使って!」と思っているので、1つしかデバイスがないのであれば優先順位を付けるしかないのだ。

そう考えると、アクションゲーム?のように前面にアプリが起動していないと意味が無いようなアプリの場合はあまり考えなくて良いだろう。バックグラウンドで動作できるアプリが複数あるのに対して、フォアグラウンドで動作できるアプリは1つしかないからだ。画面を分割することもできたと思うが、それでも入力のフォーカスを持っているのは 1アプリだけだろう。

そう考えると、定期的にアプリを起動したいのはフォアグラウンドにはいないアプリが主になるはずだ。
Android 10 からはバックグラウンドアプリから Activity を表に出すことに制限がかかった。調べ切れていないが OS のバージョンが上がってできなくなったことが増えていてもおかしくない。

こういった実装やバージョンの事情がいろいろと複雑にしているのだと思う。
developer のドキュメントはよくできていると思うのだが、Google で検索しても本家の情報が出てこないのであれば実装する人が混乱するのも仕方ないと思う。


AlarmManager 以外にも WorkManager がある。
最近は WorkManager を使うよう勧めているように思うが、得意とする分野が少し異なる。

他の API との関係性
https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja#other-APIs

Dozeモードを解除できるのが AlarmManager らしい。

プラットフォームの電源管理  |  Android オープンソース プロジェクト  |  Android Open Source Project
https://source.android.com/docs/core/power/platform_mgmt

Doze とアプリ スタンバイ用に最適化する  |  Android デベロッパー  |  Android Developers
https://developer.android.com/training/monitoring-device-state/doze-standby?hl=ja

検索していると Dozeモードが搭載された Android 6.0 のときの話が良く出てくるが、Android 7.0 でさらに拡張されたのか。
上のリンクは AOSP なので OS そのものの話になっている。今回はアプリを作る方の立場なので下のリンクの方がわかりやすい。

  • 電源と接続していない
  • 静止状態にしている(センサーで見ているのだろう)
  • 画面オフのまま一定時間過ぎる

これで Dozeモードになるそうだ。それ以降は「メンテナンス枠」ということでときどき Dozeモードを解除して、また戻る。ということは、アプリが Dozeモードになるのではなく本体が Dozeモードになるということか。
時間経過のグラフを見るとメンテナンス枠になるタイミングがどんどん延びているので、放置状態が長くなるほど働かなくなるようにすることで電池の減りをなくそうということだろう。

AlarmManager はこの Dozeモードを無視して動くことができるというのが強みなのだろう。ただそこからすぐに Dozeモードに再突入するかもしれんが、そこはアプリ次第なのか。その API も追加されたもので、なおかつ 9分に 2回以上はアラームが発生しない(ということは 9分に 1回までということか)。

 

もう1つの「アプリスタンバイ」はアプリ単位。いくつか条件が整わないといけないので、アプリスタンバイにならないように作るのは難しくないかもしれない。ただ、アプリスタンバイでなくてもリソースを使いすぎだと判断されたら OS から止められるしくみはあったと思うので、結局は実験して確認するのがよいのか? でも対策していない要因でアプリが止められたら結局調べるしかないだろうし、知識を補充するのが先か・・・。

 

アプリが途中で動かなくなることについての YES/NO シートとか、logcat にこのメッセージが出たらこの状態、みたいなものがあるとよいのだが。

2022/08/21

[android] AlarmManager でアプリを起こしたい

やりたいこと

AlarmManager を使って、アプリが起動していないように見える状況でもアプリを起動させたい。

 

結果

アプリがタスク一覧に載っていなくてもアラームから起動することはできるが、強制停止状態では無理。

本体の OS が API 29 以降では Alarm のようなバックグラウンドからアプリを起動させることに制限がかかっているので、一旦 Notification などでユーザに通知だけ行い、ユーザ操作によってアプリを起動させることになるだろう。

バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts

 


前回、AlarmManager を使って反復アラームを設定し、AndroidManifest.xml に設定した <receiver> の class がブロードキャストされた intent を受け取ってログ出力するところまで確認した。

私がやりたいのは、アプリが立ち上がっていない(と思われる)状況でも AlarmManager での発火をトリガにしてアプリを起動したい、というものだ。
プリインストールのような system 属性のアプリであれば好き勝手できそうだし、Google Play からインストールしたアプリであればあまり勝手なことをされたいとは思わないだろう。

なので今回は Google Play からインストールするような system 属性を持たないアプリで期待したことができるのか、できないならどこまでやれるのかを調べたい。


前回は 15 分にしたが、あれは WorkManager の最短が 15分だったはず、くらいで決めたものだ。
AlarmManager なら 3 分でもできた。

2022-08-20 20:48:32.650/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.MainActivity: setAlarm
2022-08-20 20:53:47.674/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 20:56:28.482/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 20:59:47.663/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 21:02:47.665/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!

まあ、間隔が短くできるということはリソースを食いやすいということでもあるだろうから、アプリの審査としては厳しくなってしまうのかも? どういう基準で見ているのかは知らんのだけど、面倒なアプリの方が審査に時間がかかりそうな気がするじゃあないか。そういう基準だったら、AlarmManager ではなく WorkManager の方が緩いと思うのだ。

ともかく、3分くらいだったらテストしやすいのでそれで見ていこう。


まず「アプリが終了している」という状態について確認が必要だ。

「アプリが動いている」という状態が一番確実なのは、そのアプリがフォアグラウンドになっていて Activity が表示されているときだ。これは間違いなく動いているだろう。

ならばActivity が表に出ていないときがどうなのか、ということになる。
調べていて出てきたのが「 Doze」と「アプリスタンバイ」というものだ。

Doze とアプリ スタンバイ用に最適化する  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=ja

書いてあるとおりなら、

【Doze】
デバイスが長い間使用されていない場合にアプリのバックグラウンド CPU とネットワーク アクティビティを保留する

【アプリスタンバイ】
ユーザーがしばらくの間操作していないアプリのバックグラウンド ネットワーク アクティビティを保留

アプリスタンバイはネットワークだけしか保留しないのか? 文章だけ読むと、先に「アプリスタンバイ」になり、それでも長い間使用されないようであれば「Doze」になるようにも見えたのだが、そうではないのだろう。

アプリで Doze をテストする
https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=ja#testing_doze

「dumpsys deviceidle force-idle」を実行してそのアプリの動作が変わるなら、Dozeモードで動作が変わるアプリということだろう。

アプリスタンバイの場合はどれをスタンバイにするか指定しているので別物かなぁ、というところだ。
ネットで検索したらもっとよい情報が出てくるだろうが、今回はそこまで考えたくないので無視だ。


私が「アプリが「終了している」というのは、アプリスタンバイに近いと思う。
そして Android のタスク一覧から該当するアプリを上スワイプしたときも「終了している」だろうから起動してほしい。
できればアプリ情報画面で強制停止(force stop)させたあとからも起動してほしいのだが、これは難しいと思う。

あとは、アプリの中から「終了」を行った場合だ。
これが何をしているのかがよくわからんのだが、Activity が 1枚しかないときに「戻る」を選ぶと「終了しますか?」と尋ねてくるアプリがあったように思う。

ユーザが「アプリを終了したい」と思うことと、アプリとして終了するという実装は、一致しないとはいわないまでも、一致しづらいことが多いと思う。例えばゲームのハイスコアをサーバで管理してみんなで共有するようなアプリがあったとして、ユーザが「ハイスコアを更新したから終了させよう」と思ったタイミングと、アプリが「スコアのデータをサーバにアップしよう」と実装されているタイミングは必ずしも一致しないだろう。ユーザは終わった気分でいても、他のユーザがアクセス中だったりしてアップロードを保留しているかもしれない。サーバにアクセスが集中しないようにわざと遅らせるようにしているかもしれない。

そう考えると、モバイルのアプリは「終了」が即時行われるとは考えづらい。たぶん即時終了させるのがアプリ情報画面から「強制停止」を行う操作だろう。
一覧から上スワイプするのは強制停止ではないと思う。Doze にさせるか、スタンバイにしているかのどちらかではないか。調べていないけどスタンバイじゃないかなぁと思っている(個人の感想です)。

端末を USB接続しているとスリープしない状態になったりするし、その設定をオフにしても実は何かあるんじゃないの?とか、15分を 3分にしたことで挙動が違ったりするんじゃないの?とかいろいろ考えてしまうのだが、まあそのときはそのときだ。

Android Studio の Logcat タブでアプリが (DEAD) になるようであればアラームを受け付けることはさすがにできないだろう(と思ったが、タスク一覧から上スワイプすると DEAD になったので違うのか)。


adb shell でログインすると、 dumpsys というコマンドが使用できる。

dumpsys  |  Android デベロッパー  |  Android Developers
https://developer.android.com/studio/command-line/dumpsys?hl=ja

dumpsys コマンドだけたたくとすべてのシステムサービス情報を出力しようとするので、対象とするサービスを指定するのがよろしい。今回であれば alarm だ。
指定してもたくさん出力されるのだが、試作している alarmtest2 で検索するとこういう情報が見つかった。

    ELAPSED_WAKEUP #5: Alarm{bef203a type 2 origWhen 27853758 whenElapsed 27853758 com.hiro99ma.alarmtest2}
      tag=*walarm*:com.hiro99ma.alarmtest2/.NotificationReceiver
      type=ELAPSED_WAKEUP origWhen=+2m54s204ms window=+2m15s0ms repeatInterval=180000 count=0 flags=0x0
      policyWhenElapsed: requester=+2m54s204ms app_standby=-5s796ms device_idle=-- battery_saver=-5s796ms
      whenElapsed=+2m54s204ms maxWhenElapsed=+5m9s204ms
      operation=PendingIntent{77e94eb: PendingIntentRecord{b073248 com.hiro99ma.alarmtest2 broadcastIntent}}

ここでは ELASPSED_WAKEUP と経過時間で発火するように指定している。おそらく 3行目の type=ELAPSED_WAKEUP の次に出ている origWhen が発火するまでの残り時間だと思う。時間をおいて取得すると減っていたからだ。

これは、alarmtest2 アプリを起動すると出てきたし、タスク一覧で上スワイプして消したときも残っている。たぶん発火すると「App Alarm history」というところにも出てきたし、Alarm stats にも出てくるようになった。しかしアプリ情報から強制停止させると出てこなくなった。おそらくだが、アプリ情報から強制停止ボタンがタップできるような状態であればアラームは動くんじゃないかな。強制停止でアラームが消えるのは、登録元が消えたからか通知先が消えたからか。まあどっちでもよいが、確認するためには通知先を別アプリにしないとわからんな。

 

ん?
ということは、自分で自分にアラームを仕掛けておいて自分を起動する、というのは無理というか、既に起動しているから表に出してあげるだけで目的を達成できるんじゃなかろうか。

override fun onReceive(context: Context?, intent: Intent?) {
    val actionIntent = Intent(context?.applicationContext, MainActivity::class.java).apply {
        action = Intent.ACTION_MAIN
        addCategory(Intent.CATEGORY_LAUNCHER)
        flags = Intent.FLAG_ACTIVITY_NEW_TASK
    }
    context?.startActivity(actionIntent)
}

こんな感じだろうと思ったのだが、例外は発生しないもののアプリが表に出てくることはなかった。 Notification をタップしてアプリを表に出すときはこれでよかったのだが、状況が違うのか。

? I/ActivityTaskManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.hiro99ma.alarmtest2/.MainActivity} from uid 10502
? W/ActivityTaskManager: Background activity start [callingPackage: com.hiro99ma.alarmtest2; ...(中略)...; inVisibleTask: false]
? D/ActivityTaskManager: getPreferredLaunchDisplay userset_displayid false current displayId -1
? D/ActivityTaskManager: getPreferredLaunchDisplay userset_displayid false current displayId -1
? E/ActivityTaskManager: Abort background activity starts from 10502

BroadcastReceiver だからダメなのかと思い、AlarmManager から setRepeating() するときの PendingIntent で同じ actionIntent を呼ぶようにして設定したのだが、それでも同じだった。

 

「ダメだった」という状況について説明していなかった。
手順はこうだ。

  1. alarmtest2 を起動する
  2. タスク一覧を出して alarmstart2 を上スワイプする
  3. アラームが発火するまで待つ(本体は 2分でスリープになる)

3分以上待っているとアラームが発火して Logcat に何か出てくるので、そのときにスマホをスリープ解除して画面を出し、alarmtest2 アプリが表に出ていれば成功、出ていなければダメ、という判定の仕方をしている。


logcat の 「Abort background activity start」がエラー出力なので、そこら辺か。

バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts

あー、ユーザが操作しているのにいきなり表に出てくる可能性があるから制限がかかるのか。
わかる。私だったら怒るね。

ただ、私は alarmtest2 を API 26 以上ということで作っている。であれば API 29 からというこの制限から外れるんじゃないのか?

image

雰囲気からすると、ここに書いている APIレベルは本体の OS のものということだろう。

 

ともかく、Android としては「時間に依存する通知」を使ってほしいそうだ。リンク先や "ヘッドアップ通知" という言葉も出てくるので、いわゆる Notification を使えってことなのだと思う。 Notification → 通知をタップしてアプリを出す、がよいのかな。


というわけで、Notification を追加した alarmtest2 の最新版がこちら。

https://github.com/hirokuma/AndroidAlarmTest/commit/7aea025ed07fd53d4774a526f3328984ff4994a7

前々回の Notification 記事から持ってきたのだが、Notification を表示するタイミングとコンテキストが違うので多少変更されている。

アプリが起動して行うのは Notificationチャネルの作成のみ。
BroadcastReceiver の onReceive() で Notificationの作成と表示を行っている。記事を書いたときは with という Kotlin のスコープ関数なるもので notify() を呼んでいたが、BroadcastReceiver からは使えなかったので普通に書いた。

これを実行すると、数分おきに Notification の通知が行われる。スリープしていても Notification が出てくるので非常にうっとうしい。 API が間違っていなくても用法を間違うと嫌な動作をするという例と思ってもらおう。

2022/08/15

[android] AlarmManager でアラームする

Android の基礎学習。
AlarmManager でアプリが起動していなくても何かしたい。 notification が出したいのだが、まずは Log が出るだけでも良いだろう。

Schedule alarms  |  Android Developers
https://developer.android.com/training/scheduling/alarms

 

いろいろ書いてあるのだが、こういう系統はだいたいイベントが発火するときに PendingIntent で何かするものだろう。
PendingIntent は作るとき?に Intent を与えられて、PendingIntent の殻をかぶった Intent みたいなものだろうと思ってる。

作るといっても、PendingIntent.getActivity() だったり PendingIntent.getBroadcast() だったりといくつか作り方がある。前回はnotification の学習だったが、そこでは notification をタップしたときの挙動用に PendingIntent.getActivity() を使っていた。
タップすると自アプリが表に出てくる=Activity に対して通知するからだと思っている。

では、notification を出したい場合は誰が受け取るのだろう?
今回は Activity を表に出したくないので getActivity() ではないだろう。
受け取るために Service がいるのだったら getService() かもしれない。でもわざわざ service を作りたくないとも思う。
PendingIntent で他に PendingIntent を返す static メソッドはなさそうだ(ForegroundService みたいなの以外は)。

PendingIntent.getBroadcast() で作ってやって、それを受け取るところで処理できるなら一番楽そうな気がする。


ブロードキャストを受け取る方法について。

Broadcasts overview  |  Android Developers
https://developer.android.com/guide/components/broadcasts

ローカルだったら LocalBroadcastManager があるようなことを書いてあるが、class 自体が deprecated 扱いだったから今から使わなくてもよいだろう。他の observable pattern で置き換えられるようなことは書いてあったが、これを使えとは書かれていないのでチュートリアルの通りにやれば良いのだろう。

英語版を見ているが、受け取り方は「Manifest-declared receivers」と「Context-registered receivers」の2種類のようだ。
AndroidManifest.xml にあらかじめ書いておくタイプか、ソフトウェア上で必要なときだけ受け付けるタイプか。
その2択であれば、アラームについては AndroidManifest に書くタイプだろう。アプリが立ち上がっていないときに受け取りたいのだから。でも、もしかしたら一度登録しておけばよいというタイプかもしれんので実験はしてよかろう。


これに従ってアラームを作っていく。
「反復アラーム」というのは、ワンショットではないアラームのことだろう。

AlarmManager  |  Android Developers
https://developer.android.com/reference/android/app/AlarmManager

 

アラームの大きな設定は、タイプと精度のようだ。

タイプは、「何分後」みたいな経過時間タイプ(ELAPSED_REALTIME 系)と「何時何分」みたいな時間指定タイプ(RTC 系)があるようだ。
ELAPSED は経過時間といいつつ、デバイスが起動してからの経過時間という基準になっているそうだ。実装は、例では「SystemClock.elapsedRealtime()」からの相対値を指定していた。

精度は、曖昧だけど電池に優しいか、時間きっちりかを選択するもののようだ。メソッドとしてはおそらく第1引数に type を取るものがそれだと思う。

image

チュートリアルでは setInexactRepeating() と setRepeating() が紹介されている。 exact の頭に in- が付くことで「正確ではない」ということなのだろう。反復アラームだからその 2つだけ紹介されているのだろう。

アラームというものが Androidのバージョンで変化しているもののようで、APIの説明にいろいろ書いてある。

  • API 31以降で Intent.EXTRA_ALARM_COUNT を使うなら、 PendingIntent の flags に FLAG_MUTABLE がいる
  • API 19 の時点で反復アラームはすべて inexact になっている。繰り返しで正確にしたいなら、ワンショットのアラームを繰り返すことになる。

うーん、よくわからん。
API 20 以降なら setRepeating() だけでexact にできるということかな?

 

とりあえず動いているようなので記録を残しておいた。
Kotlin + Android 8以降 + Empty Activyty をベースにしている。

https://github.com/hirokuma/AndroidAlarmTest/tree/260da35b09a5012999a96de6d1e62903e7d525c2

 

Logcat のログを必要そうなところだけ抜粋。

2022-08-15 17:33:25.893/com.hiro99ma.alarmtest2 D/MainActivity: setAlarm
2022-08-15 17:52:25.390/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:14:40.987/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:23:33.683/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:35:38.638/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!

15分にしたのだが、15分より短いのもある。
WAKEUP の方にしたから放置した状態で確認したかったのだけど、ときどき触ったりしたので実際はどうだか分からん。

何が苦労したかって、 Activity::onCreate() でアラームを設定すると動作しなかったことだ。
何か方法はあるのかもしれないが、setAlarm() を onCreate() で呼び出すとダメだったのだ。動くサンプルがあったのだけどアラームの設定に違いがなさそうだったのでひどく悩んだのだ。そのサンプルはボタンをタップしてアラームを設定するようになっていたので、ためしに onResume() に持っていったら動作したのだった。

まだいろいろわかっていないが、今回はアラーム動いた記念なのでよしとしよう。

2022/08/11

[android] Notificationを出す

基礎の勉強。

Android Studio でプロジェクトを作る。
対象は Android 8.0 以降。Kotlin。Empty Activity。

Create a Notification  |  Android Developers
https://developer.android.com/training/notify-user/build-notification

日本語版もあるのだけど、若干更新されていない。まあ、実行時エラーで気づける程度のものだが。
ともかく、基礎的な notification を作成する。

Create a basic notification
https://developer.android.com/training/notify-user/build-notification#SimpleNotification

手順

  1. notification で表示する中身を作る。
    NotificationCompat.Builder() の後ろに設定するメソッドを連結して呼んでいくだけ。
    Builder() の第2引数である CHANNEL_ID は String型。これは Android 8.0 以降で追加されたものらしく、それより前は無視するとのこと。
    これは、この次に作る notification channel を指定するものだ。
    なお、日本語版で~と書いたのは、ここの PendingIntent.getActivity() の最後の引数が 0 になっていたからだ。今は何か指定しないといけないそうである。フラグの違いは検索すると出てくるので探してほしい。
  2. notification channel を作る。
    そういえば、Android のどこからかアプリの上にドットを出したりするようになったなぁ。
  3. notify() で表示。

 

こんな感じだ。

    private fun createNotificationChannel() {
        val name = "のてぃふぃけーしょんちゃんねる"
        val descriptionText = "のてぃふぃけーしょんちゃんねるのですくりぷしょん"
        val importance = NotificationManager.IMPORTANCE_DEFAULT
        val channel = NotificationChannel(NOTIFICATION_ID, name, importance).apply {
            description = descriptionText
        }
        val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }

    private fun createNotification(): NotificationCompat.Builder {
        // 自アプリを起こす
        val wakeupMeIntent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val wakeupMePendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, wakeupMeIntent, FLAG_IMMUTABLE)

        return NotificationCompat.Builder(this, NOTIFICATION_ID)
            .setSmallIcon(com.google.android.material.R.drawable.ic_clock_black_24dp)
            .setContentTitle("のてぃふぃけーしょんたいとる")
            .setContentText("のてぃふぃけーしょんてきすと")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(wakeupMePendingIntent)
            .setAutoCancel(true)
    }

これらを、たとえば onCreate() などで呼び出す。
notify() の第1引数は Int 型で、ユニークならよいらしい。あとから中身を更新したり削除したりするときに識別するものだそうだ。

        createNotificationChannel()
        val builder = createNotification()
        with(NotificationManagerCompat.from(this)) {
            notify(NOTIFY_TEST_ID, builder.build())
        }

まあ、アプリの起動と同時に notification を出すことは少ないかもしれんが、うまいことやってほしい。


このアプリを起動すると notification が出てくる。
左側の時計アイコンは、適当に Android Studio の自動補完で候補に出てきたものだから、ちゃんとしたものかどうか知らん。

image

notification の中身は出てきたから良いとして、notification channel で設定したものはどこで出てくるのか?

見つけたのは、アプリごとの「アプリ情報」画面から「通知」をタップして表示される画面だった。

image

これだと notification channel の name だけなのだが、さらに表示されている項目をタップすると description の方も出てきた。

image

 

日本語にしておくと、そしてひらがなにしておくと他の文字列やデバッグ情報と重ならないからありがたいね。

 

ちなみにアプリ名が「AlarmTest」なのは、AlarmManager の実験をしたかったからだ。
時間になったら何かしたい→ notification を出すか、という流れだ。

2022/08/07

[android] ネットワーク通信のライブラリがいろいろある

Linux でネットワーク通信を行うならば、クライアント側だと

  1. socket つくる
  2. サーバ側と connect する
  3. あとはよしなにー

という感じだったと思う。

ただまあ、socket のような素に近いところを直接アプリで触らせたくないと思う。ラップするならそこだろう。
なので「Android では通信するのに volley というライブラリを使う」と書いてあったので、うまいことラップされているのかな、くらいに思っていた。

しかしそこにあったのは、socket をラップしたライブラリとかではなく、なんだかいろいろ隠したライブラリだった。
最終的には「便利」なのかもしれないが、ちょっと通信したいのに使うには難しそうだった。
まあ、使わないことにはわからないので、基本的なところはやってみよう。

関係ない話だが、volley を検索して見ていたページが google.cn の方だった。偽サイトに引っかかったのか?とあせったが google.cn も google の証明書だった。よかった。


まず、最初に書かれているこちら。

Volley は、Android アプリのネットワーク操作を容易化、高速化する HTTP ライブラリです。

socket 通信ではなく、HTTP系専用らしい。
HTTP だったら、というわけでもないが、ネットワークに関しては普通の Java でもライブラリがあったはずだ。 volley だって Android の最初からあったわけではなく、どちらかといえばつい最近だったと思う。それまで HTTP で通信できなかったわけではないので、 volley でなくてもよいはずなのだ。

しかし、Android Developer の接続に関するページを見ると、2項目目にはもう volley が出てきているので、なんらかの理由で推しているのだろう。

接続  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/topics/connectivity?hl=ja

ただねぇ、ちょっと内側に行くと Retrofit というライブラリを使う説明をしてあった。なんなんだ。。。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting?hl=ja#choose-http-client

どうやらライブラリについていろいろと経緯があったらしい。

非同期通信をVolleyとOkHttpとRetrofitそれぞれで書いた場合の超簡易サンプル - Qiita
https://qiita.com/yamacraft/items/2bbc8867ef0a8ffb0945

ああ、okhttp も聞いたことがある。 gRPC を動かすときも okhttp のライブラリがなくてエラーになったし。
しかしすべてを把握したいわけでもない初心者としては、廃れなさそうなライブラリを 1つだけ覚えたいのだ。
volley はgoogle が開発しているから無難なのかな?
しかし OkHttp は HTTP/2 のことが書いてあったり、Retrofit と同じ Square が開発しているというのもあったりして魅力を感じてしまう

Overview - OkHttp
https://square.github.io/okhttp/

Retrofit は OkHttp を使っているので、HTTP 向けにしたラッパみたいなものなのかな? いや、 HTTP client だから違うな。 GitHub の About では、

OkHttp
Square’s meticulous HTTP client for the JVM, Android, and GraalVM

Retrofit
A type-safe HTTP client for Android and the JVM

となっているから、Retrofit はタイプセーフなところが売りなのだろう。

タイプセーフってなんじゃ?と思ったが、文字列ではなく Java や Kotlin の型で扱えることを指すのだろう。 volley はサンプルを見た感じでは文字列が返ってくるようだ。ただ、レスポンスが文字列、画像、JSON の場合は通常の Request で処理してくれるらしいし、特殊なことがしたければカスタマイズもできるようだ。画像って base64 なのかな?

どうせ私が使うなら JSON でやりとりするだろうし、それだったらどっちでもよいのかな。となると決め手がない。。。
なにか明確な違いがあればよかったのだが。
なので、どっちも同じことができるなら、実装してみて気に入った方を使うようにすれば良いと考えることにした。


ライブラリを試す前にちょっと脇道に逸れる。

新規でプロジェクトを作ろうとしたのだが、こっちもいろいろある。。。
どれが「普通」なのだろう?

image

まず、(Material3) の付いているのと付いていないのがある。
今のマテリアルデザインは 3 らしいので、付いていないのは 2 ってことか? 4 のページはなかったのでそうだと思う。

あとは、ただの Activity か Compose Activity かだ。
たぶん Jetpack Compose のことだろうと思うが、私に分かるんだろうか?

とりあえず Net1 という名前で作ってみた。
最初の 3行くらいは Kotlin の class の書き方なので分かるが、setContent 以降が初めて見る形だ。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    Net1Theme {
        Greeting("Android")
    }
}

Kotlin の文法をある程度把握する方が先かもしれんな・・・。


まず、Project Settings の Dependencies で "com.android.volley:volley" を search して追加。
今の時点では 1.2.1 が見つかった。

スニペットを貼り付けてみた。
Alt + Enter で解決させていったのだが、そもそもスニペットを Compose Activity の構造としてどこに置いたらいいのかがよくわからない。
@Compose の関数内だとコンテキストがダメそうだったので MainActivity の private 関数として追加し、 onCreate() から呼ぶようにした。画面に出すのはわからないから Log にした。

いつものように Android Studio の再生ボタンを押したのだが、画面は出るものの関数が呼ばれている気配がない。ブレークポイントも止まらない。
どうやら Compose Activity でプロジェクトを作った際には Configuration が DefaultPreview になっているようで、それだと MainActivity の onCreate() すら呼ばれないようだった。

Configuration を app に切り替えると、ようやく動きがあった。

E/Volley: [37] NetworkDispatcher.processRequest: Unhandled exception java.lang.SecurityException: Permission denied (missing INTERNET permission?)
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)

ああ、AndroidManifest.xml を追加していなかった。

<uses-permission android:name="android.permission.INTERNET" />

これだけで動いた。
Koglin で変更したのは MainActivity だけで済んだ。青文字がそれである。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        network()
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }

    private fun network() {
        // Instantiate the cache
        val cache = DiskBasedCache(cacheDir, 1024 * 1024) // 1MB cap

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.google.com"

        // Formulate the request and handle the response.
        val stringRequest = StringRequest(
            Request.Method.GET, url,
            { response ->
                // Do something with the response
                Log.d("network", response)
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }
}

呼び出しが一回だけだったら Volley.newRequestQueue() でよいそうだ。キャッシュを確保するのにもコストがかかるので、そこら辺は自分のアプリと相談することになるだろう。

JSON での API 実行のようにデータが小さいことがわかっている場合は、むしろキャッシュ無しという選択もあるんじゃなかろうか?
ああ、ちゃんと NoCache.java という

https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json

cache のところを書き換えても動いたので、それでよいのかな?
ついでに JSON のデータを取ってくるようにした。返ってくるデータが JSON の Array なのか Object なのかによって関数名が変わるのは注意しておきたいところだ。

    private fun network() {
        // Instantiate the cache
        val cache = NoCache()

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json"

        // Formulate the request and handle the response.
        val stringRequest = JsonArrayRequest(
            Request.Method.GET, url, null,
            { response ->
                // Do something with the response
                Log.d("network", response.toString())
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }

最初は String で受け取っていたのだけど、日本語の部分が文字化けするのだった。 JSON で取ると化けなかったのだけど何でだろう? Windows だからか?


volley の欠片くらいは味わえたと思うので、次は Retrofit だ。

https://square.github.io/retrofit/

こういう感じかな?

  1. Retrofilt.Builder() でアクセスする根っこを指定する
  2. interface を作って、 GET したりする下側を準備する
  3. Call で呼び出す

ここだと、根っこの部分が「https://api.github.com/」で、GET するのは「https://api.github.com/users/{ユーザ名}/repos」。
ユーザ名は "octocat" を指定している。

https://api.github.com/users/octocat/repos

 

まず Project Structure > Dependencies からライブラリを追加。
これを書いている時点では 2.9.0 だった。

com.squareup.retrofit2:retrofit

INTERNET の permission は既に追加してあるので、あとはまねしていけばよいだけ。

だとおもったのだが、「android.os.NetworkOnMainThreadException」が出てしまった。
コルーチンでやるのが Compose Activity っぽい気はするが、別スレッドにすればよかろうということで Tread で囲んだ。

    interface Tenki {
        @GET("/bosai/forecast/data/forecast/{area}.json")
        fun listTenkis(@Path("area") area: String?): Call<List<TenkiData?>?>?
    }

    class TenkiData(val publishingOffice: String, val reportDatetime: String)

    private fun networkRetrofit() {
        Thread {
            val retrofit = Retrofit.Builder()
                .baseUrl("https://www.jma.go.jp/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

            val service = retrofit.create(Tenki::class.java)
            val data = service.listTenkis("400000")
            val result: List<TenkiData?>? = data?.execute()?.body()
            Log.d("network", "${result?.get(0)?.publishingOffice}, ${result?.get(0)?.reportDatetime}")
        }.start()
    }

"?" が多いのは Android Studio の指摘をそのまま採用したためだ。
ここで TenkiData に取得しないパラメータを追加しても、それは別にエラーとはならなかった。なので、あれば読み取るし無ければ読み取らないだけのよう。型が違う場合には変換動作によって例外が発生する。


こうやってみると、楽かどうかという意味では volley の方が事前準備が少なくて楽だった。ただ、読み取って使うくせに型も知らないのか?という意味では Retrofit の方がよいのかもしれんと思った。 JavaScript 風に扱えるといえばよいのかな。

2022/08/06

[android] gRPCの準備をしてみよう(2022年8月)

Android で gRPC をしてみたいので、今の最新と思われる情報でやってみる。
便利なライブラリが公開されているかもしれんが、まずは gRPC のサイトに従ってやってみよう。

Quick start | Kotlin for Android | gRPC
https://grpc.io/docs/platforms/android/kotlin/quickstart/

プロジェクトは Kotlin で、Android OS は 8.0 にした。あまり下げすぎても上げすぎてもなあ、というくらいで決めた。

そこまではよかったのだが、この Quick Start は example code を使うようになっていた。
うーん、プロジェクトに何も入っていないところからやりたかったのだが・・・。
仕方ないので、プロジェクトファイルから変更箇所を探していこう。

grpc-kotlin/examples at master · grpc/grpc-kotlin · GitHub
https://github.com/grpc/grpc-kotlin/tree/0681fc85677e2cca53bdf1cbf71f8d92d0355117/examples

ここが protobuf のファイルから生成された gRPC API のライブラリ?にアクセスするヶ所を作っているのだと思う。

private val greeter = GreeterGrpcKt.GreeterCoroutineStub(channel)
  

しかし GreeterGrpcKt という class はGitHub にはない。
package が io.grpc.examples.helloworld なのでこの proto ファイルでよいと思うので、自動生成されるのだろうか? 検索したら "BUILD.bazel" というファイルの中に protos/ がしばしば見つかる。

Bazel上のAndroid  |  Android オープンソース プロジェクト  |  Android Open Source Project
https://source.android.com/setup/build/bazel/introduction

うーん、サンプル伸びるど手順だと gradle を使うようだから、とりあえずは気にしなくてよいのかな?

それにしても Stub とかテスト用のコードが多いな・・・と思ったら、テストのスタブとは別物だそうだ。

2022年はネイティブ×gRPCが激アツかもしれん
https://zenn.dev/efx/articles/e90a93c1bd210e

そうなのか。

 

2021年11月の Google Developers には、Kotlin が protobuf をサポートしたという記事があった。

Google Developers Blog: Announcing Kotlin support for protocol buffers
https://developers.googleblog.com/2021/11/announcing-kotlin-support-for-protocol.html

そこにリンクされていたのが 2020年4月の記事で、これは grpc-kotlin のことを指しているのかな?

gRPC, meet Kotlin | Google Cloud Blog
https://cloud.google.com/blog/products/application-development/use-grpc-with-kotlin

grpc-kotlin のことなら、まあそれを使うと言うことでよしとしよう。
気になるのは Kotlin の protobuf  サポートについてだ。

From a proto definition, you can use the new built-in Kotlin support in the proto compiler to generate idiomatic Kotlin Domain Specific Languages (DSLs).

protobuf のコンパイラを用意しなくてもうまいことやってくれるということだろうか。。。
いや、記事の真ん中くらいに gradle の設定が書いてあるな。サンプルはこちら。なのだが、このサンプルは拡張子が .kts なので Android Studio で普通にプロジェクトを作った場合の build.gradle を文法が異なる。困ったものだ。


grpc-kotlin/examples を Android で使うだけだったらどのくらい削除して大丈夫か調べてみた。
そうしないと、どれがいるものかわからないからだ。

  • grpc-kotlin/ 直下は examples/ 以外は削除してよさそうだ
  • examples/ 直下は、 client/ , native-client/ , server/ , stub-lite/ , stub/ は削除してもよさそうだ
  • examples/protos/ の中は、下の方までたどって helloworld/ 以外は削除してよさそう

ビルドできるかどうかという観点だとこうなった。動くかどうかとはまた別だろうが、動的にファイルを読み込むこともないと思うから大丈夫なのかな?

stub-android/ はいらないかと思ったのだけど android/build.gradle.kts が参照しているのだった。なくすと io.grpc.ManagedChannelBuilder が import できなくなるので何か使っているんだろう。

 

差分がこちら。
JavaVersion や jvmTarget が 11 になったり、androidx.core:core-ktx が 1.8.0 になったりするのは、まあおまけのようなものだ。

https://github.com/hirokuma/grpc-kotlin-sample/commit/64d02c28220ef5b7c7611c0df8ab3f105c09bf61

protos/ と stub-android/ は grpc-kotlin から持ってきた。
追加した位置は grpc-kotlin と同じ高さで、 app/ の中ではなく app/ と同列。 settings.gradle を変更しているのはそれらを module として追加するためだと思う。
build.gradle は protobuf のプラグインを読み込んでいたのをそのまま持ってきた。 stub-android だけ追加してもなんとかなりそうな気もしたが試してはいない。

プロジェクトの依存関係としては、 app が stub-android を参照するようにした。 stub-android は protos を参照するようになっている(もともと)。

 

取りあえずビルドは通ったのだが、Android から呼び出そうとすると、今の私には知識が足りなかった。。

https://github.com/grpc/grpc-kotlin/blob/0681fc85677e2cca53bdf1cbf71f8d92d0355117/examples/android/src/main/kotlin/io/grpc/examples/helloworld/MainActivity.kt

by lazy ?  setContent ??  suspend fun ???  @Composable ???? といった感じだ。

Android Compose のチュートリアル  |  Android デベロッパー  |  Android Developers
https://developer.android.com/jetpack/compose/tutorial?hl=ja

Kotlin もまだ足りてないのに、コルーチンだの Jetpack だのいろいろ追いついていないのだった。
見栄えは悪いが、簡易的に表示まで確認した。

response · hirokuma/grpc-kotlin-sample@27e6b39
https://github.com/hirokuma/grpc-kotlin-sample/commit/27e6b39fe06e66681bc148d0c8a631d2dfbe3179

2022/07/31

[vscode] vscode と editorconfig

私はテキストエディタでハードタブ、キャラクタコードで 0x09 の表現をスペース4つで見えるようにしていることが多い。

UNIX とかは 8 文字だけど、横幅が広くなると困るので 4 つにしている。
もともと C言語で書いていて、X Window とかで書いているときはそのまま従って 8 つだったのだけど、当時の文化として「横幅はだいたい 80桁で!」だったはずだ。メールも 76 だったと思う。
そうなるとインデントで幅を取られることが馬鹿らしくなり、自然と短くしていったのだった。

 

HTML を触ることがあった。
そのときに「タブは 2文字」というのがよく使われていたのだ(私の作業では)。
そもそもその時代になると、ハードタブで個人差が生じるくらいだったらソフトタブでスペースを直接埋め込めば間違いがない、という風潮だったように思う。
「インデントを削るのに文字数分削除するのが面倒!」「間違えて 3 とか 5 になってしまうかもしれないじゃないか!」と心の中で反発しつつも使っていくと、うん、慣れてしまった。
今やスペースでタブを表現せずにどうするんだ、くらいまでになっている。
人間、そんなもんだよね。

 

さて、ここまでは時代の風潮くらいのものなのだが、言語仕様でタブを意識するモノが出てきた。
Python と Golang だ。

Python は、インデントが正しくなっていないと実行時エラーになっていたと思う。
面倒なので例として PEP8 のインデントルールを見てみよう。

PEP 8 – Style Guide for Python Code | peps.python.org
https://peps.python.org/pep-0008/#indentation

> Use 4 spaces per indentation level.

なるほど、4スペースか。
・・・ではなく、Python の仕様確認が先だろう。

2.1.8 Indentation
https://docs.python.org/release/2.5.1/ref/indentation.html#l2h-9

> First, tabs are replaced (from left to right) by one to eight spaces such that the total number of characters up to and including the replacement is a multiple of eight (this is intended to be the same rule as used by Unix). The total number of spaces preceding the first non-blank character then determines the line's indentation. Indentation cannot be split over multiple physical lines using backslashes; the whitespace up to the first backslash determines the indentation.

Google翻訳
> まず、タブは (左から右へ) 1 から 8 個のスペースで置き換えられ、置換を含む最大文字数が 8 の倍数になります (これは、Unix で使用されるのと同じ規則になるように意図されています)。次に、空白以外の最初の文字の前にあるスペースの総数によって、行のインデントが決まります。バックスラッシュを使用してインデントを複数の物理行に分割することはできません。最初のバックスラッシュまでの空白がインデントを決定します。

うーん、これを正しく解釈できる自信がない。
tabs を one to eight spaces に置換するということは、純粋に 8文字のスペース文字に変換するのではなく、 8文字インデントになっている文章と見なして最大 8 文字のスペース文字に変換して頭を揃えようとする、ということだろうか。
インデントエラーの例も、けっこう緩いと思った。

  • 関数の先頭がインデントされているのはエラー
  • 関数の中身がインデントされていないのはエラー
  • インデントされないはずの行でインデントが変化しているのはエラー
  • インデントが戻るはずがない行でインデントが変化しているのはエラー

いくつインデントすべき、みたいなのは要件に入らないということだろう。


さて、いつものように本題から逸れていますが、いつものことです。

お勉強中の Go言語だけど、あっちは「インデントはハードタブ」という仕様だ!・・・と思っていたのだけど、Launguage Specification に "indent" は出てこなかった。

The Go Programming Language Specification - The Go Programming Language
https://go.dev/ref/spec

あーれー、私の勘違い??
まあいいや。golang のフォーマッタを使うと自動的にタブ文字になっていたと思うし。

 

そこらへんはよかったのだが、他から clone した GitHub のプロジェクトのファイルを開いたときに何か違和感があった。
おかしい・・・初めて見るコードなのに違和感がある・・・。

ああ、タブ文字が 8 スペースで表示されているんだ。
でも vscode の設定では 4 スペースにしているし。。。
4 スペースに変更しても別のファイルでは 8 スペースで表示されるし。。。
なんなの??

 

ようやく本題ですが、このプロジェクトには `.editorconfig` というファイルがあり、それを vscode が読み込んで反映させていたようだった。

EditorConfig
https://editorconfig.org/

私の設定よりもこっちが優先されるのね。
まあ、そうでないと意味が無いわな。


EditorConfig のページはなかなか気合いが入っている。
サポートしているサイトの紹介があるのだが、そのリンク先画像がたぶん自作なのだ。 GitHub なんかこれだよ。

image

普通に引用した方がかんたんだろうに、あえてこのページの世界観でアイコンを描くというところに気合いを感じる。だって、アイコンをそのまま借りた方がはるかに楽なのですぞ。

 

EditorConfig はすべてのエディタがサポートしているわけではないと思うが、いくつかの主要なエディタが採用しているならファイルとして作って置いても損にはならないかもしれない。テキストエディタで外部からの設定を求めるというのはどうなんだろうと考えなくもないが、嫌なら使わなければよいだけの話である。

vscode だとプラグインがいりそうなことを書いてあるのだが、使っていないつもりなのに反映されてしまった。
こういうのは個人の好みもあるから、けっこう難しいね。

2022/07/30

[win10] キーアサインの変更(2022年7月)

Windows でキーアサインを変更したいという要望はそこそこあると思う。
そうでなければ PowerToys にそういう機能を付けないだろう。

PowerToysWindows 用の Keyboard Manager ユーティリティ | Microsoft Docs
https://docs.microsoft.com/ja-jp/windows/powertoys/keyboard-manager

私は AutoHotKey と Change Key で割り当てている。

AutoHotkey
https://www.autohotkey.com/

「Change Key」非常駐型でフリーのキー配置変更ソフト - 窓の杜
https://forest.watch.impress.co.jp/library/software/changekey/

Change Key は CapsLock を 左Ctrl にするのと、半角/全角 を Esc と入れ替えるのに使っている。
AutoHotKey だけでいいやん、と思うかもしれないが、少なくとも私が AutoHotKey を使い始めた頃はまだうまくいかなかったのだ。

それに、AutoHotKey はアプリが起動しないと使えないが、Change Key はその前から使えるのでログインする前でも確か使えていたと思う。

そんな Change Key 万能!という雰囲気をかもし出しつつも、これは単独のキーしか変更できない。
Ctrl+F で右矢印キーの代わりにする、なんてことはできない。


そんなわけで、ショートカットキーのキーアサインを変更するアプリについてだ。

 

AutoHotKey も PowerToys もだが、Ctrl+[何か] のアサインを割り当てた場合、たまに Ctrl が押されない動作をしてしまうことがある。

AutoHotKey は長押しすると発生しやすいし、CPU負荷が高まるとさらに発生しやすいと思う。私は Ctrl+F を右矢印キーに割り当てているのだが、 Android Studio のエディタでそれをやるとときどき「f」が打ち込まれてしまってあせる。そして Android Studio は保存キーを使わずに勝手に保存したりするので、コンパイルエラーになって気付くことがたまにある。

PowerToys の場合はキーの押し始めに抜けることが多いと思った。処理が重たくなると押しっぱなしの状態でもすり抜けることはあるが、AutoHotKey よりは少ないかも?

 

AutoHotKey についてはもう 9年も前の記事だが、これがやっぱり出てくる。

AutoHotkey:キー押しっぱなし病・ホットキーすり抜け病対策の研究
https://did2memo.net/2013/10/03/autohotkey-ctrl-key-is-stuck/

まあ、すり抜けるときはすり抜けるのだけど、PowerToys で発生するということは OS として発生するのではないかと思う。それを対策したら AutoHotKey などでもうまくいくようになってくれるとよいのだがね。

AutoHotKey はスクリプトで書けるから楽なのが良い。自由度も高いし。そのせいで PowerToys よりもすり抜けやすくなっているのではないかという気がしなくもない。

難しいもんだね。

2022/07/10

[golang] 埋め込む順番で type は異なる

構造体の勉強中。

interface は「こういうメンバたちを持ってるよね? 持ってないと我が一族の名を名乗れないからね?」という脅しだと思う。いや、コンピュータ言語で脅しても仕方ないのだが、コンパイルエラーになるという意味では脅しと受け取った方がよいんじゃなかろうか。

ここ数年 C++ から離れているが、 class のメソッドで =0 にしていると abstract なメソッドという扱いになって、必ず派生した class でオーバーライドしないといけないようになってた気がする。
=0 が 1つでも入っているとインスタンスにできなかったと思うので、それがなくなるごとに抽象度が減るという表現をするのかもしれない。

 

その interface と似てるような似てないようなのが「埋め込み」とか「annonymous field」というやつだと思う。今のところお仕事で使う機会が無いのだが、C言語で class 的なものを使いたい場合のやり方だと認識している。
C言語だと構造体のメンバを定義した順にメモリ配置するので、

struct A {
  int a;
  char b;
}

struct B {
  int a;
  char b;
  double c;
}

という構造体を用意すると、a と b については型とメモリ上のアドレス相対位置が一致するので struct A と struct B の変数は a と b については気にせずアクセスできる。
c については、 struct A の変数を struct B としてキャストすればアクセスはできるだろうが、読み込めるのはそのアドレスにあるデータなのでどうなるかは不明だ。まあ alignment はpack(0) とかしてなければ問題ないので死にはしないだろう。

結局のところ、メモリ上にあるものはすべて「データ」に過ぎない。
それがプログラムなのか文字列なのかExcelのデータなのかというのは後付けの理由だ。画像データなんてフォーマット以外の部分はバイナリデータに過ぎないので、そこにプログラムとして動作する値があったとしても不思議ではない。ウイルスだったりセキュリティホールだったりはそういうところからやってくるんだろう。

 

さて話を戻して。

struct の説明で、メンバの並びが違うと違うものとして扱われるというのをどこかで読んだ気がする。まあ、私は C言語のことを思い浮かべながら Go言語を見ているので、書いてなくても「きっとそうなんだろう」と思い込んでいるだけかもしれない。

埋め込みだとどうなのか、ちょっと確認してみよう。

package main

import "fmt"

func main() {
    // annonymous field
    type Name struct {
        name string
    }
    type Age struct {
        age int
    }
    type User1 struct {
        Name
        Age
    }
    type User2 struct {
        Age
        Name
    }

    var u1 User1
    var u2 User2
    u1.name = "kuma"
    u1.age = 98
    u2.age = u1.age
    u2.name = u1.name
    fmt.Printf("u1=%v\n", u1)
    fmt.Printf("u2=%v\n", u2)
    fmt.Printf("compare: %v\n", u1 == u2)
}

最後の u1 == u2 がコンパイルエラーになる。

invalid operation: u1 == u2 (mismatched types User1 and User2)

ただ・・・これはAge と Name が違うからではなく、純粋に struct で定義したものが違うからエラーにしているだけだと思う。並びを同じにしてもエラーになるからだ。

 

多少異なるのがキャストについてだ。
埋め込む順番を同じにした場合、 User2(u1) も User1(u2) もエラーにならない。

しかし Name(u1) も Name(u2) もエラーになる。
そこは通してやってもいいんじゃないかと思ったが、そうすると Age(u1) なんかも通さないといけなくなってしまうのかもしれない。メンバの先頭だけ OK というのは説明しづらいからな。

2022/07/09

[javascript] プリミティブ型と toString()

困ったことに、JavaScript の try-catch で catch するものは Error 型とは限らないそうだ。
取りあえずログに出したかったのだが、JSON.stringify(e) で {} になったり、 e で [Object Object] になったりして、もう考えるのが面倒になってきた。
Error 型だったら、e.message と e.name はあるのだけど、ライブラリを使っていると派生したオブジェクトを返したりして、もうどうしてよいやら・・・。

Error - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Error

よく見ると、インスタンスメソッドで toString() があった。

Error - JavaScript | MDN
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#instance_methods

Error を派生したんだったら toString() もオーバーライドしてほどほどの文字列を作るのが礼儀なんじゃなかろうか、きっとそうだ、と思い込むことにした。

 

しかし、throw が Error 型じゃなくてもよいということはプリミティブ型でもよいはずだ。
プリミティブ型はオブジェクトじゃないはずだ。
オブジェクトじゃないなら Object を継承しないので toString() を持たないかもしれない。
だったら e の型チェックしてからじゃないと toString() が使えないのか。
めんどくさい・・・。

 

と思って文字列や数字を throw したのだが、toString() で文字列として出力されていた。
なんで??

本を読んだところ、プリミティブ型は使われる際にラッパーオブジェクトが一時的に生成されるようになっているらしい。
つまり、文字列なら String型、数字なら Number型が生成される。
これらはオブジェクトなので toString() を使える、という理屈のようだ。

もしかしたら toString() などのメソッドを使う直前に生成されるのかもしれん。そっちの方が理屈に合うか。

 

それはそうと、元の問題である catch したデータをログに出した意見だが、これは昔からどうしたもんだかって話のようだ。 Error の派生についてStackoverflow のリンクが載っているくらいだから、 throw となるとさらにめんどくさそうだ。

throw - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/throw

オンラインでコードを動かせるので throw を書き換えながら試したが、 new Error(文字列) は console.log で出力できるが、単に Object として {message: 文字列} なんかを throw すると [object Object] になる。
じゃあ console.log で JSON.stringify() したものを出せば良いのだが、 new Error() だと {} だけになる。

 

そういえば、console.log を使いたいわけでは無く、ログファイルに出力させたいので「文字列」として取得したいのを忘れていた。 console.dir が使えるという記事があったのだけど、そういうわけで今回は対象外だ。

となると、無難に toString() しておけば文字列として取得できるのか。
オブジェクトの中を全て見れば詳細な情報が確認できる場合もありそうだが、そこはもうピンポイントで調べたい場合だけとあきらめても許してくれるんじゃないだろうか。

2022/07/03

[golang] よく出てくる context.Context

通信するライブラリなどを使おうとするとしばしば登場するのが context.Context だ。
gRPC だとこんな感じ。

Basics tutorial | Go | gRPC
https://grpc.io/docs/languages/go/basics/#simple-rpc-1

context.Background() でコンテキストを作って与えておけば取りあえず使えるので深く考えずに使っていたのだが、真面目に実装していくと理解していないとまずいと感じた。というより、あまり理解していなくてコードレビューで指摘を受けてしまった。

よく出てくるのでほどほどに理解したい。
ネットで「golang context」で検索するとたくさん出てくるので、きれいな解釈はそちらを見た方がよいだろう。なら書くなよと思われそうだが、私は書かないと理解ができないタイプなので仕方ないのだ。


まずは godoc

context package - context - Go Packages
https://pkg.go.dev/context#Context

出てくるのは context.Context だが、パッケージの存在目的が Overview に書いてあるので目を通す。

  • プロセス間で伝播
    • デッドライン
    • キャンセル
    • シグナル
    • その他 request-scoped な値

「between proceses」とあるが、Linux でいうところのプロセスではなく goroutine を指してるのだろうか。
「request-scoped values」は、処理の要求を行ったコンテキストが持っている値だろうか。 goroutine だったらそういう値はがんばらなくても参照できそうなものだが、うーん?

「プログラミング言語Go」には context が載っていない。本は v1.5 のときに書かれているのだが context が生まれたのは v1.7 のようだ。ちなみに本では goroutine のキャンセルについて書かれており、それは channel を使って行うようになっている。

 

こちらが、よく紹介されている go.dev のブログ。

Go Concurrency Patterns: Context - The Go Programming Language
https://go.dev/blog/context

まあ、もう 8年も経っているので日本語で紹介しているサイトの文面を読むだけでいいや。。。


いろいろ使い道はあるのだが、主な使い方はキャンセル処理の通知だと思う。
チャネルを使えば処理の終了を待ち合わせることもできるし、キャンセル処理をさせることもできる。

hiro99ma blog: [golang] chan で待つ
https://blog.hirokuma.work/2022/06/golang-chan.html

ただ、それをおのおのの実装でやっていくと違いが出てくるだろうし、似たような処理があちこちに出てきて格好が悪くなるから context にそういうのをまとめておいてみんな使いましょう、ということなのだろう。
A が Bライブラリを使って、 B が Cライブラリを使って、とやっていって、Aがキャンセルする処理を Bに伝えて、それを B が Cに伝えて、ということになるのをルール化したというところか。

なので、goroutine を走らせておいて「あとは好きにやっとけ」というタイプの処理だと使うことが無いかもしれない。多少なりとも goroutine を走らせた方が結果を待ったりするからこそ自分がキャンセルすることを下々に通知する必要が出てくると思われるからだ。

ただ、goroutine を呼び出した方が先に終了してしまうと goroutine の処理が終わる前に中断させられてしまうことになるから、何かしら終了を待つようなことにはなるのではなかろうか。
そう考えると、あまり難しく考えず、goroutine の中で context.Context を引数にとる API を使うなら その外側で context.Context を渡すようにした方がよい、くらいでいいのかな。

 

そういう見方をすると context のメンバーの用途がわかりやすい。

Done() は呼び出し元がキャンセルしたのを子が知るための channel 。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    fmt.Printf("start\n")
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go func(ctx context.Context) {
        fmt.Printf("goroutine start\n")
        time.Sleep(10 * time.Second)
        fmt.Printf("goroutine done\n")
        <-ctx.Done()
        fmt.Printf("detect Done!")
    }(ctx)

    time.Sleep(5 * time.Second)
    cancel()
    fmt.Printf("main done.\n")
}

$ go run .
start
goroutine start
main done.

まあ、待ち始めるのが 10秒後なので、5秒後に cancel() するとそうなるよね。。。
だからといって goroutine の方を先に終わらせて Done() を待つというのは意味が無い。それなら goroutine を終われば良いだけだ。
となると、select で Done() を待つのと同じように、別の channel も待つようなシーンでないと意味が無いのか。

やるなら、cancel() を通知した後、自分が実行した goroutine が終わるのは待つようにするというところか。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    fmt.Printf("start\n")
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    done := make(chan struct{})
    go func(ctx context.Context) {
        fmt.Printf("goroutine start\n")
        time.Sleep(10 * time.Second)
        fmt.Printf("goroutine done\n")
        <-ctx.Done()
        fmt.Printf("detect Done!")
        done <- struct{}{}
    }(ctx)

    time.Sleep(5 * time.Second)
    cancel()
    <-done
    fmt.Printf("main done.\n")
}

5秒経過したら cancel() を呼び、待ち合わせている goroutine からの channel done を待ってから終了させる。
done を通知するのは自作の goroutine だから、done しなかったら自分のバグということでよいだろう。

では、もしその goroutine の中で別の goroutine を呼び出しているのであればどうするか?
同じだ。 Done() の channel を受け取ったら配下の goroutine をキャンセルさせるように通信して終わるまで待つのだと思う。

いやー、難しいね。

2022/06/25

[vbox] 全画面とディスプレイの電源を入れる順番

VirtualBox にて、ホスト環境は Windows10 、ゲストOS で Ubuntu という環境で作業することが多い。
地味に困っていたのが、全画面表示するとなぜか対象になるディスプレイが固定されないという現象だ。

うちの PC はマルチディスプレイ環境で、構成はこうなっている。

image

3つもいらんやろう、といわれそうだが、USB の DisplayLink モニタがあったのでつないでいるのだ。
TeraTerm などを立ち上げておくのにちょうど良かったし。
なお接続方法は、1番が DisplayPort、2番が HDMI、3番が DisplayLinkである。

 

VirtualBox のゲストOS としてはシングルディスプレイで動かしているのだが、全画面表示させると、なぜか 3番になることがほとんどだったのだ。
希望としては 1番で、2番でもまあいいや、くらいなのになぜか 3番の一番小さい(800x600)になるのだ。
非常に困る。やむなくウィンドウの最大表示を使っていたのだが、ちょっととはいえ小さいウィンドウなのが気に障るのだ。

 

ようやく気付いたのは、PCに電源を入れるときの順番によって違いがあるということだ。
私の場合、ディスプレイの電源を手動でオフにしている。
そして、本体の電源を入れて、ディスプレイをオフにしたままパスワードを入力してからディスプレイの電源を入れている。手順が変わらないので表示があってもなくてもいいや、と。

しかし、USB機器は接続したままなので、最初に 3番のモニタに表示が行われる。そのあとで電源を入れたディスプレイに表示が行われることになるのだが、おそらくこの順番が影響している。
検証はしていないが、先にディスプレイの電源を入れてから本体の電源を入れるようにすると今のところ 1番に全画面表示されている。

よかったよかった。

2022/06/19

[golang] 空文字列の []byte キャストは nil ではない

ちょっとだけ不安になったので確認しておこう。

 

package main

import (
    "fmt"
)

func main() {
    x := []byte("")
    if x == nil {
        fmt.Printf("nil\n");
    } else {
        fmt.Printf("not nil\n");
    }
}

$ go run .
not nil

よかった、認識は間違えてなかった。
扱いとしては []byte{} のときと同じかな。

初期値だったり nil を代入しない限りは nil にならないという認識だ。
でないと struct{} なんかも nil になってしまって区別が付かなくなるはずだ。
ただ、 struct{} が必ず別の値になるとは限らないという記述をどこかで見かけた気がする。。。

2022/06/12

[golang] chan で待つ (3)

2番目の記事で疑問になっていたところを解消しておこう。

hiro99ma blog: [golang] chan で待つ (2)
https://blog.hirokuma.work/2022/06/golang-chan-2.html

のこちら。

その後で A が終わって待ち解除になると B 待ち状態になるが、既に処理が終わった B についてどうなるのかわからん。

についてだ。


まず、勘違いしていた件。

 

done := make(chan struct{}, 3)

こう書いたとしても、

done[0] <- struct{}{}

のようには書けないということだ。
make() は capacity を設定するものだから len() ではなく cap() を使って容量を取ってくるのだった。

 

それを踏まえて。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := []chan struct{}{
		make(chan struct{}),
		make(chan struct{}),
		make(chan struct{}),
	}
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done[0] <- struct{}{}
		fmt.Printf("goroutine sent - 1\n")
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done[1] <- struct{}{}
		fmt.Printf("goroutine sent - 2\n")
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done[2] <- struct{}{}
		fmt.Printf("goroutine sent - 3\n")
	}()
	fmt.Printf("waiting...\n")
	<-done[0]
	fmt.Printf("done - 1\n")
	<-done[1]
	fmt.Printf("done - 2\n")
	<-done[2]
	fmt.Printf("done - 3\n")
	fmt.Printf("all done\n")
}

実行。

$ go run .
start
waiting...
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
goroutine sent - 1
done - 1
done - 2
done - 3
goroutine sent - 3
all done

時間の経過が見えないが、「goroutine done - 1」以降は一気に出力されている。
「sent」のログは全部出ていないが、これはその前にプロセスが終了したからだろう。

まあ、これなら前回の最後に書いた for でぐるぐる回して待つ方がシンプルに見えるな。

Blogger に StackEdit からアップするも画像に悩む

普段、ブログにアップする際は Windows の OpenLiveWriter を使っている。
ただ、Microsoft Store だったり GitHub の exe に上がっていたりするのは最新バージョンではない。最近の(というわけでもないが) Gooble Blogger には使えなかったと思う。
自分でビルドして使っていたのだが、新しくセットアップした PC にはそれをしていない。
Blogger の管理画面でエディタを使うことはできるのだが、コードを貼り付けるても見やすくない。
面倒なので、古い PC でアップしていたのだが、そこまでしてブログを書きたいかといわれると悩ましいし、かといってこういうのを書かなくなると老化が進みそうで怖い。

そんな感じでブログエディタをどうしたものか悩んでいたのだが、 StackEdit で連携させることができるというのを偶然知った。
私のブログは README.md の延長みたいなものだから、コードが見やすく貼り付けられて、あとは画像を貼り付けられればよい。


今日はそうやってブログを書いていた。
問題は画像だ。
StackEdit はマークダウンで書くことができるだけなので、画像については範囲外だ。画像ファイルのリンクがあれば貼り付けられる。
しかし、Blogger でいつも使っていた Google Photo だと直接画像ファイルにならないのでマークダウンの画像貼り付け表記ではうまくいかない。

例えば、これは OpenLiveWriter から画像を貼り付けてアップしたときに作ってくれたリンク&画像だ。

https://drive.google.com/uc?id=1V5UE1X-xNM9F3XqVJrOpbuxc4eXqMwft

ああ、貼り付けて分かったけど Photo じゃなくて Drive なんだ。
Drive のファイルからリンクを作ってみるとこうなった。

https://drive.google.com/file/d/1V5UE1X-xNM9F3XqVJrOpbuxc4eXqMwft/view?usp=sharing

うーん、 file/duc?id= に変換して /view 以降は削除する?
大した作業ではないが、来週の私、いや明日の私もきっと忘れているな。
画像のリンクだけなら OpenLiveWriter の方が楽だな。

[golang] chan で待つ (2)

golang の goroutine は、確か実装依存だったと思う。
Linux だったらきっと pthread なんだろうな、と思っていたが、そうではなくて自前で並列処理を作っていたような記憶がある。
まあ、使う方としてはそこまで意識をしないけど、OS 無しだったり独自OS だとどうなるのか気になるね。


さて、前回は 1つのチャネルで 1つの処理が終わるのを待つのを確認した。処理結果がいらない場合にわざわざ struct{}{} を返すので、別に bool とかでいいんじゃないとも思ったが、本に struct{}{} を返すよう書かれているので、そういうお作法と考えるのが良いだろう。値を返す、ということ自体が結構めんどうな処理になりそうだしね。

では複数の goroutine が全部終わるのを待つ場合も考えよう。
make() を使うので第2引数に待ちたいだけの数を設定して待てばよさそうな気がしたのだが、for で待つにしてもどの順番で処理が終わるか分からないから、make() で 3つ作って順に 処理A, B, C に割り当てたとすると、A を待っている間に B, C が先に終わってしまうこともあるだろう。その後で A が終わって待ち解除になると B 待ち状態になるが、既に処理が終わった B についてどうなるのかわからん。


 それはともかく、make()で複数の chan を作るのは「バッファ有りチャネル」として紹介されていた。
本の説明では 3つ作っている。そして 4つめは「送信文は待たされます」と書かれている。ということは chan は受信待ちだけでなく送信待ちというものもあるということか。
ただ、これは対象がスライス全体を与えているからそうなるのだと思う。スライスの各要素で送受信するようにしたらバッファ無しのチャネルと同じになるはずだ。

あるいは、スライス全体にすることで「早い者勝ち」になるようだから、それをループで回数だけ待てばよいはずだ。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := make(chan struct{}, 3)
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done <- struct{}{}
	}()
	for range done {
		<-done
	}
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/xxx/golang/src/chan/main.go:30 +0x125
exit status 2

あれ、ダメなんだ。
for の回数を固定させるとエラーは起きないようなので range done

package main

import (
	"fmt"
	"time"
)

func main() {
	const eventNum = 3
	fmt.Printf("start\n")
	done := make(chan struct{}, eventNum)
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done <- struct{}{}
	}()
	for i := 0; i < eventNum; i++ {
		<-done
	}
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
done

本には、

  • len() は現在バッファされている要素の個数を返す
  • cap() はバッファ容量を返す

とある。
range donelen(done) と同じルールならダメそうだ。
本によると、 range でチャネルを回すのはチャネルに対して送信されたすべての値を受信するときと書かれているので len()と同じなのだろう。

いろいろやってみらんとわからんもんやね。

[golang] chan で待つ

オライリーさんからGo言語の本が出たので悩み中。

O’Reilly Japan - 実用 Go言語
https://www.oreilly.co.jp/books/9784873119694/

まだプログラミング言語Goも読み進めていないので早すぎるという気もするが、お仕事で使っている以上実用の本もほしいのが正直なところだ。
まあ、そのうち買うんだろうね。


さて、本題。

処理Aがあって、その処理の後に処理Bを行いたいことがあった。
それだけなら続けて書けば良いだけだったのだが、コンテキストが同じ状態で処理Aと処理Bを実行できないようで(自分で作ってない処理なのでよくわからん)、処理A は goroutine してやらないと処理Bが必ず失敗していた。
つまり、処理Aが非同期になったのだ。

今は処理Aが終わりそうな時間だけtime.Sleep()で待ってから処理Bを実行させているのだが、そういえば非同期処理の待ち合わせがあったなあ、と思い出した。
channelだ。
まあ、goroutine と channel はプログラミング言語Goでも同じ章で説明してあるくらいだしセットで覚えるものよね。

チャネルでの待ち合わせ

チャネルはmake()で作り、close()で閉じられる。使わなければよいだけなのだろうけど、明示的に「これ以降は使えない」というフラグを立ててくれるそうだ。

make(chan 型)で作るが、型は戻り値と思っておけばよさそうだ。
TypeScriptでいえばPromise<型>みたいな感じかな。
単に待ち合わせたいだけだったら戻り値はいらないのだが、そういうときはstruct {}を使えばよいようだ。golangにvoidはないのだね。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := make(chan struct{})
	go func() {
		fmt.Printf("goroutine start\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done\n")
		done <- struct{}{}
	}()
	<-done
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start
goroutine done
done

10秒開けているのだけど、終わってからdoneが実行されている。

2022/06/05

[golang] go.mod の pseudo-version は go mod tidy で修正される

相変わらず go.mod で悩んでいた。

hiro99ma blog: [golang] go.mod の pseudo-version の調べ方がわからん
https://blog.hirokuma.work/2021/11/golang-gomod-pseudo-version.html

hiro99ma blog: [golang] go.mod の pseudo-version
https://blog.hirokuma.work/2021/12/golang-gomod-pseudo-version.html

なんとなく pseudo-version の書き方は分かったものの、面倒であることに変わりは無い。

開発中は replace で相対パス指定しておけばごまかせるのだが、GitHub Actions で linter を仕込んでいたりするとエラーになって気持ちが悪い。 Actions の方も相対パスで使えるようにすることができるのかも試練が、そこまでするくらいだったら go.mod を修正した方が楽そうだが、修正が面倒。。。

 

何か pseudo-version を自動で取ってくるツールがないか調べていた。

Mastering Go Modules Pseudoversions | JFrog GoCenter
https://jfrog.com/blog/go-big-with-pseudo-versions-and-gocenter/

真ん中当たりに「How to Fix Improper Pseudo-versions」という項目があり、修正の仕方が載っていた。なんと go mod tidy でうまいことやってくれるそうだ。

書き方は、pseudo-version を書く代わりに git の commit-id を書くだけ。これで go mod tidy すると pseudo-version に書き換えてくれた。 replace のところでもよいようだ。

今までの苦労は一体。。。

 

まあよい。できれば良かろうなのだ。

2022/05/29

[golang] 配列の比較とスライスの比較

似てるようで似ていない golang の配列とスライス。

 

配列同士の比較

package main

import "fmt"

func main() {
    array1 := [...]int{1, 2, 3}
    array2 := [...]int{2, 2, 3}
    fmt.Printf("array1 == array2: %v\n", array1 == array2)
    array2[0] = 1
    fmt.Printf("array1 == array2: %v\n", array1 == array2)
}

$ go run .
array1 == array2: false
array1 == array2: true

 

配列は宣言時の要素数がそのまま型として扱われると考えておくと良いだろう。
なので、[3]int と [4]int は別の方だから実行時に失敗するのではなくコンパイルエラーになる。

    array3 := [...]int{1, 2, 3, 4}
    fmt.Printf("array1 == array3: %v\n", array1 == array3)

invalid operation: array1 == array3 (mismatched types [3]int and [4]int)

 


スライスは直接の比較ができない。

    slice1 := array1[:]
    slice2 := array2[:]
    fmt.Printf("slice1 == slice2: %v\n", slice1 == slice2)

invalid operation: slice1 == slice2 (slice can only be compared to nil)

 

よく出てくるのが reflect.DeepEqual() を使う方法だった。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    array1 := [...]int{1, 2, 3}
    array2 := [...]int{2, 2, 3}
    array3 := [...]int{1, 2, 3, 4}

    slice1 := array1[:]
    slice2 := array2[:]
    slice3 := array3[:]
    fmt.Printf("slice1 == slice2: %v\n", reflect.DeepEqual(slice1, slice2))
    fmt.Printf("slice1 == slice3: %v\n", reflect.DeepEqual(slice1, slice3))
    array2[0] = 1
    fmt.Printf("slice1 == slice2: %v\n", reflect.DeepEqual(slice1, slice2))
    slice1 = append(slice1, 4)
    fmt.Printf("slice1 == slice3: %v\n", reflect.DeepEqual(slice1, slice3))
}

$ go run .
slice1 == slice2: false
slice1 == slice3: false
slice1 == slice2: true
slice1 == slice3: true

配列をスライスに変換しているのは、単に前のコードを使い回していただけで意味はない。
いま気付いたが、スライスに変換しているといっても、その場で値をコピーしてまるまる別物になるわけではないのだな。
なんでかというと、array2[0] に代入したら slice1 と slice2 が同じ値と判定されているからだ。
COW(Copy On Write)かな?

package main

import (
    "fmt"
)

func main() {
    array2 := [...]int{2, 2, 3}
    slice2 := array2[:]

    slice2[0] = 1
    fmt.Printf("array2[:]: %p\n", &array2)
    fmt.Printf("slice2: %p\n", slice2)
    slice2 = append(slice2, 4)
    fmt.Printf("slice2: %p\n", slice2)
}

$ go run .
array2[:]: 0xc0000ae000
slice2: 0xc0000ae000
slice2: 0xc0000a8030

さすがに明示的に変数への代入をしない限りは書き換わらないか。

 

[]int 型なので reflect.DeepEqual() を使ったが、 []byte 型の場合は bytes.Equal() がある。

package main

import (
    "bytes"
    "fmt"
)

func main() {
    array1 := [...]byte{1, 2, 3}
    array2 := [...]byte{2, 2, 3}
    array3 := [...]byte{1, 2, 3, 4}

    slice1 := array1[:]
    slice2 := array2[:]
    slice3 := array3[:]
    fmt.Printf("slice1 == slice2: %v\n", bytes.Equal(slice1, slice2))
    fmt.Printf("slice1 == slice3: %v\n", bytes.Equal(slice1, slice3))
    array2[0] = 1
    fmt.Printf("slice1 == slice2: %v\n", bytes.Equal(slice1, slice2))
    slice1 = append(slice1, 4)
    fmt.Printf("slice1 == slice3: %v\n", bytes.Equal(slice1, slice3))
}

$ go run .
slice1 == slice2: false
slice1 == slice3: false
slice1 == slice2: true
slice1 == slice3: true

 

シンプルなスライスだったら reflect.DeepEqual() を使うのにちゅうちょしないのだが、構造体のスライスとかになると心配になって使いづらい。説明が長いだけで使うのに不安を感じる。

理解して使えばよいのだが、それくらいだったら自分でループ回して比較した方が安心だと考えてしまう。比較するメソッドを作ればそこまで苦痛ではないだろう。

DeepEqual の実装は説明文に比べるとかなり短い。 go1.18.2 だとこうなっていた。

func DeepEqual(x, y any) bool {
    if x == nil || y == nil {
        return x == y
    }
    v1 := ValueOf(x)
    v2 := ValueOf(y)
    if v1.Type() != v2.Type() {
        return false
    }
    return deepValueEqual(v1, v2, make(map[visit]bool))
}

・片方でも nil があるなら単純比較
・型が違えば false
deepValueEqual()

短いのは条件だけだからだった......

単純比較は nil 同士なら true になるかと思ったのだがそうではなかった。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyType1 = struct {
        Value int
    }
    type MyType2 = struct {
        Value float32
    }
    var val1 *MyType1
    var val2 *MyType2
    val1 = nil
    val2 = nil
    fmt.Printf("val1=%v, val2=%v, compare=%v\n", val1 == nil, val2 == nil, any(val1) == any(val2))
    fmt.Printf("val1 == val2: %v\n", reflect.DeepEqual(val1, val2))
}

$ go run .
val1=true, val2=true, compare=false
val1 == val2: false

なお、型が違うので val1 == val2 と書くとコンパイルエラーになる。

 

DeepEqual() で期待している比較の処理は deepValueEqual() で行われていて、こちらは長い。
あまり見ていないが、型によって比較の仕方が違うから処理に時間がかかりそうだ。
パフォーマンスを気にするシーンだったら自前で書いた方がよさそう。

2022/05/28

[excel]

私がよく使う Excel 2016 では、線に関してはこのくらい選択肢がある。

image

 

線を何本か描いて、複数の関係があるのですよ、という図を描くことがしばしばある。
使えそうなのはこれらだ。

image

 

この記事を読むくらいだからわかると思うが、こういう描き方くらいでしか複数本を描くのは難しい。

image

ベジエ曲線のように、そこまで自由にはできないのだ。

面倒なので、私は間に円のオブジェクトを置いている。
曲線部分にも接続点があるので自由が多少きくからだ。

image

右下のだけ、Excel の補助無しで接続させた。
このくらい意図通りにならないので、だいたい自分で設定している。
楕円の中で接続する線がないが、それは描けば良いだけのことだ。


Excel 以外で描けば良いだけなのだが、私の開発スタイルが Excel を基準にしているのでちょっと難しいね。

2022/05/22

[typescript] インデックスシグネチャ?

覚えたい言語がいくつかある。 TypeScript, Kotlin, Go, Rust。
Rust が気持ちとしては強いのだけど、お仕事で今のところ使っていないので優先度が下がっている。
TypeScript と Go が一番多いのだが、どうやってもできないときに調べるくらいで、あとは適当に書いたら適当に動く(他のところをまねするのも含む)ので、なんとかなっている(と思いたい)。

それでもたまには本を読んで勉強する。たまにはじゃダメだろうと思うが許してほしい。
また OJT だけだと出てこないやり方に気付かないこともあるので、そこそこ体系的な勉強もいると思うのだ。

 

今日、 TypeScript の本を読んで出てきたのが「インデックスシグネチャ」だ。


TypeScript の本に出てきたのだが、インデックスシグネチャ自体は JavaScript から存在する書き方らしい。
MDN で検索したけど出てこないので、よくわからん。

オブジェクトのメンバーを参照するとき、普段は xxObj.yyItem のようにドットで指定するが、これを xxObje['zz'] のように参照するやり方のようだ。ドットで指定するやり方だと実装時にわかっていないと書けないが、添字で指定できるなら動的に参照できるというのがメリットだと思う。

Index signature(インデックス型) - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/type-system/index-signatures

JavaScript の方を良くしらんのだが、TypeScript の場合はインデックスとして使えるのが string か number に限定されるとのこと。

 

なんとなくわかったつもりになったが、これってキーとなる型が number か string の連想配列と考えてもよいのではないだろうか。
連想配列だったら Map がある。

Map - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map

同じことができそうなのだが、違いは何だろうか? JavaScript 自体には連想配列はこれだ、みたいな定義が無いとかかもしれんが、 Object と Map がそういう立ち位置のようだ。

いろいろ覚えたくない人としては Map だけ覚えておけばいい、と誰かに言ってもらえると助かるのだがね。

2022/05/15

[typescript] ちょっと触った感想

週末に typescript を少し触っていたので感想を残しておこう。

 

トランスパイル後のjsファイルもcommitしたくなる

ts ファイルがあってもトランスパイルしないと node.js で動かせない。
トランスパイル使用としたら tsc をインストールしないといかん。
大したことじゃないのだけど、全然大した作業じゃないのだけど、クラウドに VM 立てて、 node.js と npm が使えるようになって、 git clone してさあ npm start、ってやったら js がないってなったときにちょっとガックリするのだよね。

ts-node がよく紹介されるので使っていたけど、リポジトリ見たら結構大きい・・・。
それなら Makefile 作って、make install、make、npm start、の 3ステップくらいにしたらよいんだろうか。
でも make も何かインストールしないと使えないので、それはそれでって感じだ。

今回は gRPC のためにやっていたので proto ファイルもある。
proto ファイルからツールを使って d.ts ファイルと js ファイルを作ってもらい、それを使った ts ファイルを作り、トランスパイルして js ファイルを作ってもらう。

最後には proto も ts もいらなくなるのだから、なんとなく悲哀を感じてしまった。
node.js みたいに node.ts とかあるとよいのだけどね。

 

トランスパイルしたファイルのディレクトリ

tsconfig に "outDir" があったので "./out" にしてみた。
proto ファイルから d.ts ファイルと js ファイルを作るので、それは "./proto" に置いてみた。
そうすると tsc は成功するのだが node.js で実行しようとするとエラーになる。
import する場所が "./proto" なのだけど js ファイルがあるのは "./out" なので、実際には "../proto" にあることになるからだ(あとから js ファイルを ./out/proto に置けば良いことに気付いたがね)。

rootDirs に "./proto" を追加して import は相対パスで指定すればよいと思ったのだけどダメだった。
「This does not affect how TypeScript emits JavaScript」とあるからトランスパイル後のパスまでは考慮しないってことだと思うけど、tsc で効いてない理由が分からん。

私は挫折して d.ts ファイルをカレントディレクトリに持ってきて "./" で参照するようにし、js ファイルは "./out" に置くようにした。

TypeScript の paths はパスを解決してくれないので注意すべし! – 自主的20%るぅる
https://www.agent-grow.com/self20percent/2019/03/11/typescript-paths-work-careful/

これは paths なのでちょっと違うけど、import が解決できないという意味では同じだ。 ts-patch , typescript-transform-paths を使ってみたが、なんかもう一手間いる感じがした。

[typescript] gRPCはどうやるのがよいんだかわからん

前回、import したファイルを export することについて調べていた。

https://blog.hirokuma.work/2022/05/typescript-import.html


が、元々は gRPC というか proto ファイルだけある状態で呼び出すコードを typescript で書きたいけどどうすりゃいいんだ、というのが目的だった。

proto-loader-gen-types を使って自動生成された tsファイルが大量にあったのでまとめようとしたのだが、よく見るとまとめているようなファイルも生成されていることに気付いた(つまり export で調べたことは関係なくなった)。言い訳だが、ファイルが大量にあって気付かなかったのだ。

別のツールを使っているサイトを探してみた。

OK Google, Protocol Buffers から生成したコードを使って Node.js で gRPC 通信して | メルカリエンジニアリング
https://engineering.mercari.com/blog/entry/20201216-53796c2494/

ようやく気付いたのだが、typescript のコードを生成するということは proto ファイルは実行時にいらないんじゃないか? proto ファイルは情報が載っているだけで、そのファイルを使って gRPC を行うわけではない。proto ファイルが TypeScript でいう型情報みたいなものだと考えて良いと思う。

そう考えれば、typescript もそんなに身構えなくてよいのではなかろうか。構えてないけどね。

 

[typescript] 複数のimportするファイルを何とかしたい

タイトルだと全然わからんのでちゃんと説明をします......

 

発端は gRPC クライアントアプリを JavaScript で作っていたので TypeScript にしようと考えたことだった。
いままで TypeScript のことを調べはしたものの、 1ファイルで終わるような内容であれば JavaScript で済ませていたのだが、大きくなりそうな気配があったので TypeScript にしておこうとしたのだ。

--init で tsconfig.json を作り、ちょっとだけ変更して、拡張子を js から ts にリネームし、プリミティブ型以外のところは any で逃げてコンパイルというかトランスパイルというか、ともかく JavaScript にして node.js で実行するところまではできた。

次の段階は any を何かの型に置き換える作業だろうと思って進めていた。
作っていた JavaScript の中では特に Object を作るようなことはしていなかったのだが、 gRPC のリクエストとレスポンスは proto ファイルに由来する型を使っていた。

では proto ファイルから型を作る何かがあるだろうと調べて出てきたのがこれだった。

Generating TypeScript types
https://github.com/grpc/grpc-node/tree/master/packages/proto-loader#generating-typescript-types

npx proto-loader-gen-types を実行すると、proto ファイルを読み込んで型情報を持つ ts ファイルを作ってくれた。
そこまでは良かったのだが、proto ファイルに記述されている message ごとに ts ファイルを作るようだった。使う予定の proto ファイルにはたくさん message があり、大量の ts ファイルが生成された。

これを import すれば使えそうなのだが、その前に「この大量のファイルを import するということ自体を何とかできんだろうか、と考えたのだ。


まず考えたのがワイルドカード。
import もモジュールといいつつファイル名みたいなもんだから from のところに './proto/*' みたいな書き方をすればよいんじゃないの?と思うのは仕方あるまい。

結果としてダメだった。
まあ、同じディレクトリにあるだけで読み込まれるってのはさすがに危険すぎるな。

 

dynamic import というやり方も見つけたのだが、別に静的で良いのだ。
単に import 文をたくさん書きたくないだけなのだよ。
いや、書いても良いけど import だけで何十行もあるような状況が嫌なのだよ。

 

というわけで次に思いついたのが、別の ts ファイルに import だけ詰め込んで それを import するような手段が執れないだろうか、ということだった。

import - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

JavaScript だろうと TypeScript だろうと import は import のはずだから MDN の説明でよいと思う。
ただね、読む前に想像すると、そんなことはできないんじゃないかと思うのだ。
まあ、やらんとわからん。

 

image

image

>node index.mjs
aaa=30
bbb=20
ccc=10

TypeScript ではないが、まあよかろう。
これが基本形だ。

 

ダメだったワイルドカード。

import * as aaa from './files/*.js';

console.log(`aaa=${aaa.A}`);
console.log(`bbb=${aaa.B}`);
console.log(`ccc=${aaa.C}`);

Cannot find module になる。
ワイルドカードと見ずに "*.js" というモジュール名として処理するからだろう。

 

a.js, b.js, c.js は変更しないとして files/index.mjs を追加してみた。
まあ、ダメなんだけどね。

image

>node index.mjs
aaa=undefined
bbb=undefined
ccc=undefined

import でモジュール名しか指定していない場合は import はしないのだね。。。

副作用のためだけにモジュールをインポートする
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only

それに、もし import できていたとしても export していないから参照できないだろう。
これなら動いた。

image

>node index.mjs
30
20
10

export {a.A} みたいな書き方をすれば total.A で参照できるかと思ったがダメだった。 export {a.A as A} もダメだ。
再export すればいける。

image

export from という書き方もあるそうだ。

export
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/export

image

これくらいだったら bash で機械的に作れるんじゃないかな。

2022/05/08

[golang]go.modの入れ子を理解できなかった

go.mod をずいぶん理解できたんじゃないかと思っていたのだが、入れ子になった場合、つまり サブディレクトリの中にも go.mod がある場合にどうしたらよいのかがわからなかった。

 

なんで入れ子にしようとしたかというと、gRPC 用の proto ファイルを別管理しようとしていたからだ。過去の記事にも同じことを書いたような気がするが、

  • proto ファイルをサーバ側のアプリと同じリポジトリに置いた
  • でも proto ファイルの方はバージョンが変わらずサーバアプリだけ更新されることが多い
  • うーん、ならば go.mod を別にした方がよいのでは?

という思考からだ。

リポジトリごと分ける方が無難なのだろうけど、それはそれでやりすぎな気がしてそうしたのだ。
サーバもクライアントもアプリが小さいなら同じリポジトリに置いてしまってよかったかもしれないが、clone するだけで大ごとになるようなサイズなので「サーバ+proto」「クライアント」の2つに分けている。

と「仕方ない」という感を出そうと思ったが、初回のリリースまでは proto もサーバアプリと同じ go.mod にして、リリースしたらどうせ tag を付けるだろうからバージョン管理で逃げてしまえばよかったと思う。
開発中は tag を付けていないので、go.mod に書くときは commit-id を元にした pseudo-version を使っている。

 

入れ子にしたのはこれだけではなかった。
クライアント側も gRPC にアクセスする部分だけ go.mod を別にしてしまったのだ。
gRPC のテスト用クライアントアプリを別に作ったので、gRPC アクセス部分だけ使い回そうとしたのだ。

そんなこんなして、面倒な構成になってしまった。
実際はこんなにシンプルではなく、もっと大量に import している。

image

こうなったとき、go.mod をどう書いて良いのかが結局分からなかった。
go mod tidy してもエラーが出て、いろいろ書き換えても解消できなくなったのだ。 replace を使ってローカルディレクトリを参照したのも原因かもしれない。

結局あきらめて、クライアントアプリの grpcアクセス部分は go.mod を削除して単にクライアントアプリの 1パッケージとして使うことにした。

image


悔しいからいうわけではないが、この構成に変更してよかったと思う。
そもそも、テストアプリはテストアプリに過ぎないのだから、そのために苦労しすぎるのは馬鹿らしい。
それに、grpc の部分を本格的に別のところでも使いたいならクライアントアプリとは別リポジトリにすべきだろう。サーバが複数あることは考えにくいからセットにしたが、クライアントは種類が複数あっても変ではないのだ。

 

だから構成を変更したことに対しては文句がないのだが、その理由が「できなかった」というのは納得がいかないものだ。
できるけどやらない、くらいにしておきたい。

というわけで、入れ子になった go.mod 環境を作って試していこう(本題)。


その前に、用語の整理をしたい。
パッケージは goファイルの中に package として書くのでわかるのだが、モジュールとかバージョンになるとちゃんと理解できていない。今回だって「go.mod の入れ子」と書いているが、正式な名称じゃないと思う。

Modules, packages, and versions
https://go.dev/ref/mod#modules-overview

モジュール

これは package なんかの集合で、識別はモジュールパスで行われる。
モジュールパスは go.mod に依存関係などと一緒に書く。
モジュールルートディレクトリは go.mod があるディレクトリ。
メインモジュールはちょっと違って、go コマンドを実行したディレクトリに含まれているモジュールを指す。つまりルートディレクトリの下にあるサブディレクトリで go コマンドを実行しても、そこに go.mod が含まれていなければ上にたどって直近にある go.mod のモジュールがメインモジュールになるということだ。

git で管理していて普通に作るなら、モジュールパスは git のリポジトリと同じにするだろう。 git 以外でも同じようなものだろう。
ローカルファイルでも悪くはないと思うのだが、どうなんだろうね。


というわけで、ローカルディレクトリで試していこう。

$ go version
go version go1.18.1 linux/amd64

最近は GOPATH を使わなくてもよくなりつつあるようだが、よくわからんので GOPATH を使う。

$ export GOPATH=`pwd`
$ mkdir bin src
$ cd src
$ mkdir mod1 mod2
$ cd mod1
$ go mod init
$ cd ../mod2
$ go mod init
$ cd ..

これで mod1 と mod2 というモジュールができた。
モジュールパスも mod1 と mod2。

なにか import させて go.mod を賑やかにしたいので、昔作った gogo-test1 を持ってこよう。

$ cd mod1
$ go get github.com/hirokuma/gogo-test1
go: downloading github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
go: github.com/hirokuma/gogo-test1@v0.0.0-20211121014212-382e4677bbfb requires
        github.com/hirokuma/yoshio@v0.0.0: reading github.com/hirokuma/yoshio/go.mod at revision v0.0.0: git ls-remote -q origin in /home/xxxx/golang/pkg/mod/cache/vcs/301146d5f0494aad6678724d644f7711fa7ff362d0bc8683a33b7d297ca8e2c3: exit status 128:
        remote: Repository not found.
        fatal: repository 'https://github.com/hirokuma/yoshio/' not found

なんだこりゃ?と思ったが、 gogo-test1 の go.mod で require github.com/hirokuma/yoshio にしてて、それを replace で github.com/hirokuma/gogo-test2 にしているだけだ。

gogo-test1 を clone して go mod tidy してもエラーにならない。
もしかして replace って import すると使えなくなったりするんだろうか。

依存関係がない gogo-test4 はすんなり go get できた。

$ go get github.com/hirokuma/gogo-test4

go.mod

module mod1

go 1.18

require github.com/hirokuma/gogo-test4 v0.1.0

 

main.go

package main

import (
    "fmt"

    gogo "github.com/hirokuma/gogo-test4"
)

func main() {
    gogo.SetValue(3)
    fmt.Printf("%v\n", gogo.GetValue())
}

$ go run .
3


hirokuma/gogo-test1 がダメな理由だが、なんとなく tag で付けた v3 もよくない感じがする。
gogo-test1/go.mod にあった replace をこちらにも追加したのだが、これはダメだった。

$ go mod tidy
go: errors parsing go.mod:
/home/ueno/golang/src/mod1/go.mod:7: no matching versions for query "v3"

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae

require github.com/hirokuma/gogo-test1 v3

 

こっちだと go mod tidy できた。

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb

 

tag に v0.0.3 という名前で追加しても成功したので、 x.y.z 系の tag を付けておくのが無難ということか。
本題とは関係ないのに時間がかかってしまった。


というところで気付いたが、ローカルディレクトリだとバージョンも何もないので意味が無いのでは・・・?

とりあえずやっておこう。

src/
  mod1/
    go.mod
    main.go
  mod2/
    go.mod
    gogo.go

 

mod2/go.mod

module mod2

go 1.18

mod2/gogo.go

package mod2

import "fmt"

func Gogo() {
    fmt.Printf("mod2 GoGo!\n")
}

 

mod1/go.mod

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae
replace mod2 => ../mod2

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
require mod2 v0.0.0

mod1/main.go

package main

import (
    "fmt"
    "mod2"

    "github.com/hirokuma/gogo-test1/gogo"
)

func main() {
    fmt.Printf("gogo-test1!\n")
    gogo.Gogo()
    mod2.Gogo()
}

$ cd mod1
$ go run .
gogo-test1!
gogo!
mod2 GoGo!

mod2 は replace するから require しなくてもよいかと思ったが、それはダメだった。


ここで、mod2 の下に modmod2 を追加する。

image

 

mod2/modmod2/gogogo.go

package modmod2

import "fmt"

func Gogogo() {
    fmt.Printf("modmod2 GoGo!\n")
}

そして mod1 で使う。

 

mod1/go.mod

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae
replace mod2 => ../mod2
replace modmod2 => ../mod2/modmod2

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
require mod2 v0.0.0
require modmod2 v0.0.0

mod1/main.go

package main

import (
    "fmt"
    "mod2"
    "modmod2"

    "github.com/hirokuma/gogo-test1/gogo"
)

func main() {
    fmt.Printf("gogo-test1!\n")
    gogo.Gogo()
    mod2.Gogo()
    modmod2.Gogogo()
}

$ go run .
gogo-test1!
gogo!
mod2 GoGo!
modmod2 GoGo!

 

なんというか、これだと単にディレクトリの位置が違うというだけで、入れ子になっているのは関係ないことになる。

バージョンのことを除けば、go.mod が入れ子になろうと関係がないということかしら。
いつも親?のモジュール名のサブディレクトリっぽいモジュール名を付けていた(今回なら mod2/modmod2 みたいな)ので go.mod がないときと同じように扱っていたのだが、全然違うモジュール名を付けていてもなんとかなったのかもしれない(どうやってリポジトリと紐付けるのかは知らんが)。

mod2/gogo.go から modmod2.Gogogo() を呼ぶこともできた。

package mod2

import (
    "fmt"
    "modmod2"
)

func Gogo() {
    fmt.Printf("mod2 GoGo!\n")
    modmod2.Gogogo()
}

こちらは mod2/go.mod に追加せずにできた。

mod1/go.mod のように追加しなければならないかと思ったのだが、そこはサブディレクトリという特権なのか?
ディレクトリ名を mod2/modmod2 から mod2/modmodmod2 に変更だけしてみたら、mod1/go.mod の replace したパスが存在しないというエラーになった。

$ go run .
../mod2/gogo.go:5:2: modmod2@v0.0.0: replacement directory ../mod2/modmod2 does not exist

mod1/go.mod の replace を ../mod2/modmodmod2 に変更すると go run できた。
なんで mod2/gogo.go は変更しなくてよかったのだろう?
キャッシュかもしれんと考えて各 go.mod があるディレクトリで go clean -modcache と go clean -cache を実行したが変わらなかった。

まあ、やっぱりサブディレクトリの特権ということか。


そう考えると、あまりサブディレクトリに go.mod を持つ利点よりも、バージョン管理が難しくなる欠点が目立ちそうなので避けたい気もする。

最初に書いた proto のディレクトリに go.mod を付けたとしても、tag をディレクトリごとに付けられるわけでもないので、やるなら v0.0.1-proto みたいにサフィックスを付けるとかになるんだろうか。

バージョンの付け方は go.dev に説明がありそうだったが・・・読む気にならん。

Minimal version selection (MVS)
https://go.dev/ref/mod#minimal-version-selection

 

golang の言語仕様自体は結構シンプルな印象を受けているのだが、 go.mod のことで悩む時間が多いのがきついところだ。なんとなく、バージョン管理にまで口を出す傲慢な言語、っていうイメージになってしまった。まあ Java が出てきたときも「ディレクトリが言語に合わせないといけないなんて!」と思ったので今さらかもしれん。オープンソースとはいえ開発主体があるならその意向に沿ってしまうのは仕方ないだろう。嫌なら自分で開発すれば良いだけなのだから。

 

最後の方になって役に立ちそうな記事を見つけた。

Go のモジュール管理【バージョン 1.17 改訂版】
https://zenn.dev/spiegel/articles/20210223-go-module-aware-mode

ありがたや。