Goで熊手を螏たない方法

この投皿は、私自身の英語の蚘事「Goでゎッチャを避ける方法」のバヌゞョンですが、 ゎッチャずいう単語はロシア語に翻蚳されおいたせん。


Gotchaは、説明どおりに動䜜するシステム、プログラム、たたはプログラミング蚀語の正しい蚭蚈ですが、同時に誀っお䜿いやすいため、盎感に反し、゚ラヌを匕き起こしたす。

Go蚀語にはそのような萜ずし穎がいく぀かあり、それらを詳现に 説明および説明する倚くの優れた蚘事がありたす 。 私は定期的に同じレヌキに萜ちる人を芋るので、これらの蚘事は、特にGoの初心者にずっお非垞に重芁だず思いたす。


しかし、ある質問が長い間私を苊しめたした-なぜ私は自分でこれらの間違いを犯したこずがないのですか 真剣に、nilむンタヌフェヌスずの混乱やappendによる䞍可解な結果-eスラむス-などの最も人気のあるものは、私の緎習では決しお問題になりたせんでした。 どういうわけか、Goでの仕事の最初の日からこれらの萜ずし穎を回避できたのは幞運でした。 䜕が私を助けたしたか


そしお答えは非垞に簡単でした。 Goのデヌタ構造の内郚構造ずその他の実装の詳现に぀いお、いく぀かの優れた蚘事をすぐに読みたした。 そしお、これは実際には非垞に衚面的なものであり、知識は盎芳を開発し、これらの萜ずし穎を回避するのに十分でした。


定矩に戻りたしょう。 「gotcha ...は正しい構造です...これは盎感に反したす...」です。 これがすべおの塩です。 実際、2぀のオプションがありたす。



最初のオプションは、倚くのhabrachitatelにアピヌルしたすが、もちろんオプションではありたせん。 Goには埌方互換性の玄束がありたす -蚀語はこれ以䞊倉わらず、これは玠晎らしいこずです-2012幎に曞かれたプログラムは、今日、最新バヌゞョンのGoでコンパむルされ、1回も枊巻くこずはありたせん。 ちなみに、囲inには混乱はありたせん:)


2番目のオプションは、 盎感を開発するためにより正確に呌び出されたす。 むンタヌフェむスたたはスラむスがどのように機胜するかを裏から知るずすぐに、盎感がより正確にプロンプ​​トを出し、間違いを避けるのに圹立ちたす。 この方法は私を倧いに助けおくれたしたし、他の人にもきっず圹立぀でしょう。 そこで、Goの内郚に関するこの基本的な知識を1぀の投皿に入れお、Goが内郚からどのように構築されるかに぀いおの盎感を他の人が開発できるようにするこずにしたした。


デヌタ型がメモリに栌玍される方法の基本的な理解から始めたしょう。 以䞋に、孊習する内容の短いリストを瀺したす。



ポむンタ


Goは、家系図にC蚀語が含たれおいるため、実際にはかなり鉄に近いものです。 int64型64ビットの敎数倀の倉数を䜜成する堎合、メモリに必芁な容量を正確に確認できたす。たた、 unsafe.Sizeofを䜿甚しお他の型を芋぀けるこずができたす。


私は、メモリ内のデヌタの芖芚的衚珟を䜿甚しお、倉数、配列、たたはデヌタ構造のサむズを「芋る」こずが本圓に奜きです。 芖芚的アプロヌチは、芏暡をすばやく理解し、盎感を開発し、生産性などを芖芚的に評䟡するのに圹立ちたす。


たずえば、Goの最も単玔な基本タむプから始めたしょう。


画像


このような芖芚化では、 int64型の倉数がint32の 2倍の「スペヌス」を占有し、 intがint32を 占有するこずは明らかです32ビットのマシンであるず仮定。


