なんとなく自宅学習しようと思って、丸善出版の『プログラミング言語Go』を買ってみました。
(正確には旦那様に買ってもらった、ですが)
せっかくなので、感想とか練習問題の解答とかを書いて行こうと思います。
最後まで続くといいですねぇ。。。
Goのインストールや開発環境の構築は終わっているものとして話を進めます。
ちなみに私は Atom エディタとターミナルで学習してます。
もくじ
1.1 ハロー、ワールド
1.2 コマンドライン引数
1.3 重複した行を見つける
1.4 GIFアニメーション
1.5 URLから取得
1.6 URLからの並列な取得
1.7 ウェブサーバ
1.8 残りの項目
1.1 ハロー、ワールド
ご存知、ハローワールド。
*書籍がカタカナで書いているので、それに習ってます。
以前旦那様がハローワークを見て、「あぁ、ハローワールドがあるんだねぇ」とつぶやいたくらい
エンジニア的にはお馴染みのワードですね。
細かいことはあとから説明があると思うので、とにかく本に書いてあるコードを写経します。
1 2 3 4 5 6 |
package main import "fmt" func main() { fmt.Println("Hello, 世界") } |
「Hello, 世界」って斬新だなぁ、とか思ったりもしますが、日本語版の書籍だからですかね。
そんなこんなで実行してみましょう。
このソースの動作確認をするには以下のコマンドを実行します。
1 |
$ go run helloworld.go |
Go言語はコンパイル言語なので、指定されたソースファイルをコンパイルして、ライブラリとリンクして、実行ファイルを生成して、それを実行する、というのを run は一度にやってくれるようです。
もちろんコンパイルとリンクをして実行ファイルを作る方法もあって、以下のコマンドを実行します。
1 |
$ go build helloworld.go |
そうすると同じディレクトリに「helloworld」というファイルが作られるので、これを実行するには
1 |
$ go ./helloworld |
としてあげます。
run の場合も、build した実行ファイルから実行した場合も
1 |
Hello, 世界 |
と表示されるはずです。
Go言語は Unicode を扱うので、日本語も化けることなくきちんと表示されますねー。
この章は練習問題はないので次へ!
1.2 コマンドライン引数
この章ではコマンドライン引数の取り方のチュートリアルになっています。
コマンドライン引数は os.Args
取得できます。
os.Args
は文字列のスライスです。
スライスはここでは動的に大きさを決めることができる配列だと思っておけばいいでしょう。
スライスの要素にアクセスするには、os.Args[0]
と言ったようにJavaなどの他の言語と同じように添字を指定しればOKです。
0番目の要素にはコマンド自身の名前が入ってきて、それ以降の要素がコマンドライン引数になります。
まあ、、、スライスの説明はあとの章で詳しく出てくるようなので、ここではコマンドライン引数の取り方を覚えておけばいいと思います。
書籍に載っているソースコードはここで記載してもしょうがないと思うので、省略します。
というわけで練習問題の解答を…(間違ってても苦情は受け付けません…w)
練習問題1.1
コマンド名も表示されるようにするという問題なので、単純に for のカウンタを 0 からにしてみました。単純すぎ…?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import ( "fmt" "os" ) func main() { var s, sep string for i := 0; i < len(os.Args); i++ { s += sep + os.Args[i] sep = " " } fmt.Println(s) } |
練習問題1.2
個々の引数のインデックスと値を1行毎に表示する、ということなので、単純に fmt.Println()
で index と arg を表示するように修正しました。
不要になったソースも削除しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
package main import ( "fmt" "os" ) func main() { for index, arg := range os.Args[1:] { fmt.Println(index, arg) } } |
練習問題1.3
ベンチマークテストをしろってことなのですが、必要となる知識はこのあとの説明になるのようなので、今回は飛ばします…
知識を得たら再度チャレンジするということにします(汗)
チュートリアルだからなのか、変数の定義の仕方とか、メソッドの使い方とか、そのへんはあまり言及されていないので、ちょっとわからなくなることがありますが、とりあえずいまのところは、そんなものなんだろう、程度にとどめて先に進めることにします。
1.3 重複した行を見つける
この節では、if文、for文、map、標準入力からの読み込みやファイル入出力などの使い方について説明されています。
C言語でお馴染みのfprintfに似た、Fprintfなんかも出てきて、書式指定子なども出てきます。
Go言語ではヴァーブ(verb)って呼ぶようですね。
詳しい説明は書籍を読んでもらうこととして、練習問題をやってみたいと思います。
練習問題1.4
ファイル読み込みで、重複した行のそれぞれが含まれていた全てのファイルの名前を表示するようにする。
私はファイル名の保存用にもう一つ map を追加して、行をキーとして、さらに map ファイル名を追加していく、という流れにしました。
最後にスラッシュ区切りでファイル名を並べた文字列を作成して、重複行と一緒に表示しています。
多重の map にしたので初期化の仕方で手間取ったりもしましたが、一応こんな感じでいいのかな、と…w
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package main import ( "bufio" "fmt" "os" ) func main() { filenames := make(map[string]map[string]int) counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts, filenames) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, "dup2: %v\n", err) continue } countLines(f, counts, filenames) f.Close() } } for line, n := range counts { if n > 1 { names, sep := "", "" for name := range filenames[line] { names += sep + name sep = "/" } fmt.Printf("%d\t%s\t%s\n", n, line, names) } } } func countLines(f *os.File, counts map[string]int, filenames map[string]map[string]int) { input := bufio.NewScanner(f) for input.Scan() { str := input.Text() counts[str]++ if filenames != nil { if filenames[str] == nil { filenames[str] = make(map[string]int) } filenames[str][f.Name()]++ } } } |
1.4 GIFアニメーション
この節ではGo言語の標準画像パッケージの基本的な使い方のチュートリアルになっています。
リサージュ図形というGIFアニメーションを作成するのですが、動いた時はちょっと感動しますw
ここでは、const宣言や構造体型なども出てきますが、詳しくはあとの章で出てくるので、ここではさらっとこんなものもあるという程度にど止めておくのがいいかと思います。
では、練習問題の解答を…
練習問題1.5
リサージュプログラムの背景を黒として、線を緑になるように修正する、というものです。
書籍にRGBでのカラーの作成方法は載っています。緑なので、#00FF00 で作っています。
paletteの0番目が背景色、1番目が線色なので、その順番に黒、緑と指定しています。
修正点はそこだけだったと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package main import ( "image" "image/color" "image/gif" "io" "math" "math/rand" "os" "time" ) var palette = []color.Color{color.Black, color.RGBA{0x00, 0xff, 0x00, 0xff}} const ( whiteIndex = 0 blackIndex = 1 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) lissajous(os.Stdout) } func lissajous(out io.Writer) { const ( cycles = 5 res = 0.001 size = 100 nframes = 64 delay = 8 ) freq := rand.Float64() * 3.0 anim := gif.GIF{LoopCount: nframes} phase := 0.0 for i := 0; i < nframes; i++ { rect := image.Rect(0, 0, 2*size+1, 2*size+1) img := image.NewPaletted(rect, palette) for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex) } phase += 1.0 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) } |
練習問題1.6
リサージュプログラムを修正して、paletteに色をもっと追加して、複数の色で画像を生成しなさい、というものです。
paletteには赤、青、緑を追加しています。
SetColorIndex の代3引数を変更して、ということだったので、paletteの1〜3の色を使って順に色が変わるようにしてみました。
これでいいのかな…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package main import ( "image" "image/color" "image/gif" "io" "math" "math/rand" "os" "time" ) var palette = []color.Color{color.White, color.RGBA{0xff, 0x00, 0x00, 0xff}, color.RGBA{0x00, 0xff, 0x00, 0xff}, color.RGBA{0x00, 0x00, 0xff, 0xff}} const ( whiteIndex = 0 blackIndex = 1 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) lissajous(os.Stdout) } func lissajous(out io.Writer) { const ( cycles = 5 res = 0.001 size = 100 nframes = 64 delay = 8 ) freq := rand.Float64() * 3.0 anim := gif.GIF{LoopCount: nframes} phase := 0.0 for i := 0; i < nframes; i++ { rect := image.Rect(0, 0, 2*size+1, 2*size+1) img := image.NewPaletted(rect, palette) for t := 0.0; t < cycles*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), uint8(i)) } phase += 1.0 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) } |
1.5 URLから取得
この節ではHTTPを使って情報を取得する簡単なチュートリアルになっています。
引数で渡されたURLからHTMLを取得する、といったものです。
http.Get(url)で取得できちゃうので、すごく簡単ですね。
というわけで、練習問題を…。
練習問題1.7
ioutil.ReadAll
の代わりに io.Copy
を使いなさい、というものです。
io.Copy
の使い方とかいろいろ調べたりして時間がかかりましたが、問題をきちんと読んでからやれば簡単でした…。
基本的には ioutil.ReadAll
を io.Copy
に置き換えただけなのですが、io.Copy
の出力先を os.Stdout
にしてあげればOKです。
エラーは取らないといけないので、戻り値は _, err
にする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main import ( "fmt" "io" "net/http" "os" ) func main() { for _, url := range os.Args[1:] { resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v¥n", err) os.Exit(1) } _, err = io.Copy(os.Stdout, resp.Body) resp.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } fmt.Printf("%v", os.Stdin) } } |
練習問題1.8
コマンドライン引数で渡される url に http://がなければ追加する、というものです。
strings.HasPrefix が使えるというヒントが載っているので、そちらを使ってみます。
引数を取得したところで、http:// があるかないか判定をして、なければ追加するようにしています。
ハードコードしてますが、許してください…w
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package main import ( "fmt" "io" "net/http" "os" "strings" ) func main() { for _, url := range os.Args[1:] { if !strings.HasPrefix(url, "http://") { url = "http://" + url } resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v¥n", err) os.Exit(1) } _, err = io.Copy(os.Stdout, resp.Body) resp.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } fmt.Printf("%v", os.Stdin) } } |
練習問題1.9
さて、最後の練習問題。この節は問題が多くて辛いですw
resp.Status に設定されている HTTP ステータスコードも表示するように、というものです。
何か問題の意味を履き違えているかもしませんが、とりあえず表示する処理を最後に追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package main import ( "fmt" "io" "net/http" "os" "strings" ) func main() { for _, url := range os.Args[1:] { if !strings.HasPrefix(url, "http://") { url = "http://" + url } resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v/n", err) os.Exit(1) } _, err = io.Copy(os.Stdout, resp.Body) resp.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n status: %s\n", url, err, resp.Status) os.Exit(1) } fmt.Printf("%v\n%s\n", os.Stdin, resp.Status) } } |
1.6 URLからの並列な取得
ここでは並列プログラミングについてのチュートリアルになります。
Go言語の並列処理を行うゴルーチンとチャネルについて簡単に触れられていますが、詳しくはあとの章で説明されるようなので、ここではこんなふうに使えるんだ、くらいにとどめておくのがいいと思います。
練習問題1.10
fetchall プログラムの出力をファイルに保存するように修正しなさい、というものです。
特にヒントも書いていないので、file.Write
を使ってみましたが、もっと簡単に書ける方法はないのかしら…。
これで一応、同じ内容が log.txt というファイルに保存されるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
package main import ( "fmt" "io" "io/ioutil" "net/http" "os" "time" ) func main() { start := time.Now() ch := make(chan string) for _, url := range os.Args[1:] { go fetch(url, ch) } for range os.Args[1:] { fileWrite(([]byte)(<-ch)) } content := fmt.Sprintf("%.2fs elapsed", time.Since(start).Seconds()) fileWrite(([]byte)(content)) } func fetch(url string, ch chan<- string) { start := time.Now() resp, err := http.Get(url) if err != nil { ch <- fmt.Sprint(err) return } nbytes, err := io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() if err != nil { ch <- fmt.Sprintf("while reading %s: %v", url, err) return } secs := time.Since(start).Seconds() ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) } func fileWrite(content []byte) { file, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { fmt.Printf("file open: %v", err) return } defer file.Close() file.Write(([]byte)(content)) file.Write(([]byte)("\n")) } |
1.7 ウェブサーバ
簡単なウェブサーバの作り方のチュートリアルです。
なんか、すごく少ないコード量で動いてしまうので驚きました。
バックグランドでプログラムを起動させる方法が載っているので、以下のコマンドのように &
を付けて起動します。
1 |
$ go run server1/main.go & |
この後にサーバーを終了しないといけないの、その手順を。(旦那様には常識だって言われたけど…)
まず、起動時に以下のような数字が表示されます。これはプロセス番号になっています。
1 |
[1] 13710 |
色々動かしたあとですでに起動時のログが見つからない、という場合は以下のコマンドを打ってください。
1 |
ps |
そうすると、現在起動されているプロセスの一覧が表示されます。
1 2 3 4 |
PID TTY TIME CMD 419 ttys000 0:00.64 -bash 10333 ttys001 0:00.08 /bin/bash --login 13710 ttys001 0:00.19 go run server1/main.go |
もっとたくさんあるかもしれませんが、一覧の中から自分が起動したアプリがあるはずなので、先頭に記載されている PID の部分がプロセス番号になります。
で、そのプロセス番号を使って、kill します。
1 |
kill 13710 |
これだけだとまだlocalhost:8000にアクセスできてしまうので、次のコマンドを打って、8000ポートを使っているプロセスを探します。
1 |
lsof -i:8000 |
8000ポートを使っているプロセスの一覧が表示されます。
1 2 |
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME main 13713 user_name 5u IPv4 0xa354a7c2d7be9639 0t0 TCP localhost:irdmi (LISTEN) |
ここで main と書いてあるのが、今起動しているプログラムで、PID はプロセス番号になります。
なので、このプロセス番号を使って再度 kill します。
1 |
kill 13713 |
これでようやく全部のプロセスが終了するので、改めてプログラムを起動することができます。
が、、、面倒なので、バックグランドで起動させずに通常起動させて、Ctrl + C
で終了させたほうがラクだと思います。
書籍では1.6 URLからの取得で作成した fetch プログラムと合わせて使うためにバックグランドで起動しているようですが、もうひとつターミナルを起動すればいいだけなので、通常起動のほうがなにかと便利かと思います。たぶん…。
練習問題1.12
文中でリエージュ図形と組み合わせるプログラムを書くのですが、それの修正になります。
リエージュ図形のソース部分は 1.4 GIFアニメーション で作ったものを流用しています。
パラメータを取得して、それを使ってリエージュ図形を変える、という感じです。
cyclesをパラメータで取得して、lissajous()に渡すように変更しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
package main import ( "image" "image/color" "image/gif" "io" "log" "math" "math/rand" "net/http" "strconv" ) func main() { handler := func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { log.Print(err) } cycles := 5 for k, v := range r.Form { if k == "cycles" { cycles, _ = strconv.Atoi(v[0]) } } lissajous(w, cycles) } http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe("localhost:8000", nil)) } var palette = []color.Color{color.White, color.Black} const ( whiteIndex = 0 blackIndex = 1 ) func lissajous(out io.Writer, cycles int) { const ( // cycles = 5 res = 0.001 size = 100 nframes = 64 delay = 8 ) freq := rand.Float64() * 3.0 anim := gif.GIF{LoopCount: nframes} phase := 0.0 for i := 0; i < nframes; i++ { rect := image.Rect(0, 0, 2*size+1, 2*size+1) img := image.NewPaletted(rect, palette) for t := 0.0; t < float64(cycles)*2*math.Pi; t += res { x := math.Sin(t) y := math.Sin(t*freq + phase) img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex) } phase++ anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) } |
1.8 残りの項目
この節はswitch、名前付き型、ポインタ、メソッドとインタフェース、パッケージ、コメントについての簡単な説明になっています。
基本的には読み物で、ソースコードも記載されていますが、実際に書いて動かく必要はないかな、という感じです。
どれもこのあとの章で詳しく説明があると思うので、さらっと流してしまうのがいいでしょう。
この章は練習問題はないので、ここまで!
ようやくチュートリアルが終わりました…。地味に難しかったです、練習問題(汗)
さて、次は第2章 プログラム構造です。変数やパッケージ、スコープなどの説明が出てくるようです。
練習問題もちょくちょくあるようなので、がんばります…w
当たり前ですが細かい説明は書籍を読んでくださいね。