原文:私は行くのプロジェクトで見てきたトップ10のほとんどのよくある間違い
作者: Teiva Harsanyi
翻訳:サイモン・マ
テン一般的な間違いは、私が行くの開発に会いました。順序は重要ではありません。
不明な列挙値
簡単な例を見てみましょう:
type Status uint32
const (
StatusOpen Status = iota
StatusClosed
StatusUnknown
)
ここでは、イオタ、以下の結果を列挙を作成して使用します。
StatusOpen = 0
StatusClosed = 1
StatusUnknown = 2
今、私たちはこのことを前提としましょうStatus
タイプがJSONリクエストになりますの一部ですmarshalled/unmarshalled
。
私たちは、次の構造を設計しました:
type Request struct {
ID int `json:"Id"`
Timestamp int `json:"Timestamp"`
Status Status `json:"Status"`
}
そこで、このような要求を受けました:
{
"Id": 1234,
"Timestamp": 1563362390,
"Status": 0
}
ここでは何も特別な、状態はならないでしょうunmarshalled
にStatusOpen
。
しかし、私たちは別の要求のステータス値は例を設定しないようにしましょう:
{
"Id": 1235,
"Timestamp": 1563362390
}
この場合、要求の構造Status
フィールドは、そのゼロ値に初期化される(のためのuint32
データ型:0)、その結果であるStatusOpen
はありませんStatusUnknown
。
そして、最善のアプローチは、することです0に未知の値の列挙を設定します:
type Status uint32
const (
StatusUnknown Status = iota
StatusOpen
StatusClosed
)
状態はJSONリクエストの一部ではない場合、それが初期化されますStatusUnknown
。この我々の期待に沿って、。
ベンチマークの自動最適化
正しい結果を得るために考慮すべきベンチマーク多くの要因。
よくある間違いはしている目に見えない間に、コンパイラのテスト・コードを最適化すること。
ここにあるteivah/bitvector
ライブラリーの例:
func clear(n uint64, i, j uint8) uint64 {
return (math.MaxUint64<<j | ((1 << i) - 1)) & n
}
この関数は、指定された範囲内のビットをクリアします。次のようにそれをテストするために、彼らはそうすることがあります。
func BenchmarkWrong(b *testing.B) {
for i := 0; i < b.N; i++ {
clear(1221892080809121, 10, 63)
}
}
このベンチマークでは、clear
我々は他の関数、ノー呼び出さない副作用。そのため、コンパイラがされますclear
インラインに最適化されました。アライアンス内部たら、それは不正確なテスト結果につながります。
一つの解決策はあるのグローバル変数の関数としての結果、以下のように、:
var result uint64
func BenchmarkCorrect(b *testing.B) {
var r uint64
for i := 0; i < b.N; i++ {
r = clear(1221892080809121, 10, 63)
}
result = r
}
このように、コンパイラが知ることができませんclear
、それは副作用をもたらすでしょうか。
したがって、それはされませんclear
インライン関数に最適化されました。
深い読み
転送ポインタ
関数呼び出しでは、値によって渡された変数は、変数のコピーを作成し、ポインタのみを通過させることにより、変数のメモリアドレスを渡します。
だから、ポインタを渡すことは速くあなたよりも値渡しされますか?見てみましょう。この例を。
私は地元の環境をシミュレートし0.3KB
たデータ、および、速度のためにテストされ、値ポインタで渡されました。
結果があることを示す:4倍以上速く値渡し変速比ポインタを、これは反直感的です。
テスト結果を移動して、メモリ関連の管理する方法。私はようにすることはできませんが、ウィリアム・ケネディだけでなく、それを説明するが、私は何を要約してみましょう。
翻訳者注開始
著者は、翻訳者が何かを追加、基本的なストレージ囲碁メモリを説明しませんでした。
ここでは、聖書の囲碁言語からの説明は次のとおりです。
ゴルーチンは、一般的にのみ2キロバイトに必要な、小さなスタックとそのライフサイクルを開始します。
ゴルーチンスタック、および同じオペレーティングシステムスレッド、それは関数のローカル変数を保存しますが、アクティブまたは保留中のコール、およびゴルーチンスタックサイズが固定されておらず、同一のOSスレッドではなく、スタックサイズは、上のベースとなります私たちは、動的ストレッチする必要があります。
通常の状況下で、最もゴルーチンは、このような大規模なスタックを必要としないものの、最大スタックゴルーチンは、従来の固定サイズのスレッドスタックよりも1ギガバイトは、はるかに大きいいます。
翻訳自身の理解:
スタック:各Goruntineの先頭にデータを格納することとは別のスタックを持っています。(Goruntine Goruntine主点と他のGoruntineは、違いは、初期スタックサイズです)
ヒープ:Goruntineパイルの頂部に格納されたデータを複数で共有する必要があります。
翻訳者注エンド
我々はすべて知っているように、あなたができるヒープやスタックに割り当てられた変数を。
- 現在のスタックSAVE
Goroutine
変数が使用されている(翻訳者注:ローカル変数として理解されます)。関数が戻ると、変数がスタックからポップアップ表示されます。 - 店舗スタック共有変数(グローバル変数など)。
簡単な例を見てみましょう、単一の値を返します。
func getFooValue() foo {
var result foo
// Do something
return result
}
関数を呼び出すときに、result
ときに関数が戻り変数は、現在のGoruntineスタックに作成され、値は、受信者のコピーに渡されます。result
変数自体は、現在のGoruntineスタックからポップされます。
それはまだメモリ内に存在するが、それはアクセスできなくなります。そして、他の消去されたデータ変数があるかもしれません。
ポインタの例を参照してください戻っ:
func getFooPointer() *foo {
var result foo
// Do something
return &result
}
関数を呼び出すとき、result
関数が戻るが、それが受信者へのポインタ(変数のアドレスのコピー)を通過するときに変数は、現在のGoruntineスタックに作成されます。場合はresult
、可変電流Goruntineがスタックからポップ、受信機はそれにアクセスすることはできません。(翻訳者注:この状況は、「メモリー・エスケープ」と呼ばれています)
このシナリオでは、囲碁コンパイラがしますresult
変数はエスケープ:あなたは、変数を共有できる場所に、ヒープを。
しかし、別のケースでは、ポインタを渡しています。例えば:
func main() {
p := &foo{}
f(p)
}
我々は同じゴルーチンに呼び出すためf
、そのp
変数をエスケープする必要はありません。それはちょうどサブ機能がそれにアクセスすることができ、スタックにプッシュ。(翻訳者注:Goruntineが共有なしの変数はスタック上に保存されていることができます)
例えば、メソッドシグネチャ、スライスパラメータを受信し、コンテンツをスライスを読み取り、読み取ったバイト数を返します。代わりに読書のスライスを返します。(翻訳者注:リターン・スライスした場合、スライスは、ヒープにエスケープします。)io.Reader
Read
type Reader interface {
Read(p []byte) (n int, err error)
}
なぜそんなに速いスタック?2つの主な理由があります:
- スタックは、ガベージコレクタを必要としません。我々が言ったように関数がスタックから戻りますと、変数は、一度作成されたスタックにプッシュされます。これは、未使用の変数を再利用するために複雑なプロセスを必要としません。
- 店舗変数は、同期化を検討する必要があります。ゴルーチンは、スタックに属し、可変ストレージヒープに格納変数と比較して、したがって、同期を必要としません。
あなたが関数を作成するとき要するに、私たちのデフォルトの動作は、値を使用することであるべきポインタではなく。我々は唯一の場合のみ、共有変数のポインタを使用したいです。
我々はパフォーマンスの問題が発生している場合は、使用することができgo build -gcflags "-m -m"
、ヒープの特定の操作にコンパイラ変数のエスケープを表示するコマンドを。
ここでも、ほとんどの日常的な使用事例のために、転送の値が最も適切です。
深い読み
予期しないブレーク
場合はf
trueを返し、以下の例では、何が起こりますか?
for {
switch f() {
case true:
break
case false:
// Do something
}
}
我々は呼ぶbreak
声明を。しかし、それは次のようになりますのではなく、文のサイクリング。break
switch
for
同じ質問:
for {
select {
case <-ch:
// Do something
case <-ctx.Done():
break
}
}
break
とselect
に関連した文、およびfor
サイクルに依存しません。
break
for/switch或for/select
一つの解決策は、することですタグ付きBREAKを使用して、以下のように、:
loop:
for {
select {
case <-ch:
// Do something
case <-ctx.Done():
break loop
}
}
欠落コンテキストエラー
エラー処理に進みますが、まだ今GO2のエラー処理が最も期待需要があるように、改善する必要があります。
(ゴー1.13前の)現在の標準ライブラリのみerror
のコンストラクタは、自然に他の情報が欠落します。
のは、見てみましょうPKG /エラーはエラー処理におけるシンクタンク:
エラーは、処理する必要があります 一度。エラーがロギングされた エラーを処理します。だから、エラーがなければならない のいずれか ログインまたは伝播すること。
(翻訳:エラーが記録された一度だけ処理する必要がありますログインエラーはエラーでそう取り扱いされ、エラーが記録または分散させる必要があります。)
現在の標準ライブラリの場合、我々がエラーにいくつかのコンテキスト情報を追加したいので、それは階層構造を有し、これを行うことは困難です。
たとえば、次の目的のREST
サンプル・データベースの問題のコールの結果:
unable to server HTTP POST request for customer 1234
|_ unable to insert customer contract abcd
|_ unable to commit transaction
私たちが使用している場合pkg/errors
、あなたはこれを行うことができます。
func postHandler(customer Customer) Status {
err := insert(customer.Contract)
if err != nil {
log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
return Status{ok: false}
}
return Status{ok: true}
}
func insert(contract Contract) error {
err := dbQuery(contract)
if err != nil {
return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
}
return nil
}
func dbQuery(contract Contract) error {
// Do something then fail
return errors.New("unable to commit transaction")
}
それは最初の外部ライブラリによって返されない場合error
に使用することができますerror.New
作成します。中間層insert
、このエラーは、追加のコンテキスト情報を追加します。最後に渡されたlog
エラーのエラーを処理します。各レベルは、エラーを返すか、エラーハンドリングのいずれか。
我々はまた、彼らは再びしようとするかどうかを解釈するために、エラーの原因を確認したいことがあります。私たちは外からのライブラリーがあるとdb
データベース・アクセスを処理するためのパッケージを。ライブラリは、名前付き返すことがありdb.DBError
、一時的なエラーが発生しました。あなたが再試行する必要があるかどうかを決定するために、我々は、エラーの原因をチェックする必要があります。
使用pkg/errors
提供errors.Cause
、エラーの原因を特定することができます。
func postHandler(customer Customer) Status {
err := insert(customer.Contract)
if err != nil {
switch errors.Cause(err).(type) {
default:
log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
return Status{ok: false}
case *db.DBError:
return retry(customer)
}
}
return Status{ok: true}
}
func insert(contract Contract) error {
err := db.dbQuery(contract)
if err != nil {
return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
}
return nil
}
私が見てきた一般的な間違いは、部分的に使用されていますpkg/errors
。例えば、このようにエラーをチェックすることにより:
switch err.(type) {
default:
log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
return Status{ok: false}
case *db.DBError:
return retry(customer)
}
あなたがいる場合は、この例では、db.DBError
あるwrapped
、それが実行されることはありませんretry
。
深い読み
拡大はスライスされています
時々 、私たちは、スライスの最後の長さを知っています。私たちがしたいとしFoo
に変換スライスBar
二つの部分の長さが同じであることを意味し、スライス。
私は頻繁に初期化し、次のセクションで参照してください。
var bars []Bar
bars := make([]Bar, 0)
データ構造のない魔法のスライス使用可能なより多くのスペースがない場合、それは二重の展開となります。この場合、それは自動的にスライス(高容量)を作成し、その要素をコピーします。
あなたは要素の数千人を収容したい場合は、我々は拡大を必要とする回数を想像してみてください。挿入時の複雑さがO(1)
、それがパフォーマンスに影響を与えます。
だから、我々は最終的に長さを知っていれば、私たちは次のことができます。
それは、所定の長さで初期化されます
func convert(foos []Foo) []Bar { bars := make([]Bar, len(foos)) for i, foo := range foos { bars[i] = fooToBar(foo) } return bars }
または0の所定の長さを使用して、それに容量を初期化します。
func convert(foos []Foo) []Bar { bars := make([]Bar, 0, len(foos)) for _, foo := range foos { bars = append(bars, fooToBar(foo)) } return bars }
コンテキストの規定はありません
context.Context
しばしば誤用。公式文書によると:
コンテキストAPIの境界を越え期限、キャンセル信号、および他の値を運びます。
この説明は、それが一部の人々は混乱し、それを使用しますことを、非常にあいまいです。
私たちは詳細にそれを記述してみましょう。Context
これは、可能性があります。
- DEADLINE(締め切り)。それはの有効期限(250ミリ秒または指定した日付の後に)した後、我々は(現在進行中の操作を停止しなければならないことを意味
I/O
するために待って、要求をchannel
、入力など)。 - キャンセリング信号(キャンセル信号)。我々は、信号を受信したら、我々は継続的な活動を停止する必要があります。最初をキャンセルするためにいくつかのデータを挿入するための1、他の要求:たとえば、私たちは2つの要求を受けたとします。これは、最初の呼び出しで使用することができ
cancelable
、我々は第2の要求を得れば、コンテキストを実装するために、コンテキストがキャンセルされます。 - キー/値(キー/値リスト)のリストが基づいている
interface{}
タイプ。
あることを言及する価値があるコンテキストを組み合わせることができます。例えば、我々は期限を継承し、キー/値のリストを持つことができますContext
。また、より多くのgoroutines
同じを共有することができますContext
キャンセルは、Context
複数の活動を停止することがあります。
戻る私たちのトピックに、私が経験した例を与えます。
ベースのurfave / CLIは(あなたがわからない場合、これは良いライブラリがあり、あなたが行くでコマンドラインアプリケーションを作成することができます)囲碁アプリケーションを作成しました。開始すると、プログラムは、親を継承しますContext
。これは、アプリケーションが停止されたときに、これを使用することを意味しますContext
キャンセルする信号を送信します。
私の経験では、これがあることであるContext
への呼び出しであるgRPC
私がやりたいことではありません直接転送、。代わりに、私は100ミリ取り消し要求を送信した後、アプリケーションの動作を停止するかどうかをしたいです。
このような理由から、あなたは単に組み合わせを作成することができますContext
。もしparent
親Context
(の名前urfave / CLIで作成された後、)、以下の操作の組み合わせ:
ctx, cancel := context.WithTimeout(parent, 100 * time.Millisecond)
response, err := grpcClient.Send(ctx, request)
Context
複雑ではない、私の意見では、それはゴーの最高の機能の一つとして記述することができます。
深い読み
忘れた-raceパラメータ
間違いは、私はよくのない状態でいることがわかり-race
テストパラメータ状況下でアプリケーションを移動します。
この報告書は述べ、ゴーものの「は、エラーが発生しやすい並行プログラミングを容易にし、より少ないように設計され、」我々はまだ同時実行の問題の多くを経験しています。
もちろん、囲碁大会検出器は、すべての同時実行の問題を解決することはできません。アプリケーションをテストする場合しかし、それはまだ大きな価値を持って、我々は常にそれを有効にする必要があります。
深い読み
囲碁レース検出器は、すべてのデータレースバグをキャッチしていますか?
もっと完璧なパッケージ
もう一つのよくある間違いは、関数にファイル名を渡すことです。
我々は空白行の数は、ファイルを計算する機能を実装したとします。初期の実装はこれです:
func count(filename string) (int, error) {
file, err := os.Open(filename)
if err != nil {
return 0, errors.Wrapf(err, "unable to open %s", filename)
}
defer file.Close()
scanner := bufio.NewScanner(file)
count := 0
for scanner.Scan() {
if scanner.Text() == "" {
count++
}
}
return count, nil
}
filename
指定されたパラメータとして、その後、私たちは、ファイルを開き、空白行、ああ、何の問題を読み取るためのロジックを実装します。
私たちはユニットテストでこの機能を実現したいとして、試験対象のファイルの種類でエンコードされた通常のファイル、空のファイルを、使用して。コードは簡単に維持することは非常に困難になることができます。
私たちがしたい場合に加えて、HTTP Body
同じロジックを実現する、あなたは、この目的のために別の関数を作成する必要があります。
二つの大きな設計されたインターフェースを行くio.Reader
とio.Writer
(翻訳者注:一般的なIOコマンドライン、ファイル、ネットワークなど)
だから、抽象データソースを渡すことができio.Reader
、ファイル名を渡すのではなく。
ただ、統計ファイルについて考えますか?HTTPボディ?バイトバッファ?
答えは、それがあるかどうかと同じくらい重要ではありませんReader
、我々は同じ使用するデータの種類を読み取るためRead
の方法を。
この例では、偶数プログレッシブ入力バッファに(使用してそれを読んでbufio.Reader
、そのReadLine
方法)。
func count(reader *bufio.Reader) (int, error) {
count := 0
for {
line, _, err := reader.ReadLine()
if err != nil {
switch err {
default:
return 0, errors.Wrapf(err, "unable to read")
case io.EOF:
return count, nil
}
}
if len(line) == 0 {
count++
}
}
}
論理ファイルは現在のコールに開くcount
ことによって:
file, err := os.Open(filename)
if err != nil {
return errors.Wrapf(err, "unable to open %s", filename)
}
defer file.Close()
count, err := count(bufio.NewReader(file))
データソースに関係なく、あなたが呼び出すことができますcount
。あなたは文字列から作成することができますので、それはまた、ユニットテストを促進するbufio.Reader
効率を大幅に向上させ、。
count, err := count(bufio.NewReader(strings.NewReader("input")))
ループ変数をGoruntines
最後に、私が見てきた一般的な間違いは、ゴルーチンとループ変数を使用することです。
次の例の出力は何だろうか?
ints := []int{1, 2, 3}
for _, i := range ints {
go func() {
fmt.Printf("%v\n", i)
}()
}
オーダーのうち出力1 2 3
?彼はそれが間違っていました。
この例では、各インスタンス変数ゴルーチンは同じなので、最も可能性の高い出力を共有します3 3 3
。
この問題を解決するための2つのソリューションがあります。
最初にすることであるi
可変クロージャ(内部関数)に値を渡します。
ints := []int{1, 2, 3}
for _, i := range ints {
go func(i int) {
fmt.Printf("%v\n", i)
}(i)
}
第二は、for
ループの範囲内で別の変数を作成します。
ints := []int{1, 2, 3}
for _, i := range ints {
i := i
go func() {
fmt.Printf("%v\n", i)
}()
}
i := i
それは奇妙に思えるかもしれないが、それは完全に有効です。
サイクル内の別のスコープ内で、それがあることを意味するので、i := i
他と呼ば作成の等価i
インスタンス変数。
もちろん、読みやすくするために、別の変数名を使用することが好ましいです。
深い読み
また、他の一般的な間違いを言及しますか?議論を継続して、共有すること自由に感じて下さい。)