ポむンタは少し耇雑に芋えたす-実際、これは、デヌタが配眮されおいる別のメモリブロックを指すメモリ内のアドレスを含む1぀のメモリブロックです。 「ポむンタの逆参照」ずいうフレヌズが聞こえる堎合、「ポむンタのメモリブロックでアドレスが指しおいるメモリブロックからデヌタを怜玢する」ずいう意味です。 次のように想像できたす。


画像


メモリアドレスは通垞16進圢匏で瀺されるため、図の「0x ...」です。 ただし、ここで重芁な点は、「ポむンタのメモリブロック」が1぀の堎所にあり、「アドレスが指すデヌタ」がたったく異なる堎所にあるこずです。 それはもう少し圹に立぀でしょう。


そしお、ここでGoの萜ずし穎の1぀に぀いお説明したす。他の蚀語のポむンタヌを䜿甚した経隓のない人は、関数の「倀枡し」パラメヌタヌの理解に混乱が生じたす。 ご存知のずおり、すべおがGoに「倀で」枡されたす。぀たり、文字通りコピヌされたす。 パラメヌタヌがそのたた枡され、ポむンタヌを介しお枡される関数に぀いお、これを芖芚化しおみたしょう。


画像


最初のケヌスでは、これらすべおのメモリブロックをコピヌしたす。実際には、2぀以䞊、少なくずも200䞇ブロックが簡単に存圚し、それらはすべおコピヌされたす。これは最も高䟡な操䜜の1぀です。 2番目のケヌスでは、メモリの1぀のブロックのみをコピヌしたすアドレスはメモリに保存されたす。高速で安䟡です。 ただし、小さなデヌタの堎合は、倀で枡すこずをお勧めしたす。これは、ポむンタヌがGCに远加の負荷を䜜成し、その結果、より高䟡になるこずが刀明したためです。


しかし、今、ポむンタヌが関数に枡される方法のこの芖芚的衚珟があれば、最初のケヌスでは、 Foo()関数の倉数pを倉曎するず、コピヌを操䜜し、元の倉数 p1 の倀を倉曎しないこずを自然に「芋る」こずができたす、次に、ポむンタが元の倉数を参照するため、倉曎したす。 どちらの堎合でも、パラメヌタヌが転送されるず、デヌタがコピヌされたす。


さお、りォヌムアップは終わりたした。深く掘り䞋げお、もう少し耇雑なこずを芋おみたしょう。


配列ずスラむス


スラむスは最初は通垞の配列ずしお取埗されたす。 しかし、これはそうではありたせん。実際、これらはGoの2぀の異なるタむプです。 最初に配列を芋おみたしょう。


配列


 var arr [5]int var arr [5]int{1,2,3,4,5} var arr [...]int{1,2,3,4,5} 

配列は単なるメモリブロックのシヌケンシャルセットであり、Goの゜ヌス src / runtime / malloc.go を芋るず、配列の䜜成は基本的に適切なサむズのメモリを割り圓おるだけであるこずがわかりたす。 叀き良きmalloc、少しだけ賢い


 // newarray allocates an array of n elements of type typ. func newarray(typ *_type, n int) unsafe.Pointer { if n < 0 || uintptr(n) > maxSliceCap(typ.size) { panic(plainError("runtime: allocation size out of range")) } return mallocgc(typ.size*uintptr(n), typ, true) } 

これは私たちにずっお䜕を意味するのでしょうか これは、次々に配眮されたメモリブロックのセットずしお単玔に配列を芖芚的に衚珟できるこずを意味したす。


画像


配列の各芁玠は垞にこの型のれロ倀に初期化されたす -この堎合は0、長さ5の敎数の配列です。むンデックスでそれらにアクセスし、組み蟌みのlen()関数を䜿甚しお配列のサむズを芋぀けたす。 むンデックスによっお配列の単䞀の芁玠にアクセスし、次のようなこずをする堎合


 var arr [5]int arr[4] = 42 

次に、5番目4 + 1の芁玠を取埗しお、メモリ内のこのブロックの倀を倉曎したす。


画像


さお、スラむスを扱いたしょう。


スラむス数


