A Tour of Goの解答まとめ【挫折した人がやるべきこと】

  • A Tour of Goの解答が知りたい
  • A Tour of Goが難しくて挫折しそう…

こういった方向けにGoのチュートリアル「A Tour of Go」のExerciseの解答をまとめました。

さらに後半では、A Tour of Goが難しいと感じる人向けにおすすめのGo言語のキャッチアップ方法を書いています。

最後まで読めば効率的にGo言語のキャッチアップができるようになります。

なお、この記事を書いているぼくは現役のGoエンジニアです。

目次

A Tour of Goの解答まとめ

Exercise: Loops and Functions

ニュートン法を用いて平方根の近似値を求めるプログラムを書き、ループを使って更新式を何度も回す問題です。

まず計算を 10 回繰り返してそれぞれの z を表示します。とあるため、こちらからやっていきます。

exercise-loops-and-functions.go
package main

import (
	"fmt"
)

// Sqrt は、ニュートン法を用いて引数 x の平方根を10回の反復で近似する。
// 反復ごとに途中経過を表示する。
func Sqrt(x float64) float64 {
	z := 1.0
	for i := 0; i < 10; i++ {
		z -= (z*z - x) / (2 * z)

		fmt.Printf("%d回目: %v\n", i+1, z)
	}
	return z
}

func main() {
	fmt.Println(Sqrt(2))
}

続いて次に値が変化しなくなった (もしくはごくわずかな変化しかしなくなった) 場合にループを停止させます。 それが 10 回よりも多いか少ないかを確認してください。 x や x/2 のように他の初期推測の値を z に与えてみてください。の解答です。

Go
package main

import (
	"fmt"
	"math"
)

// Sqrt は、ニュートン法を用いて引数 x の平方根を近似する。
// 前回の近似値と次の近似値の差が 1e-10 未満になったら収束とみなし、
// その時点の値と反復回数を文字列形式で返す。
func Sqrt(x float64) string {
	z := 1.0
	i := 1
	for {
		next := z - (z*z-x)/(2*z)
		i++
		if math.Abs(z-next) < 1e-10 {
			return fmt.Sprintf("%d回目: %v", i, next)
		}
		z = next
	}
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(3))
}

学べるポイント

  • forループの基本的な使い方と無限ループの適切な終了
  • math.Abs(z - next) < 1e-10 といった形で「差が十分小さくなったら打ち切る」アプローチ

Exercise: Slices

2次元配列を完成させる問題です。

Goのスライスを使いこなしや型変換の理解を深めるのに役立つ問題だと思います。

exercise-slices.go
package main

import "golang.org/x/tour/pic"

// Pic は、dx×dy のサイズの2次元スライスを生成し、
// (x+y)/2 の値を uint8 に変換して各要素に代入する。
func Pic(dx, dy int) [][]uint8 {
	pic := make([][]uint8, dy)
	for y := range pic {
		pic[y] = make([]uint8, dx)
		for x := range pic[y] {
			pic[y][x] = uint8((x + y) / 2)
		}
	}
	return pic
}

func main() {
	pic.Show(Pic)
}

学べるポイント

  • スライスの動的な作成と初期化(make関数の使用)
  • 多次元スライスの操作方法
  • rangeを使ったスライスの繰り返し処理
  • 型変換の基本(計算結果をuint8に変換)

Exercise: Maps

文字列を単語に分割して、単語ごとの出現回数を集計する問題です。

exercise-maps.go
package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

// WordCount は、文字列 s を空白で区切って単語ごとに集計し、
// 結果を map[string]int で返す。
func WordCount(s string) map[string]int {
	words := strings.Fields(s)
	counts := make(map[string]int)
	for _, w := range words {
		counts[w]++
	}
	return counts
}

func main() {
	wc.Test(WordCount)
}

mapの使い方やfor rangeによるループ処理、標準ライブラリstrings.Fieldsとの連携などを学べる問題ですね。

学べるポイント

  • mapの作成と初期化
  • mapのキーと値の操作
  • 標準ライブラリstringsパッケージの活用

