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 2 kimuson13 #50

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

Conversation

kimuson13
Copy link

課題3-2 分割ダウンローダを作ろう

  • Rangeアクセスを用いる
  • いくつかのゴルーチンでダウンロードしてマージする
  • エラー処理を工夫する
  • キャンセルが発生した場合の実装を行う

pgetを参考にしました。ダウンロードのテストを書くのがうまくいかなかったです。それについて教えていただけると助かります。Resetという名前のコミット以降から見てください。

parallel := flg.Int("p", runtime.NumCPU(), "separate Content-Length with this argument")
timeout := flg.Int("t", 30, "timeout for this second")
filename := flg.String("f", "paralleldownload", "save the file as this name")
flg.Parse(args)

Choose a reason for hiding this comment

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

失敗するケースを再現できなかったのですが、errorを返す関数であるのでエラー処理しておいた方が良さそうです。

}

acceptRange := res.Header.Get("Accept-Ranges")
fmt.Fprintf(os.Stdout, "got: Accept-Ranges: %s\n", acceptRange)

Choose a reason for hiding this comment

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

Fprintfもerrorを返す巻数なので、エラー処理しておいた方が良いです

if err != nil {
return err
}
sub.Close()

Choose a reason for hiding this comment

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

ここでsub.Closeを呼ぶと上でエラーになったときにクローズされない可能性があります。そのため

		sub, err := os.Open(n)
		if err != nil {
			return err
		}
                 defer sub.Close()

このようにエラー処理の直後にdeferで閉じ処理をするまでが基本的な書き方です。

ただし今回のようにループでdeferを使うのは問題があるケースが多いです。最後にsubに対してループ回数分deferが実行されるので意図しない動作になります。なので、結論としてループの中の処理を関数に切り出してその中でdeferを使うのが適切かと思います。

if err != nil {
return err
}
defer file.Close()

Choose a reason for hiding this comment

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

L210でも指摘しましたが、ループでdeferは避けましょう

if err != nil {
return err
}
defer fh.Close()

Choose a reason for hiding this comment

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

エラー周りで複数指摘を入れていますが、これらを見つけるためにgithub.com/kisielk/errcheckというツールを使っています。

[~/go/src/github.com/kimuson13/gopherdojo-studyroom/kadai3-2/kimuson13] % errcheck ./...
download/download.go:77:13:     fmt.Fprintf(os.Stdout, "Start HEAD request to check Content-Length\n")
download/download.go:91:13:     fmt.Fprintf(os.Stdout, "got: Accept-Ranges: %s\n", acceptRange)
download/download.go:100:13:    fmt.Fprintf(os.Stdout, "got: Content-Length: %v\n", contentLength)
download/download.go:127:19:    defer file.Close()
download/download.go:164:13:    fmt.Fprintf(os.Stdout, "start GET request: bytes=%d-%d\n", r.low, r.high)
download/download.go:170:22:    defer res.Body.Close()
download/download.go:180:20:    defer output.Close()
download/download.go:197:16:    defer fh.Close()
download/download.go:210:12:    sub.Close()
download/option.go:23:11:       flg.Parse(args)

deferでのエラーハンドリングは少し工夫が必要な上、おきてもログに出すぐらいしかできないケースが多いのですが、それらに埋もれて本当にハンドルしなきゃいけないものが漏れるよりかは指摘がゼロになるように直した方が早いかと思うのでそうすることをお勧めします。

以下のような形で無名関数を使えば良いので参考にしてください。
https://joniy-joniy.hatenablog.com/entry/2017/12/31/182004

//Preparate method define the variables to Donwload
func (d *Downloader) Preparate() error {
if d.parallel < 1 {
d.parallel = 2

Choose a reason for hiding this comment

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

flagでデフォルト runtime.NumCPU()になっているのでそれに揃えるか、この関数自体をバリデーション用の関数として(名前もValidationとかに)、その上で1未満だとエラーを返すとかの方が良いのではと思います。

}

if d.timeout < 1 {
d.timeout = 5

Choose a reason for hiding this comment

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

ここもflagでは30sなので、そちらに揃える方が自然です。
かつ数字を直接書くよりは定数にしてflagの方もそれを使うのが良いでしょう。

}

//Preparate method define the variables to Donwload
func (d *Downloader) Preparate() error {

Choose a reason for hiding this comment

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

一応現状ソースのままならエラーが返ることは無いので、エラーを返す関数にする必要がないですね

if err := os.RemoveAll("tempdir"); err != nil {
fmt.Println("Error:", err)
}
}

Choose a reason for hiding this comment

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

tempdirの削除はdownload.goに持たせるべきなので、そこにまとめましょう

if err != nil {
return err
}

Choose a reason for hiding this comment

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

Downloadでtempdirの作成、MergeFileでtempdirの削除を行なっていますが、エラーになったときにtmpdirが消されないケースがあるので、ここで以下のように作成の関数と削除の関数を作り削除の関数をdeferを使って呼ぶと良いです

if err := createTmpDir(); err != nil {
    return err
}
defer deleteTmpDir()

@gosagawa
Copy link

Downloadのテストはファイルをダウンロードして、それがいろいろな条件のもと意図したファイルが落とせているかを検証すれば良さそうです。その際におそらく困るのがURLで、使っていい外部のURLがあったとしても毎回テストの度にアクセスが走ることやテストのURLにアクセスできる環境を担保しなければならないなどの問題があります。URLが用意できればいいということは、サーバがあればいいということなのでダミーサーバを作れればやりたいことができそうです。

ダウンロードできるまでは試しに書いてみました。(拡張子はgoに直してください、githubでそのまま送れなかったので)
download_test.txt
download/_testdata/foo.pngを作れば動くはずです。
ここからファイルが完全に一致すること、様々なオプションを変えてみて動くことやエラーが出ることを検証すればテストしては良さそうです。

同じようにやっているケースとして、私が参加したGopher道場三回でよく書けているケースを貼っておきます。参考にしてください!
gopherdojo/dojo3#50

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.

2 participants