ポインタが分からない②
ポインタのことがやっぱり分からない。
どうしても納得いかないので、もうちょっと良く考えてみることにした。
以下のサンプルコードを動かして色々試してみると、どうも難しく考えるからいけないんだと思うようになった。
単なるルールとして、「ポインタを使うと同じものを指す」とだけ覚えておけば別に困らない気がしてきた。
package main import ( "fmt" ) type Point struct { x int y int } func main() { var p = &Point{10, 20} var P = Point{3, 6} // これは、qがpそのものであることを直感的に示している。 q := p // こちらも、(PとQは別物であることを前提に)、QがPそのものを指し示すよう指示しているように見える。 Q := &P // これは、rとpが異なるものであることを直感的に示している。 r := *p // ところが、これが、R !== P であることがわからない。 R := P // つまり、この式でR === Pであるという前提を暗に抱いているといえる。 // よって、「ポインタを代入しない限り、別物」から考え始めるのが良い。 // sにはqのポインタを代入するので、s === q であることに疑いはない。 s := &q // これは、p自体を操作しているようにしか見えない。 (*p).x = 12 S := *p; S.x = -100 // これをやってもpは一切影響を受けない。 // これもまた、P自体を操作しているようにしか見えない。 (&P).x = 45 r.y = 17 // rにはポインタを取らなかったので、ここでpは影響を受けない。 fmt.Println(p, q, r, *s) // &{12 20} &{12 20} {10 17} &{12 20} fmt.Println(P, Q, R) // {45 6} &{45 6} {3 6} }
どうもわたしが理解できていなかったことは、
- &や*を付けると同じものや違うものを指すと誤解していた
- そうではなくて、代入すると別々に分けられるタイミングが来るようだ
- しかし、アドレス演算子を使ってポインタを代入すると、左辺と右辺が同一であることを示せる
- (*p)や(&P)を使ってある変数そのものを操作できることから、やっぱり演算子自体にオブジェクトを分別する役割は無くて、代入に起因するとしか思えない。
ここまで考えるうちにつらくなってきて、要は「ポインタを使うと同じものを指す」とだけ覚えておけば良いんじゃないかと思うようになった。
元々、変数をポインタ型で宣言してしまうと、今度は「それ自身」を取り出す術がなくなるので、「*」という演算子が用意されている、と。
(まだよくわかってない・・)
ポインタがわからない①
わたしがプログラミングを初めて覚えたのはJavaだったけど、以来、JavaScriptやPythonなど、ポインタを使わない言語しか経験が無い。
だから、Goの学習で一番苦戦しているのが「ポインタ」である。
まず、何がわからないのかもわからない。
暗中模索というか、完全に手探りで色々試してみるうちに、
- 星とか&とかからして何がなにやらという状態。「変数は箱、ポインタはメモリの番地」とか言われてもさっぱり理解不能(苦笑)
- ローカル変数の宣言に*を付けるべきか?
- 関数のレシーバーないし引数に*を付けるべきか?
- 関数の戻り値に*を付けるべきか?
- 変数の頭にも*を付けて利用できることを知って、頭がパァ?
やっとここまで進んできたという状態である。
自分の理解のためにも、ここまで理解できたことをメモしておくことにする。
アドレス演算子とポインタの間接参照
まず、&と*の呼び名からして識別困難だった。(記号なのでぐーぐらびりてーが低いし)
Goでこれらを使って自分なりに説明してみたのが以下。
var p Point // pはPoint型 var q *Point // qはPointのポインタ型 (普通の型とポインタ型は区別される) p = Point{10, 20} // pにPointのオブジェクトを作って代入 q = &p // qに、アドレス演算子&を使ってpのポインタを作って代入 r := *q // 間接演算子*を使って、...代入
&のことを「アドレス演算子」、*のことを「間接演算子」と呼ぶらしい。
Goのドキュメントでは「間接演算子」という言葉は出てこなかったので、MSDNから言葉を拝借した。
*T型(ポインタ)であるxをオペランドとするポインタの間接参照*xは、xによって指されるT型の値を表します。
代入したとき
ではこの後、qのフィールド(q.x, q.y)を書き換えるとp, rには影響するだろうか?
var p Point var q *Point p = Point{10, 20} q = &p r := *q q.x, q.y = 7, 8 fmt.Println(p, q, r) // ここで何が出力されるか?
わたしは「{7 8} {7, 8} {7, 8}」が出力されると思ったが、実際は
{7 8} &{7 8} {10 20}
何と!? 間接参照すると本体を複製して代入するようだ。
それと、ポインタと普通の型は出力の表記も区別され、ポインタだと先頭に「&」がついている。
関数を通したとき
次に、関数を定義するときに引数や戻り値にポインタ型を指定するとどうなるのか確かめた。
point := func(p Point) Point { return p } point2 := func(p *Point) *Point { return p } point3 := func(p *Point) Point { return *p } L := Point{1, 2} M := point(L) N := point2(&M) O := point3(N) M.x, M.y = -5, -6 fmt.Println(L, M, N, O) // ここで何が出力されるか?
これを実行すると何が表示されるだろうか?
まず、関数を定義すると引数はコピーされるという話は知っていたので、LとMは別物でMとNが同じものを指すだろうと思った。
関数パラメータは値渡しか?
C言語系の言語と同様にGo言語ではすべて値渡しです。すなわち関数は常に値をパラメータに代入する代入ステートメントがあるかのように、渡されたもののコピーを受け取ります。たとえば関数にint型の値を渡すとint型のコピーが作成され、ポインタの値を渡すとポインタのコピーが作成されますが、ポインタが指し示すデータがコピーされるのではありません。
なので、わたしは「{1 2} {-5 -6} {-5 -6} {-5 -6}」が出力されると思っていたが、実際は
{1 2} {-5 -6} &{-5 -6} {1 2}
やはり間接演算子でポインタの本体の方を引っ張り出すと複製するルールは変わらないようだ。
それと、ポインタ型の変数は先頭に「&」をつけて出力される。
Goのflagパッケージを使ってサブコマンドを作る
GoでPythonのargparseみたいなサブコマンドを処理したい場合はflag.NewFlagSetというやつを使えば良いらしい。
Goでのやり方はこのページでドン!なんだけど。忘れないようにメモ。
Golang: Implementing subcommands for command line applications · Software adventures and thoughts
FlagSetを別々に作ってParseする
サブコマンドを作る場合はflag.NewFlagSetを呼び出すとFlagSetが返ってくるのでそれぞれ引数、オプションの解析をすれば良い。
今回は
- help: ヘルプを表示する. -vオプションでより詳しく表示する.
- money: 入力したふたつの通貨の為替レートを表示する.
上記2つのサブコマンドを持つコマンドを作る。
helpコマンドにはオプションを付けるので、以下のようにする。
help := flag.NewFlagSet("help", flag.ExitOnError) verbosity := help.Int("verbosity", 0, "") // または // var verbosity int // help.IntVar(&verbosity, "verbosity", 0, "") money := flag.NewFlagSet("money", flag.ExitOnError)
必須引数はFlagSet.Args()で参照できる
moneyコマンドは通貨をふたつ入力するので、以下のようにする。
if len(money.Args()) < 2 { // e.g. $ <cmd> money USD fmt.Println("エラー: 通貨をふたつ入力してください") return }
verbosityの数でメッセージの詳細度を上げたい
例えば「-v」よりも「-vv」、「-vvv」の方が詳細なメッセージを表示可能としたい。
最初、安易に以下のようにやってしまったが、
help := flag.NewFlagSet("help", flag.ExitOnError) v1 := help.Bool("v", false, "") v2 := help.Bool("vv", false, "") v3 := help.Bool("vvv", false, "")
これだと、「-vvvv」などが解析エラーになってしまう。
なので、help.Args()をループで回して最大値を取るよう書き直した(以下)。
// 引数からメッセージ詳細度を算出する。算出に使わなかった引数は戻り値のスライスに返却する。 var regex = regexp.MustCompile("^-v+$") func Verbosity(xs []string) (int, []string) { var v float64 others := make([]string, 0, len(xs)) for _, x := range xs { if regex.MatchString(x) { v = math.Max(v, float64(strings.Count(x, "v"))) } else { others = append(others, x) } } return int(v), others } help := flag.NewFlagSet("help", flag.ExitOnError) var verbosity int help.IntVar(&verbosity, "verbosity", 0, "") v, others := Verbosity(os.Args[2:]) help.Parse(others) if verbosity < v { // e.g. <cmd> help -verbosity=0 -vvv help.Set("verbosity", strconv.Itoa(v)) }
サンプルコード
プログラム全体は以下のようになった。
package main import ( "flag" "fmt" "math" "os" "regexp" "strconv" "strings" ) var regex = regexp.MustCompile("^-v+$") // 引数からメッセージ詳細度を算出する。算出に使わなかった引数は戻り値のスライスに返却する。 func Verbosity(xs []string) (int, []string) { var v float64 others := make([]string, 0, len(xs)) for _, x := range xs { if regex.MatchString(x) { v = math.Max(v, float64(strings.Count(x, "v"))) } else { others = append(others, x) } } return int(v), others } func main() { help := flag.NewFlagSet( "help", flag.ExitOnError) var verbosity int help.IntVar(&verbosity, "verbosity", 0, "") money := flag.NewFlagSet("money", flag.ExitOnError) if len(os.Args) == 1 { fmt.Println("usage: argparse <command> [<args>]") fmt.Println("\thelp: ヘルプをプリントします") fmt.Println("\tmoney: 為替レートを調べます") return } switch os.Args[1] { case "help": v, others := Verbosity(os.Args[2:]) help.Parse(others) if verbosity < v { // e.g. <cmd> help -verbosity=0 -vvv help.Set("verbosity", strconv.Itoa(v)) } case "money": money.Parse(os.Args[2:]) default: fmt.Printf("%q is not valid command.\n", os.Args[1]) os.Exit(2) } if help.Parsed() { message := "Help me." if verbosity == 1 { message = "Help me!" } if verbosity == 2 { message = "Help me!!" } if verbosity >= 3 { message = "HELP ME!!" } fmt.Println(message) } if money.Parsed() { if len(money.Args()) < 2 { // e.g. $ <cmd> money USD fmt.Println("エラー: 通貨をふたつ入力してください") return } from, to := money.Arg(0), money.Arg(1) if from == "" || to == "" { // e.g. $ <cmd> money USD '' fmt.Println("エラー: 通貨をふたつ入力してください") return } fmt.Printf("%v/%v: 100円/$\n", from, to) // 適当 } }
動かすとこのような結果が出る。
$ go run foo.go help -verbosity=0 -vvv # => HELP ME!! $ go run foo.go help -vv # => Help me!! $ go run foo.go money USD JPY # => USD/JPY: 100円/$
参考リンク
ポインタを渡すときはIntVar, XXXVarを使う旨、説明されている。
Go言語のflagパッケージを使う - uragami note
Golang: Implementing subcommands for command line applications · Software adventures and thoughts
ANSIエスケープコードを使ってコンソールアニメーション
『プログラミング言語Go』のスピナー
予約語「go」を使うとメインgoroutineと別のgoroutineで動かせる
『プログラミング言語Go』の「ゴルーチンとチャネル」という章を読んでいて、メインルーチンの処理中にスピナーを回すサンプルコードが出てきたので自分でも試していた。
この、スピナーのコードが以下の通り、1文字出力してはキャリッジリターン「\r」でカーソル位置を戻して上書きを繰り返すものなのだが、これに予約語「go」を付けると別ルーチンで動いてくれる。
func spinner(delay time.Duration) { for { for _, r := range `-\|/` { fmt.Printf("\r%c", r) time.Sleep(delay) } } } func main() { go spinner(100 * time.Millisecond) // 中略: 変数accに処理時間を累積していく fmt.Printf("%v かかりました\n", acc) }
ところが、スピナーが出力する文字列を以下のように変えてしまったら問題が起きた。
func spinner(message string, delay time.Duration) { for { for _, r := range `-\|/` { fmt.Printf("\r%c%v", r, message) time.Sleep(delay) } } } func main() { go spinner("コンピュータ思考中...", 140*time.Millisecond) // 中略: 変数accに処理時間を累積していく fmt.Printf("\r%v かかりました\n", acc) }
スピナーで使った文字列が残ってしまう
これだと、処理が終わるまでは「-コンピュータ思考中...」というメッセージの先頭文字が切り替わり続けているが、終了時に
2.5s かかりました中...
このように、何ともマヌケな表示になってしまう。
これを、
2.5s かかりました
このように改めるにはどうした良いのかわからなかった。
キャリッジリターンを出力するとある程度文字を書き換えられるのだから、同じような特殊文字を出力すれば良いだろうとは思っていたのだが。
ANSIエスケープコード
調べてみると、stackoverflowの記事がヒットし、「ANSIエスケープコード」というものを出力すれば良いらしい。
How can I clear the terminal screen in Go? - Stack Overflow
You could do it with ANSI escape codes:
print("\033[H\033[2J")
そういえば.vimrcや.bashrcを書き換えて、プロンプトやlsで表示される文字色を変えたときに「\033[チョメチョメ」という表記を見かけたなぁと思った。
カーソル移動と文字消去
このページに非常に詳しく書いてあるのだが、カーソル移動や行や文字を消すコードがちゃんと設けられている。
ESC[nG
カーソルを現在の横位置に関係なく左端からnの場所に移動させる。 (左端を1とする。nには整数が入る、省略すると1)ESC[nK
行消去、nを省略、もしくは0を指定した場合、カーソルより後ろを消去、 1を指定するとカーソルより前を消去、2を指定すると行全体を消去となる。
最初のESCは表示文字でないので便宜上このように表示しているが、(中略)
どちらも同じものを表現しているが、8進数を利用した\033で書かれる場合が多いようだ。
上記から、行を消し、その後カーソルを行頭に移動するには以下の出力をしてあげれば良い。
fmt.Printf("\033[2K\033[G")
文字に色をつける
更に、出力する文字の色を変えることも可能らしい。
そういえばAnsibleやgrepコマンドの出力結果には適宜色がついていたなぁと思い出す。
そこで、以下の通りプログラムを書き換えて、終了メッセージの先頭に「[ok] 」というプレフィクスを緑色で表示してみることにした。
fmt.Printf("\033[32m[ok]\033[39m %v かかりました\n", acc)
すると、このように、ちゃんと色がついた。
Goの乱数&time.Durationの算術演算
ランダムに数百ミリ秒スリープしたい
Goで乱数とスリープを組み合わせた処理を書こうとして苦しんだ。
やりたいことは以下なのだが、
- 100〜900ミリ秒スリープする処理を5回繰り返す
- 但し、同じ秒数の待機は一度しか許さない(一回100ミリ秒待ったら、もはや100ミリ秒待ってはいけない)
- 5回繰り返した累積の待機秒数を最後に標準出力に出す。
これは思ったより難しかった。
サンプルコード
まず最初にサンプルコードを示して、ハマりポイントを解説したい。
紆余曲折を経て、以下のコードで期待通りの動きをしてくれた。
package main import ( "fmt" "math/rand" "time" ) func main() { var acc, delay time.Duration rand.Seed(time.Now().UnixNano()) // ハマりポイント1 choices := rand.Perm(9) for _, x := range choices[:5] { // ハマりポイント2 delay = time.Duration((x+1)*100) * time.Millisecond acc += delay time.Sleep(delay) } fmt.Printf("合計: %v", acc) }
ハマりポイント1: ランダムにならない(乱数シードの初期化忘れ)
このプログラムを取り敢えず動く状態にして何度か実行していたら、すべて結果が同じ秒数になってしまうことに気づいた。
これは、乱数のシードを適切に初期化しなかったことが原因らしい(以下の記事を参照)。
Random Seed って言葉を初めて知った! | ちょまど帳
こちらはコメント欄が重要
Goで乱数 - Qiita
乱数シードはナノ秒の方が良さそうです。
(1秒以内に実行されたら、同じ結果になる)
サンプルコードの通り、rand.Seed(time.Now().UnixNano())したらちゃんと結果がばらけてくれた。
ハマりポイント2: time.Durationをうまく扱えなくて苦戦
既に他の記事に書いたことだが、time.Sleepは引数にtime.Durationを取る。
これの算術演算に苦戦した。
delay = time.Duration(100 * (x+1) * time.Millisecond)
これができないので、以下の通り、先にtime.Duration(100 * x)してからtime.Millisecondと乗算する。
time.Duration((x+1)*100) * time.Millisecond
どうも、数値のリテラルと掛け算する場合は暗黙にtime.Duration同士に型を推測してくれるようだが、リテラルではなく変数だとダメみたいだ。
当然、変数accもtime.Durationで宣言しておかないとダメ。
var acc int var delay time.Duration (中略) acc += delay // これはコンパイルエラー // invalid operation: acc += delay (mismatched types int and time.Duration)
両方ともtime.Durationで宣言しておくこと。
もしaccを整数として扱いたい場合は、型をキャストして使う。以下はOK。
fmt.Printf("合計: %v, 整数-> %v", acc, int(acc)+1000) // 合計: 2.8s, 整数-> 2800001000
Goでスリープしようとしてハマった
Pythonでスリープするときは
import time time.sleep(1) # 1秒待つ time.sleep(0.5) # 0.5秒待つ
こうすれば良いが、同じ感覚でGoでスリープしようとしたらうまくいかなかった。
import "time" time.Sleep(1) // 1秒待つ(?)
どうなるかというと、やけに早く戻ってきてしまうのである。
ちなみに、こっちをやるとコンパイルエラーになってしまう。
time.Sleep(0.5) // constant 0.5 truncated to integer
Goのtime.Sleepの引数はDurationという型
https://golang.org/pkg/time/#Sleep
golangのドキュメントを見ると、time.Sleepの引数にはtime.Durationという型を受け付ける。
そして、time.Durationはint64の別名型らしい。
type Duration int64
A Duration represents the elapsed time between two instants as an int64 nanosecond count.
しかもナノ秒が基礎なので、time.Sleep(1)とやってしまうとたった1ナノ秒しか待たないことになる。
どうりで思ったより早く戻ってくるわけだ。
正式なN秒スリープの仕方
N秒スリープするのに正式も自己流も無いと思うが、以下の書き方が安定的である。
time.Sleep(1 * time.Second) // 1秒待つ
先ほど書いた通り、0.5秒待つときにこれはできない。
time.Sleep(0.5 * time.Second) // これはエラー(floatと掛け算できない) constant 0.5 truncated to integer
こうすれば動く。
time.Sleep(500 * time.Millisecond) // 0.5秒待つときはMillisecondと乗算する
ちなみにこれも動く。
var s time.Duration s = 5 time.Sleep(100 * s * time.Millisecond)
しかし、こっちは動かない。int64が掛け算の式に混入するのがダメらしい。
var s int64 // この他、float64もintも試したがダメだった s = 5 time.Sleep(100 * s * time.Millisecond) // invalid operation: 100 * s * time.Millisecond (mismatched types int64 and time.Duration)
どうしてもintと掛け算したい場合はこのように一度整数からDurationを作ってから掛け算する。
var t int t = 5 time.Sleep(time.Duration(t*100) * time.Millisecond) fmt.Println("Hello, world.")
なぜ、こんなことを調べ始めたかというと、「ランダムに数百ミリ秒待つ」という処理を書こうとしてハマったからである。
(続く)
Visual Studio Codeを使ってGoのコードをデバッグする
Goのデバッガ
Goでデバッガを使いたい場合、gdb, delveといったツールは初心者には操作が難しい。
(コマンドラインからbとかlとかpとか入力して、ブレークポイントの設定からやらなければならないので、わたしには厳しかった)
しかし、Visual Studio CodeのGo拡張機能が無料で利用可能なので、これを使うと簡単にデバッグできる。
インストールが必要なもの
以下3つインストールが必要。
- Visual Studio Code
- Visual Studio CodeのGo拡張機能
- delve https://github.com/derekparker/delve
Visuao Studio CodeのGoの拡張機能が裏でdelveを操作することでGUIデバッグ機能を提供しているので、delveも入れる必要があることに注意。
前提条件
インストール手順 (Ubuntu16.04 64ビットデスクトップ版で実施)
Visual Studio Codeをダウンロードします。
$ firefox https://code.visualstudio.com/download
「.deb」のリンクを押します。
(32ビットのパソコンを使っている人は「32bit versions」のページでやって下さい)
そうすると「code_チョメチョメ.deb」のダウンロードの許可を求めるポップアップウインドウが開きます。
ここで「ファイルを保存する」を選びます。
上記debファイルが所定の場所にダウンロードされます。
(わたしの場合は「ダウンロード」配下にファイルが保存されました)
ダウンロードしたdebファイルをインストールします。
まず、debファイルを保存した場所へ移動します。
$ cd ~/ダウンロード
インストールコマンドを実行します。
$ sudo dpkg -i code_1.7.2-1479766213_amd64.deb
Visual Studio Codeが起動できることを確かめます。
$ code
こんな画面が開いたらokです。
Goの拡張機能をインストールします。
これが無いと画面からブレークポイントを設定し、デバッグできません。
Visual Studio Codeの画面左側最下の「拡張機能」のアイコンを押します。
検索ボックスに「Go」と入力すると「Go/Rich Go language support for Visual Studio Code」という候補が出てくるので、それの「インストール」を押します。
Goのプロジェクトを用意します。
$ export GOPATH=/home/echizen/_go $ mkdir -p /home/echizen/_go/src/github.com/oyakata/ $ cd /home/echizen/_go/src/github.com/oyakata/ $ git clone git@github.com:oyakata/tokyo.git $ cd tokyo
delveをインストールします。
https://github.com/derekparker/delve/blob/master/Documentation/installation/linux/install.md
このページの説明に従って、作業を行います。
(今回はgo getでやりました)
$ go get github.com/derekparker/delve/cmd/dlv
(プロンプトが返ってくるまで少し時間がかかります)
ここまで成功すればVisual Studio Codeの画面からデバッグ可能です。
実際にデバッグをやってみます。
Visual Studio Codeの画面を開きます。
(既に動作確認で開いた画面は一度閉じて、開き直しておいた方が良いでしょう)
$ code
画面左最上のファイルのアイコン「エクスプローラー」を押して、「フォルダーを開く」ボタンを押して、プロジェクトディレクトリを開きます。
先ほど用意したプロジェクトを選択し、「OK」を押します。
すると、Visual Studio Codeの画面の左側にプロジェクトの構成が読み込まれるので、デバッグしたいファイルを開いて行番号の隣の赤い●を押すとブレークポイントを設定できます。
画面左の下から2番め「デバッグ」アイコンを押すと、デバッグ実行できます。
初期状態だと「構成がありません」と表示されるので、赤い●が付いている歯車アイコンを押して「Go」を選択して下さい。
あとは、デバッグの右にある▶を押せばデバッグ機能が使えます。
以上
参考リンク
Ubuntu16.04にVisual Studio Codeをインストールする - Desktop Linux のススメ
Visual Studio Codeのすゝめ (Golang寄り) - 気まぐれLinux
Visual Studio CodeによるGo言語のデバッグ | Developers.IO
Google App Engine for Go のローカルサーバでデバッグをする #golang #GAE - Qiita
software installation - How to install Visual Studio Code on Ubuntu? - Ask Ubuntu
Debugging Go Code with GDB - The Go Programming Language