Exercise: Fibonacci closure

フィボナッチ数列を返す関数を実装する問題です。

exercise-fibonacci-closure.go
package main

import "fmt"

// fibonacci は、連続するフィボナッチ数を返すクロージャを生成する。
// a, b は関数内で閉じ込められ(クロージャ)、呼び出しのたびに更新される。
func fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

学べるポイント

  • クロージャの実装
  • 状態を保持する関数の設計

Exercise: Stringers

インターフェースを実装する問題です。

exercise-stringer.go
package main

import "fmt"

// IPAddr は、IPv4アドレスを示す4バイト配列。
type IPAddr [4]byte

// String は、IPAddr を "A.B.C.D" 形式の文字列に変換する。
// これにより、fmt.Stringer インターフェースを満たす。
func (ip IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

学べるポイント

  • インターフェースの実装方法
  • フォーマット文字列(fmt.Sprintfの活用)
  • fmtパッケージのフォーマット仕様の理解

Exercise: Errors

独自のエラー型を作成する問題です。

exercise-errors.go
package main

import (
	"fmt"
	"math"
)

// ErrNegativeSqrt は、負の数の平方根計算が行われたときに返されるエラー型。
type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
	// float64(e) に変換しないと再帰的にError()を呼んでしまう
	return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}

// Sqrt は、ニュートン法を用いて x の平方根を近似する。
// x が負の場合は ErrNegativeSqrt を返す。
func Sqrt(x float64) (float64, error) {
	if x < 0 {
		return 0, ErrNegativeSqrt(x)
	}
	z := 1.0
	for {
		next := z - (z*z-x)/(2*z)
		if math.Abs(z-next) < 1e-10 {
			// 計算成功、エラーなし
			return next, nil
		}
		z = next
	}
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

学べるポイント

  • Goにおけるエラー処理の仕組み
  • カスタム型とインターフェースの関係
  • 無限ループを避けるためのテクニック

Exercise: Readers

バイトストリームの生成を理解し、入出力のための基本インターフェース(io.Reader)を実装する問題です。

exercise-reader.go
package main

import "golang.org/x/tour/reader"

// MyReader は、読み込み要求があったバッファにすべて 'A' を書き込む。
// これにより、io.Readerインターフェースを実装する。
type MyReader struct{}

func (MyReader) Read(p []byte) (int, error) {
	for i := range p {
		p[i] = 'A'
	}
	return len(p), nil
}

func main() {
	reader.Validate(MyReader{})
}

学べるポイント

  • Goの入出力インターフェースの設計
  • バイトスライスの操作
  • io.Readerインターフェースの実装パターン
  • ストリームベースの入出力の概念

Exercise: rot13Reader

既存のReaderを拡張してデータ変換ストリームを実装する問題です。

exercise-rot-reader.go
package main

import (
	"io"
	"os"
	"strings"
)

// rot13Reader は、内部に持つ io.Reader から読み込んだバイト列を
// ROT13変換して返す構造体。
type rot13Reader struct {
	r io.Reader
}

func (rr *rot13Reader) Read(p []byte) (int, error) {
	n, err := rr.r.Read(p)
	if n > 0 {
		for i := 0; i < n; i++ {
			p[i] = rot13(p[i])
		}
	}
	return n, err
}

func rot13(b byte) byte {
	switch {
	case b >= 'A' && b <= 'M':
		b += 13
	case b >= 'N' && b <= 'Z':
		b -= 13
	case b >= 'a' && b <= 'm':
		b += 13
	case b >= 'n' && b <= 'z':
		b -= 13
	}
	return b
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

学べるポイント

  • バイト単位の変換処理
  • 条件分岐を使った文字変換アルゴリズム
  • ioパッケージの活用

Exercise: Images

座標ベースのデータ構造を操作し、Imageインターフェースを実装する問題です。

exercise-images.go
package main

import (
	"image"
	"image/color"

	"golang.org/x/tour/pic"
)

// Image は、image.Image インターフェースを実装する自作型。
// (x, y) の位置に応じて青系のグラデーションを生成する。
type Image struct{}

// ColorModel は、画像の色モデルを返す。
// この問題では RGBA としている。
func (i Image) ColorModel() color.Model {
	return color.RGBAModel
}

// Bounds は、画像の範囲を表す矩形を返す。
// 今回は、(0,0) から (255,255) の大きさの画像を作る。
func (i Image) Bounds() image.Rectangle {
	return image.Rect(0, 0, 255, 255)
}

// At は、(x,y) 位置の色を返す。
// ここでは、R = x, G = y, B=255, A=255(不透明) としている。
func (i Image) At(x, y int) color.Color {
	return color.RGBA{uint8(x), uint8(y), 255, 255}
}

func main() {
	m := Image{}
	pic.ShowImage(m)
}

学べるポイント

  • imageパッケージのインターフェース設計
  • カラーモデルと座標系の扱い方
  • 複数のメソッドを持つインターフェースの実装
  • colorパッケージの使用方法

Exercise: Equivalent Binary Trees

並行処理を使って二分木を走査し、チャネルを使ったデータの送受信を実装する問題です。

exercise-equivalent-binary-trees.go
package main

import (
	"fmt"
	"golang.org/x/tour/tree"
)

// Walk は、ツリーtのすべての値をチャネルchに送信する。
// 送信が完了したらチャネルを閉じる。
func Walk(t *tree.Tree, ch chan int) {
	walk(t, ch)
	close(ch)
}

// walk は、再帰的に二分木を探索し、値をチャンネルに送る。
// 左の子→自分自身→右の子の順で処理する中順走査で実装。
func walk(t *tree.Tree, ch chan int) {
	if t == nil {
		return
	}
	walk(t.Left, ch)
	ch <- t.Value
	walk(t.Right, ch)
}

// Same は、二分木 t1, t2 が同じ値を持つかどうかを判定する。
// 同じ値を同じ順序で持つなら true を返し、そうでなければ false。
func Same(t1, t2 *tree.Tree) bool {
	c1, c2 := make(chan int), make(chan int)
	go Walk(t1, c1)
	go Walk(t2, c2)
	for {
		v1, ok1 := <-c1
		v2, ok2 := <-c2
		if !ok1 && !ok2 {
			return true
		}
		if ok1 != ok2 || v1 != v2 {
			return false
		}
	}
}

func main() {
	ch := make(chan int)
	go Walk(tree.New(1), ch)

	fmt.Println("[Walk 関数のテスト]")
	for i := 0; i < 10; i++ {
		v := <-ch
		fmt.Printf("%d ", v)
	}
	fmt.Println()

	fmt.Println("[Same 関数のテスト]")
	fmt.Println("Same(tree.New(1), tree.New(1)):", Same(tree.New(1), tree.New(1)))
	fmt.Println("Same(tree.New(1), tree.New(2)):", Same(tree.New(1), tree.New(2)))
}

学べるポイント

  • goroutineの基本的な使い方
  • チャネルによるデータ通信
  • 再帰関数と並行処理の組み合わせ

Exercise: Web Crawler

複数のgoroutineと排他制御を用いたWebクローラを実装する問題です。

exercise-web-crawler.go
package main

import (
	"fmt"
	"sync"
)

// Fetcher インターフェースは、指定された URL の本文と
// そのページ内で見つかった URL のスライスを返す。
type Fetcher interface {
	// Fetchは指定したURLの本文と、そのページ上で見つかったURLのスライスを返す。
	Fetch(url string) (body string, urls []string, err error)
}

// Crawl は、fetcher を使って url を再帰的にクロールし、最大深度 depth まで辿る。
// 訪問済みのURLは重複してクロールしないように map と Mutex で管理する。
func Crawl(url string, depth int, fetcher Fetcher) {
	visited := make(map[string]bool)
	var mu sync.Mutex
	var wg sync.WaitGroup
	
	var crawl func(url string, depth int)
	crawl = func(url string, depth int) {
		defer wg.Done()
		if depth <= 0 {
			return
		}
		
		mu.Lock()
		if visited[url] {
			mu.Unlock()
			return
		}
		visited[url] = true
		mu.Unlock()

		body, urls, err := fetcher.Fetch(url)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("found: %s %q\n", url, body)
		
		for _, u := range urls {
			wg.Add(1)
			go crawl(u, depth-1)
		}
	}

	wg.Add(1)
	go crawl(url, depth)
	wg.Wait()
}

func main() {
	Crawl("https://golang.org/", 4, fetcher)
}

// 以下はサンプルとして与えられているfakeFetcher実装

// fakeFetcher はテスト用で、与えられたURLに対して決め打ちの結果を返す。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
	body string
	urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
	if res, ok := f[url]; ok {
		return res.body, res.urls, nil
	}
	return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
	"https://golang.org/": &fakeResult{
		"The Go Programming Language",
		[]string{
			"https://golang.org/pkg/",
			"https://golang.org/cmd/",
		},
	},
	"https://golang.org/pkg/": &fakeResult{
		"Packages",
		[]string{
			"https://golang.org/",
			"https://golang.org/cmd/",
			"https://golang.org/pkg/fmt/",
			"https://golang.org/pkg/os/",
		},
	},
	"https://golang.org/pkg/fmt/": &fakeResult{
		"Package fmt",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
	"https://golang.org/pkg/os/": &fakeResult{
		"Package os",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
}

学べるポイント

  • 複雑な並行処理パターン
  • sync.Mutexによる排他制御
  • sync.WaitGroupによる処理完了の待機

A Tour of Goに挫折した人がやるべきこと

この記事を読んでいるあなたはきっと、「A Tour of Goを最初にやると良いって聞いたけど難しすぎ…」と感じているはず。

そういう人向けにGo言語をキャッチアップするのにおすすめの教材をお伝えします。

結論: Progateとドットインストールが最強

結論として、Progateとドットインストールを使うのが良いです。

理由は次の3点です。

  1. 初学者向けのサービスなのでかなり分かりやすい
  2. ブラウザ上で手を動かして学べる
  3. Go言語の基本から並行処理など難しめのトピックまでコンパクトにまとまっている

ぼくは現役のGoエンジニアですが、A Tour of Goに挫折したのでGoのキャッチアップにはProgateとドットインストールを使いました。

その上で再度A Tour of Goをやると前よりもかなり理解できます。

「Progate → ドットインストール → A Tour of Go」の順序で進めるのがおすすめです。

補足: Progateやドットインストールに抵抗感があるエンジニアへ

Progateやドットインストールなんて未経験向けのサービスでしょ、現役エンジニアなら公式のチュートリアルやオライリーの書籍で技術をキャッチアップするべき!

このように考えるエンジニアもいるかもしれません。

たしかに、公式ドキュメントにも一度は目を通すべきです。

しかし、エンジニアにとって大切なのはGoの基本を抑え、実務で早く戦力になり、成果を出すことです。

公式チュートリアルもオライリーの書籍もそのための手段であって、目的ではありません。

学習教材ではなく仕事の成果に対してこだわりを持つことが重要です。

A Tour of Goが難しいと感じるならば、現役エンジニアであってもProgateやドットインストールを遠慮なく活用してください。

まとめ

この記事ではA Tour of Goの解答をまとめつつ、後半では「A Tour of Goが難しいと感じるならProgateやドットインストールを活用した方が効率的だよ」という話をしました。

「現役エンジニアなら公式チュートリアルで技術をキャッチアップすべき」というムードがあるため引け目を感じるかもしれませんが、大切なのは成果です。

教材にこだわるのではなく、便利なサービスをどんどん活用して、成果を出すことにこだわっていきましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

独学で未経験からRubyKaigiのスポンサーを務める上場企業へ転職。エンジニア歴1年8ヶ月で年収720万円の内定を2社獲得し転職。Goを使うフィンテック企業に在籍。

コメント

コメントする

目次