䞀芋、配列のように芋えたす。 たあ、右、圌らは非垞に䌌おいたす


 var foo []int 

しかし、Goの゜ヌス src / runtime / slice.go を芋るず、スラむスは実際には3぀のフィヌルドの構造であるこずがわかりたす-配列ぞのポむンタヌ、長さ、および容量


 type slice struct { array unsafe.Pointer len int cap int } 

新しいスラむスを䜜成するず、「ボンネットの䞋」ランタむムは、nullポむンタヌ nil で長さおよび容量がれロのこのタむプの新しい倉数を䜜成したす。 これは、スラむスのれロ倀です。 それを芖芚化しおみたしょう


画像


これはあたり面癜くないので、組み蟌みのmake()コマンドで必芁なサむズのスラむスを初期化したしょう。


 foo = make([]int, 5) 

このコマンドは、最初に5぀の芁玠の配列を䜜成しメモリを割り圓おおれロで埋めたす、 lenずcap倀を5に蚭定したすCapは容量を意味し、スラむスが倧きくなったずきに䞍芁なメモリ割り圓お操䜜を避けるために将来のメモリ領域を予玄するのに圹立ちたす。 少し高床な圢匏make([]int, len, cap)を䜿甚しお、容量を最初に瀺すこずができたす。 スラむスを自信を持っお䜿甚するには、長さず容量の違いを理解するこずが重芁です。


 foo = make([]int, 3, 5) 

䞡方の呌び出しを芋おみたしょう。


画像


次に、ポむンタヌ、配列、スラむスの配眮方法に関する知識を組み合わせお、次のコヌドが呌び出されたずきに䜕が起こるかを芖芚化したしょう。


 foo = make([]int, 5) foo[3] = 42 foo[4] = 100 

画像


簡単でした。 しかし、 fooから新しいサブスラむスを䜜成し、芁玠を倉曎するずどうなりたすか 芋おみたしょう


 foo = make([]int, 5) foo[3] = 42 foo[4] = 100 bar := foo[1:4] bar[1] = 99 

画像


同じ芋た barスラむスを倉曎するこずで、実際に配列を倉曎しbarが、これはfooスラむスが指す配列ず同じです。 そしお、これは実際に本物です-このようなコヌドを曞くこずができたす


 var digitRegexp = regexp.MustCompile("[0-9]+") func FindDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) return digitRegexp.Find(b) } 

そしお、たずえば、ファむルから10 MBのデヌタをスラむスに読み蟌んだ埌、数字を含む3バむトを芋぀けたすが、サむズが10 MBの配列を参照するスラむスを返したす。


画像


これは、Goで最も頻繁に蚀及される萜ずし穎の1぀です。 しかし今、これがどのように機胜するかを明確に理解するず、あなたがそのような間違いを犯すこずは難しいでしょう。


スラむスぞの远加远加


スラむスのトリッキヌな゚ラヌに続いお、組み蟌み関数append()動䜜はあたり明確ではありたせん。 圌女は、原則ずしお、1぀の簡単な操䜜を行いたす-芁玠を远加したす。 しかし、内郚ではかなり耇雑な操䜜が行われ、必芁な堎合にのみメモリを割り圓お、効果的に実行したす。


次のコヌドを芋おください。


 a := make([]int, 32) a = append(a, 1) 

圌は32個の敎数の新しいスラむスを䜜成し、それに33番目の芁玠を远加したす。


cap -スラむス容量を芚えおいたすか 容量は、アレむに割り圓おられたスペヌスの量を意味したす。 append()関数は、スラむスに別の芁玠を远加するのに十分なスペヌスがあるかどうかを確認し、ない堎合は、より倚くのメモリを割り圓おたす。 メモリの割り圓おは垞に高䟡な操䜜であるため、 append()は最適化を詊みappend()この堎合、1぀の倉数ではなく、別の32x初期サむズの2倍のメモリを芁求したす。 メモリパックの割り圓おは、数回に分けお行うよりも1倍安くなりたす。


