[Go言語/非同期処理]Go言語でタイピングゲームを作ってみた.go

# 0.目次
1.はじめに
　1.1 使用上の注意
2.実行環境
　2.1ファイル構造
　2.2コンパイル方法
　2.3プログラム実行方法
3.工夫した点
　3.1 エラー処理部分
　3.2 チャネルを使った非同期処理部分(**一番苦労した箇所**)
4.実際に遊んでみた

# 1. はじめに
Go言語でタイピングゲームをを作成した。
実行ファイルと同じディレクトリにあるテキストファイル内の単語から問題は出題される。

オプションを指定して制限時間を指定することも可能。

# 1.1 使用上の注意
Go言語のソースファイルをコンパイルしてからでないと実行できないので注意

# 2.実行環境

## 2.1 ファイル構造
クリックするとGitHubに飛びます。
[typinggame](https://github.com/TsubasaSato/typinggame)/
　├README.txt
　├typinggame.go
　└words.txt
**各ファイルの説明**

- README.txt:プログラムの説明書
- typinggame.go:タイピングゲームのソースファイル
- words.txt:英単語が1段落ずつ計50単語格納されているテキストファイル

```golang:typinggame.go
package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"time"
)

var t int

func init() {
	//オプションで制限時間をできる
	flag.IntVar(&t, "t", 1, "制限時間(分)")
	flag.Parse()
}

func main() {
	var (
		files     = flag.Args()
		path, err = os.Executable()
		ch_rcv    = myinput(os.Stdin)
		tm        = time.After(time.Duration(t) * time.Minute)
		words     []string
		score     = 0
	)
	if err != nil {
		//実行ファイルのパスが取得できなかったときのエラー処理
		fmt.Fprintln(os.Stderr, "読み込みに失敗しました", err)
		os.Exit(0)
	} else if len(files) != 1 {
		//コマンドの引数に1つ以上のテキストファイルが指定された時のエラー処理
		fmt.Fprintln(os.Stderr, "指定できるファイル数は1つだけです")
		os.Exit(0)
	}
	//実行ファイルのディレクトリまでのPathを取得
	path = filepath.Dir(path)
	words, err = getwords_from(filepath.Join(path, files[0]))
	if err != nil {
		//テキストファイルパスが取得できない場合のエラー処理
		fmt.Fprintln(os.Stderr, "読み込みに失敗しました", err)
		os.Exit(0)
	}

	fmt.Println("タイピングゲームを始めます。制限時間は", t, "分。1語1点、", len(words), "点満点")
	//送信用チャネル
	for i := true; i && score < len(words); {
		question := words[score]
		fmt.Print(question)
		fmt.Print(">")
		select {
		case x := <-ch_rcv:
			//標準入力に何か入力された時の処理
			// 入力された文字が一致しているかどうかをチェックする
			if question == x {
				score++
			}
		case <-tm:
			//制限時間が過ぎた際の処理
			fmt.Println("制限時間を過ぎました")
			i = false
		}
	}
	fmt.Println("あなたの点数:", score, "点 / ", len(words), " 点")

}

func myinput(r io.Reader) <-chan string {
	// サブgo ルーチン
	// 標準入力から受け取った文字列を標準出力へ出力する
	ch1 := make(chan string)
	go func() {
		s := bufio.NewScanner(r)
		for s.Scan() {
			ch1 <- s.Text()
		}
	}()
	return ch1
}

func getwords_from(txt_path string) ([]string, error) {
	// テキストファイルのパスからテキストに書き込まれた単語をリスト化して返す
	var words []string
	sf, err := os.Open(txt_path)
	if err != nil {
		return nil, err
	} else {
		scanner := bufio.NewScanner(sf)
		for scanner.Scan() {
			words = append(words, scanner.Text())
		}
	}
	return words, err
}
```

## 2.2 コンパイル方法
`$go build typinggame.go` 
`// typinggame.exe`


## 2.3 プログラム実行方法
`$typinggame [オプション] [ファイル名]`

**使用できるオプション**
`-n=[数]` :制限時間を分単位で設定できる

# 3.工夫した点
Goを徐々に理解してきたので、工夫を多くの箇所で凝らした。

- ファイルの入出力の部分でもれなくエラー処理を行った
- チャネルを使って非同期処理に挑戦した
- オプションで制限時間を設定できる
- 出題したい問題をテキストファイルにまとめることでオリジナル問題を作成できる

## 3.1 エラー処理部分

```golang
var (
		files     = flag.Args()
		path, err = os.Executable()
		ch_rcv    = myinput(os.Stdin)
		tm        = time.After(time.Duration(t) * time.Minute)
		words     []string
		score     = 0
	)
	if err != nil {
		//実行ファイルのパスが取得できなかったときのエラー処理
		fmt.Fprintln(os.Stderr, "読み込みに失敗しました", err)
		os.Exit(0)
	} else if len(files) != 1 {
		//コマンドの引数に1つ以上のテキストファイルが指定された時のエラー処理
		fmt.Fprintln(os.Stderr, "指定できるファイル数は1つだけです")
		os.Exit(0)
	}
	//実行ファイルのディレクトリまでのPathを取得
	path = filepath.Dir(path)
	words, err = getwords_from(filepath.Join(path, files[0]))
	if err != nil {
		//テキストファイルパスが取得できない場合のエラー処理
		fmt.Fprintln(os.Stderr, "読み込みに失敗しました", err)
		os.Exit(0)
	}
```

実行ファイルのPathが取得できなかった場合、ファイルの引数が1つではない場合、テキストファイルパスが取得できなかった場合、
エラー出力を行い、`os.Exit(0)`でプログラムを終了させている。

## 3.2 チャネルを使った非同期処理部分(**一番苦労した箇所**)

```Golang
func myinput(r io.Reader) <-chan string {
	// サブgo ルーチン
	// 標準入力から受け取った文字列を標準出力へ出力する
	ch1 := make(chan string)
	go func() {
		s := bufio.NewScanner(r)
		for s.Scan() {
			ch1 <- s.Text()
		}
	}()
	return ch1
}
```

`ch1 := make(chan string)`でチャネルを作成し、`go`の部分でサブルーチンを作成している。
標準入力で受けた文字列をチャネルに送信し、そのチャネルを返すことで非同期処理を実現している。

# 4.実際に遊んでみた
![image.png](https://qiita-image-store.s3.amazonaws.com/0/328735/90b7a476-1cee-09a3-0157-7f33ff0e3900.png)
こんな感じで英単語とかを学習しながらタイピングの練習もできます！