Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kadai3 tomoyuki.kobayashi #36

Open
wants to merge 41 commits into
base: master
Choose a base branch
from

Conversation

bakisunsan
Copy link

@bakisunsan bakisunsan commented Nov 22, 2018

課題3

タイピングゲームを作ろう

  • 標準出力に英単語を出す(出すものは自由)
  • 標準入力から1行受け取る
  • 制限時間内に何問解けたか表示する

下記のような構成で実現した

  • main.go
    オプションパラメタの読み込み、設定ファイルの読み込み、
    gameからチャネルに送られてくる情報を整形して出力する責務

  • /typing

    • quiz_data.go
      語彙を格納し、ゲームレベルに応じた語彙を返却する責務
    • question.go
      語彙をレベル毎に、ゲーム性のある(ランダムな)値を返却する責務
    • game.go
      ゲーム全体の制御と、進行状況のデータ保持を行う。主にやっていることは下記
      • 進行状況からゲームの現在レベルを判定し、レベルにあった語彙をquestionチャネルに送信する
      • answerチャネルから入力を受けつけ、現在questionで出題されているものとマッチしたかミスしたかを判定して、成績を更新
      • 指定した時間でゲームを終了し、resultチャネルに成績データを送る

分割ダウンロードを行う

  • Rangeアクセスを用いる
  • いくつかのゴルーチンでダウンロードしてマージする
  • エラー処理を工夫する
    golang.org/x/sync/errgourpパッケージなどを使ってみる
  • キャンセルが発生した場合の実装を行う

下記のような流れで実現した

  1. HEADしてRangeサポートしているか/Contentsのサイズを見る
  2. 分割ダウンロードした一時ファイルを作成するディレクトリを作成
  3. 任意のプロセス数で、goroutineでRangeダウンロードを行い、一時ファイルに保存する
    すでに対象ファイルが存在していたら、DLせずスキップする
  4. errgroup.Groupで待ち合わせる
  5. 全ての一時ファイルを読んで結合してダウンロードファイルを作成
  6. 一時ファイルを削除

}

// Yaml HACK 使用しているパッケージの使用でしょうがなくpublicにしている
type Yaml struct {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QuestionContainer とかそういう感じの名前にすると良さげですね
この struct は YAML データをパースした結果を入れるものですが、保存している先は別に YAML じゃなくても問題ありません。例えば JSON とかでも構わないわけです。
この struct の本質はレベルごとの問題を格納しているものなので、それに沿った名前にするほうが可読性も上がると思います。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。おっしゃる通りで、こいつが何のためにいるのか、がわかる名前じゃないとダメですね
そもそもQuestionerの所でこの構造を知っている必要もないので、名前を変えてインターフェイスとかで渡すようにしようかと

@bakisunsan bakisunsan changed the title [WIP] Kadai3 tomoyuki.kobayashi Kadai3 tomoyuki.kobayashi Nov 24, 2018
}

// Run ゲームを開始する
func (c *constGame) Run() (<-chan string, <-chan string, <-chan [2]int) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO [2]int に名前をつけてやったほうがよかった

Copy link

@mizkei mizkei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントしましたので、参考にしていただければと思います


// NewQuizData クイズデータを生成する
func NewQuizData(source io.Reader) (QuizData, error) {
data, err := ioutil.ReadAll(source)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ioutil.ReadAllはメモリにすべて載せてしまうので、streamから読み出すようにすることでメモリを圧迫しなくなります
(今回の場合は大量のファイルを読み出したりしないため、あまり影響はないと思いますが)

var s QuizSource
if err := yaml.NewDecoder(r).Decode(&s); err != nil {
  return nil, err
}
return &s, nil

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あー、Reader(ストリーム) -> buf(メモリ) -> struct だとメモリ余計に確保するから
Reader(ストリーム) -> struct にしましょうという事ですね。確かに

}

func (q *questionContainer) GetNewWord(level int) string {
rand.Seed(time.Now().UnixNano())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rand.Seed は毎回セットする必要はないため、一度だけ呼んであげればよいです

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かに

}