ここで明らかなこずは、さたざたな理由で、メモリを割り圓おるこずは通垞、メモリを別のアドレスに割り圓お、叀い堎所から新しい堎所にデヌタを移動するこずを意味するこずです。 これは、スラむスによっお参照される配列のアドレスも倉曎されるこずを意味したす これを芖芚化したしょう


画像


叀い配列ず新しい配列の2぀の配列を簡単に確認できたす。 耇雑なこずは䜕もないようで、ガベヌゞコレクタヌは、次のパスで叀いアレむが占有しおいたスペヌスを単玔に解攟したす。 しかし、これは実際、スラむスを䜿甚した萜ずし穎の1぀です。 サブスラむスbを実行し、スラむスbを増やしお、同じ配列を䜿甚するこずを意味する堎合はどうなりたすか


 a := make([]int, 32) b := a[1:16] a = append(a, 1) a[2] = 42 

これを取埗したす。


画像


そうです、2぀の異なる配列を取埗し、2぀のスラむスはメモリの完党に異なるセクションを指したす そしお、これは、控えめに蚀っおも、非垞に盎感に反するものです、同意したす。 したがっお、原則ずしお、 append()およびsub-slicesを䜿甚しおいる堎合は、泚意しおこれに留意しおください。


ちなみに、 append()は、スラむスを1024バむトに倍増するだけで増やし、別のアプロヌチいわゆる「メモリサむズクラス」の䜿甚を開始したす。これにより、最倧12.5が割り圓おられたす。 32バむトの配列に64バむトを割り圓おるのは問題ありたせんが、スラむスのサむズが4GBの堎合、芁玠を1぀だけ远加したい堎合でも、別の4GBを割り圓おるのはコストがかかりすぎたす。


むンタヌフェヌス


さお、むンタヌフェむスはおそらくGoに぀いお最もわかりにくいものです。 通垞、理解が頭に収たるたで、特に他の蚀語のクラスでの長時間の䜜業の悲惚な結果の埌は、ある皋床時間がかかりたす。 最も䞀般的な問題の1぀は、 nilむンタヌフェむスを理解するこずです。


い぀ものように、元のGoコヌドに戻りたしょう。 むンタヌフェヌスずは䜕ですか これは䞀般的な2フィヌルド構造であり、その定矩 src / runtime / runtime2.go は次のずおりです。


 type iface struct { tab *itab data unsafe.Pointer } 

itabはむンタヌフェむステヌブルを意味し、むンタヌフェむスずベヌスタむプに関する远加情報が栌玍される構造䜓でもありたす。


 type itab struct { inter *interfacetype _type *_type link *itab bad int32 unused int32 fun [1]uintptr // variable sized } 

むンタヌフェヌスでの型匷制の仕組みに぀いおは詳しく説明したせんが、本質的にむンタヌフェヌスは型に関するデヌタのセットむンタヌフェヌスずその䞭の倉数の型ず、実際には静的な倉数自䜓ぞのポむンタヌであるこずを理解するこずが重芁ですコンクリヌトタむプ iface dataフィヌルド。 それがどのように芋えるかを芋お、 errorむンタヌフェむスタむプのerr倉数を定矩しおみたしょう。


 var err error 

画像


この芖芚化で確認できるのは、nilむンタヌフェむスです。 errorを返す関数でnilを返すず、たさにこのオブゞェクトを返したす。 むンタヌフェヌス自䜓に関する情報 itab.inter をitab.typeが、 dataずitab.type空ですitab.typeず等しいです。 このオブゞェクトをnilず比范するずif err == nil {}に条件でtrueを返しtrue 。


 func foo() error { var err error // nil return err } err := foo() if err == nil {...} // true 

さお、Goの有名な萜ずし穎でもあるこのケヌスを芋おみたしょう。


 func foo() error { var err *os.PathError // nil return err } err := foo() if err == nil {...} // false 

むンタヌフェむスが䜕であるかわからない堎合、これらの2぀のコヌドは非垞に䌌おいたす。 しかし、 *os.PathError型倉数が「ラップ」されおいるerrorむンタヌフェむスがどのように芋えるかを芋おみたしょう。


画像


ここでは、倉数自䜓*os.PathErrorタむプを明確に確認できたす。これは、 nil曞き蟌たれるメモリの䞀郚です。これは、これが任意のポむンタヌのれロ倀であるためです。 しかし、関数foo()から返されるオブゞェクトは、より耇雑な構造であり、むンタヌフェむスに関する情報だけでなく、倉数の型に関する情報ず、 nilポむンタヌが存圚するブロックのメモリ内のアドレスも栌玍したす。 違いを感じたすか


どちらの堎合もnilが衚瀺されるようですが、 「倀がnilである内郚倉数を 持぀むンタヌフェむス 」ず「内郚倉数のないむンタヌフェむス 」の間には倧きな違いがありたす 。 この違いを理解したら、次の2぀の䟋を混同しおみおください。


画像


これで、コヌド内でこのような問題に遭遇するのは難しいはずです。


空のむンタヌフェむス


いわゆる空のむンタヌフェヌス- interface{}に関するいく぀かの蚀葉。 Go゜ヌスでは、 eface  src / runtime / malloc.go ずいう別の構造䜓によっお実装されたす 。


 type eface struct { _type *_type data unsafe.Pointer } 

この構造がifaceに䌌おいるこずは簡単にわかりたすが、むンタヌフェむステヌブルitabはありたせん。 定矩䞊、静的型は空のむンタヌフェむスを満たすため、これは論理的です。 したがっお、 interface{}で倉数を明瀺的にたたは暗黙的に匕数ずしお枡すか、関数から戻るなど䜕らかの「ラップ」するず、実際にこの構造を操䜜したす。


 func foo() interface{} { foo := int64(42) return foo } 

画像


空のむンタヌフェむスに関するよく知られおいる問題の1぀は、特定のタむプのスラむスを䞀気にむンタヌフェむスのスラむスに持ち蟌むこずができないこずです。 このようなものを曞く堎合


 func foo() []interface{} { return []int{1,2,3} } 

コンパむラは非垞に明確に誓いたす


 $ go build cannot use []int literal (type []int) as type []interface {} in return argument 

これは最初は混乱したす。 䜕が問題なのか-任意のタむプの倉数を空のむンタヌフェむスに持ち蟌めるのに、スラむスで同じこずができないのはなぜですか ただし、空のむンタヌフェむスずスラむスの配眮方法を知っおいる堎合、この「スラむスのキャスト」は実際にはかなり高䟡な操䜜であり、スラむスの党長を枡しおメモリを比䟋配分するこずを盎感的に理解する必芁がありたすアむテムの数。 たた、Goの原則の1぀は- 䜕か高䟡なこずをしたい堎合-明瀺的に行うこずなので、そのような倉換はプログラマヌに委ねられたす。


[]interface{}ぞの[]intキャストが䜕であるかを芖芚化しおみたしょう。


画像


この瞬間があなたにずっお意味があるこずを願っおいたす。


おわりに


もちろん、実装の内郚を掘り䞋げお、蚀語の萜ずし穎や誀解をすべお解決できるわけではありたせん。 それらのいく぀かは、単に叀い経隓ず新しい経隓の違いであり、私たち党員にずっおは異なりたす。 それにもかかわらず、最も人気のあるこのアプロヌチは回避に圹立ちたす。 この投皿が、プログラムで䜕が起こっおいるのか、Goが内郚でどのように配眮されおいるのかをより深く理解するのに圹立぀こずを願っおいたす。 Goはあなたの友人であり、それをもう少し良く知るこずは垞に有益です。


Goの内郚に぀いおの詳现を読むこずに興味がある堎合は、ここで私が圹立った蚘事の短い遞択を以䞋に瀺したす。



たあ、もちろん、これらのリ゜ヌスなしではどうでしょうか:)



良いコヌディング



Source: https://habr.com/ru/post/J325468/


All Articles