// NewGame Gameのコンストラクタです
func NewGame(timeout int, source io.Reader, input io.Reader) (Game, error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同じ型の引数は型を省略できます

func NewGame(timeout int, source, input io.Reader) (Game, error) {...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かに。そちらの方が可読性高いですね

// Run ゲームを開始する
func (c *constGame) Run() (<-chan string, <-chan string, <-chan [2]int) {
tc := time.Duration(c.timeout) * time.Second
// NOTE defer cancel() cancelするとDone条件閉じてゲーム終わっちゃう
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://golang.org/pkg/context/

Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires.

大抵の場合、cancelはちゃんと呼んでほうが良いです

その上でここでcancelを呼べないのは、設計としてここで作成したcontextを扱うべきでないか、 Run がブロックしないからだと思います

Run(ctx context.Context) でキャンセル可能にすれば、ここでの作成は必要なくなり、Runを呼び出す側で管理できるようになると思います
ただ、その場合は ゲーム が扱うべき時間制限が外の扱いになってしまうのが気持ち悪いかもしれませんが、そもそもRunという長時間実行される可能性のあるメソッドにはcontext.Contextを渡して、キャンセル可能にしておくのは悪くないと思います

もう一つ、 Run をブロックさせるという方法ですが、このメソッドの中でcontextを作成した場合、このメソッドがreturnするときにはその処理は終わっていてほしいはずです(もし終わっていてほしくない場合はもはや別のcontextとして扱われるべき状況である可能性が高いはず)
したがって、Runはゲームが終了するまでブロックする形で実行されれば、ゲームの終了とcancelは同一のタイミングでよくなるため、このメソッドの中で defer cancel() が実行できるようになるはずです

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CancelFunc cancels the child and its children, removes the parent's reference to the child

あー、Cancel呼んでやらないと、子コンテキストへの参照が生き続けるのか、それはマズイですね。。。

Run をブロックさせるという方法ですが、このメソッドの中でcontextを作成した場合、このメソッドがreturnするときにはその処理は終わっていてほしいはずです(もし終わっていてほしくない場合はもはや別のcontextとして扱われるべき状況である可能性が高いはず)

言われてみると、確かにそうですね
Contextを持っているRunは終わったけどそのContextを参照したgoroutineは元気に生きているというのはおかしい。。。

go func() {
for {
word := c.GetNewWord(c.nextLevel())
qCh <- word
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここでブロックしてしまうと、問題がchanから読み込まれるまでcontextによるキャンセルが効かなくなるので、selectに含めるとより扱いやすくなると思います

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね。selectにこれを追加して、defaultをなくすのが良さそうです
ありがとうございます。

for {
word := c.GetNewWord(c.nextLevel())
qCh <- word
c.currentWord = word
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この c.currentWord の扱いについて、このgoroutineも参照して、answerのgoroutineでも isCorrect のメソッドから参照しているのでおそらくraceしてしまいそうです(おそらくまず発生しないと思いますが)
go test -race でテストを実行してみてください

おそらく発生する流れ

  • aCh <- ansが実行される
  • mainのforのblockが次に進むので、qChから読み込まれ、c.currentWordが更新される
  • isCorrectで次の問題の答えと比較されて、間違いと判定されてしまう

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

おそらくまず発生しないと思いますが

テストで固定で複数行のstringをinputさせて動作させてみたら結果に揺れがあったので、なんでだろうと思っていましたが
多分こいつですね。。。普通に起きていました

}
}
// tmp配下にハッシュにマッチするフォルダがなければ作る
dlTmp := filepath.Join("tmp", hash)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ioutilにTempDirというメソッドがあります
https://golang.org/pkg/io/ioutil/#TempDir

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます

}

// 取得したデータをBufに読み出し
buf, err := ioutil.ReadAll(dRes.Body)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReadAllせずに io.Copy をすれば十分かと思います

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここも、ストリームはストリームのまま渡そう、という話ですね
ありがとうございます。

for i := 0; i < procs; i++ {
file, err := os.Open(filepath.Join(dlTmp, strconv.FormatInt(int64(i+1), 10)))
// 閉じるの失敗しても実害ないので、エラー処理なし
defer file.Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これは if err != nil {...} の下にあったほうがよいです
err != nil の場合、Openの実行にエラーがあったということなので、fileが開かれていないと考えたほうが良いです
まずエラーをチェックして、エラーがなければ、fileを触る、としたほうが安全です

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
ここは単純にミスです。。。

files[i] = file
}
reader := io.MultiReader(files...)
b, err := ioutil.ReadAll(reader)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここも io.Copy で良いと思います

@bakisunsan
Copy link
Author

bakisunsan commented Nov 26, 2018

色々ためになるコメントありがとうございました 🙇 @mizkei san @knsh14 san

@bakisunsan
Copy link
Author

レビュー指摘事項に対応。下記はまだ未対応
kadai3-1
ゲーム結果の[2]intにtypeをつける
raceの解消(mutex使うのがいいかな)
テストのTODO修正

kadai3-2
一時ディレクトリをioutil.TempDirで作る

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants