【golangシリーズ】golangのまとめを一気に復習する

参考リンク:1.1 Go の = と := の違いは何ですか? — Go 言語面接ガイド 1.0.0 ドキュメント (iswbm.com)

この記事は、不足点を埋めることを目的としたアピール リンクの概要レビューです。以下にリストされているコードの一部は上記の Web サイトからのものであり、この記事ではそこから教訓を引き出します。

 基本

1. go における = と := の違い

= は代入にのみ使用され、前に宣言する必要があります。

:= は宣言と代入に使用できますが、関数内に使用する必要があります

2. go におけるポインターの意味は何ですか? unsafe.Pointer ではなく &var を指します。

(1) メモリの節約 Golang は値渡しなので、パラメータを配列で渡すと毎回コピーされてメモリを多く消費します。

(2) コーディングが容易 ポインタ型を使用しない場合、復帰時に新しいオブジェクトを作成する必要がある場合があります。

3. Goにおける複数の戻り値の役割

複数変数の戻り値はプログラムが簡単で、a, b = swap(b, a) などの中間変数の宣言が不要になります。

もう 1 つあり、これはエラーがあるかどうかを判断するためによく使用されるため、戻り値に加えて追加のエラーを定義する必要があります。

4. go には例外タイプがありますか?

Go には例外タイプ (panic) はなく、エラータイプ (error) のみがあり、Go のフィールドとエラーは相互に変換できます。

(1) エラーから例外へ: たとえば、プログラム ロジックは、特定の URL へのリクエストを最大 3 回試行します。3 回の試行中にリクエストが失敗するとエラーになります。3 回目の試行後にリクエストが失敗すると、失敗が昇格されます。例外に。

(2) 例外からエラー:例えば、パニックによる例外が回復した後、戻り値にエラータイプの変数が代入され、上位層関数がエラー処理を続行できるようになります。

5. ルーンとバイトの違いは何ですか?

バイトは 8 ビットで、これは uint8 のエイリアス タイプですが、バイトで表現できるのは 1 つの ASCII 文字のみです。ルーンは 32 ビットで、これは uint32 のタイプであり、4 バイトを含むため、より多くのエンコーディングを表現できます。

6. Go の浅いコピーと深いコピーとは何ですか?

浅いコピーでは、データ アドレスがコピーされ、オブジェクトへのポインタのみがコピーされます。参照型はデフォルトで浅いコピーです。深いコピーでは、メモリ アドレスが再度開かれ、同じ値が初期化されます。

たとえば、:= []int{1,2,3}

b := ab のアドレスは a と同じです

copy(c, a) c は新しいアドレスを開くことです

7. リテラルおよび結合リテラルとは何ですか

リテラルは、「abc」、0xF 0i17 0b111 などの名前のない定数です。

リテラルと変数の違いは、リテラルはアドレスを取得できないのに対し、変数はアドレスを取得できることです。

func foo() string{
    return "hello"
}
func main(){
    fmt.Println(&foo())  //失败,返回的hello是字面量,无法取地址
    a := foo()
    fmt.Println(&a) //成功,此时是一个变量
}

組み合わせリテラルは、構造体、配列、スライス、マップの値を構築し、毎回新しい値を作成します。これらは、リテラル型とそれに続く中括弧と要素のリストで構成されます。必要に応じて、各要素の前に関連するキーを付けることができます。いわゆる結合リテラルは、実際にはオブジェクトの定義と初期化をまとめます。

type Profile struct {
    Name string
    Age int
    Gender string
}

func main() {
    // 声明 + 属性赋值
    xm := Profile{
        Name:   "iswbm",
        Age:    18,
        Gender: "male",
    }
}

プロファイル タイプを初期化する場合、値が定義されて初期化されます。

8. オブジェクトセレクターは自動的に逆参照します

この演算子はセレクターと呼ばれ、ポインターまたはオブジェクト変数を自動的に見つけて、オブジェクトのサブオブジェクトにアクセスします。メソッドを定義するときは、レシーバーを定義する必要があります。

type Profile struct {
    Name string
}
func(p *Profile) say)(){
    fmt.Pritnln(p.Name)
}

func main() {
    p1 := &Profile{"iswbm"}
    fmt.Println((*p1).Name)  // output: iswbm   正常写法
    fmt.Println(p1.Name)  // output: iswbm   自动识别指针对象
}

9. マップはアドレス指定できません。値のプロパティを変更するにはどうすればよいですか?

package main

type Person struct {
    Age int
}

func (p *Person) GrowUp() {
    p.Age++
}

func main() {
    m := map[string]Person{
        "iswbm": Person{Age: 20},
    }
    m["iswbm"].Age = 23
    m["iswbm"].GrowUp()
}

map の値はアドレス指定できないため、m["iswbm"] は同じアドレスへの参照ではなく、元の値のコピーを返すため、直接変更することはできません。

p := m["iswbm"] などの変数を使用して継承し、変更することも、ポインターを使用して "iswbm": &person{Age:20} を処理することもできます。

10. 型付き定数と型なし定数の違い

型なし定数を変数に代入すると、型なし定数は対応する型に暗黙的に変換されます。ただし、型付き定数がある場合、変換は実行されません。代入中に型がチェックされます。パスすると、エラーが直接報告されます。

// 无类型常量
const RELEASE = 3
const RELEASE2 int  = 3
func main() {
    var x int16 = RELEASE
    var y int32 = RELEASE
    fmt.Printf("type: %T \n", x) //type: int16
    fmt.Printf("type: %T \n", y) //type: int32
    var x int16 = RELEASE2 //报错,无法显式转换
}

11. パラメータを渡すときに配列ではなくスライスを使用するのはなぜですか?

配列は値型であり、スライスは参照型であるため、渡されるパラメータが配列である場合、毎回コピーされるため、メモリを大量に浪費することになります。毎回スライスを渡す場合、実際に渡されるのはへのポインタです。ヒープ配列ポインタを使用すると、多くのスペースを節約できます。もちろん、ポインターの配列を渡すこともできます。

12. Go 言語でのホット パスの用途は何ですか?

ホット パスは、その名前が示すように、頻繁に実行されるプログラム内のコードです。目的は、頻繁にアクセスされる一部のコードを最適化することで、明らかな結果が得られます。

  • 構造体の最初のフィールドにアクセスする必要がある場合、ポインターを直接逆参照して最初のフィールドにアクセスできます。
  • 他のフィールドにアクセスする場合は、構造体ポインターに加えて、最初のフィールドへのオフセットも指定する必要があります。

マシンコードでは、このオフセットは命令に渡される追加の値であり、これにより命令が長くなります。パフォーマンスへの影響は、CPU がアクセスするフィールドのアドレスを取得するために構造体ポインターにオフセットを追加する必要があることです。

したがって、構造体の最初のフィールドにアクセスするマシン コードは、より高速かつコンパクトになります。

13. 参照型とポインタの違いは何ですか?

スライスは参照型であり、引数として渡すと実引数に変更が反映されますが、関数内で展開した場合は新しく開いた引数を指しているため実引数には反映されません。メモリ アドレスであり、元のデータ関数は依然として古いアドレスを指しているため、再度ポインタを返す必要があります。

14. go は渡すこと、参照を渡すこと、またはポインタを渡すことを意味しますか?

Go 言語は、参照渡しやポインタ渡しではなく、値渡しがすべてです。参照型のみがアドレスの値を渡します。

15. Go でどれがアドレス可能でどれがアドレス不可能か

& 演算子を使用してアドレスを取得できるオブジェクトはアドレス指定可能ですが、使用できないオブジェクトはアドレス指定できません。

アドレス指定可能: 変数、ポインター、配列要素、スライス、スライス要素、結合リテラル (構造体) は、主に次のフィールドをアドレス指定できる機能を指します。

アドレス指定不可: 定数、文字列、関数およびメソッド、プリミティブ型リテラル、マップ内の要素、配列リテラル

上級編

1. スライス拡張後のメモリ計算方法

(1) まずサイズ 2 のスライスを初期化し、次に 3 つの要素を追加します。この時点では要素は 5 になっているはずです。拡張は、サイズが 1024 未満の場合は 2 倍、1024 を超える場合は 1.25 倍になります。ただし、それでも拡張後の長さより短い場合は、拡張後の長さが優先されます。

(2) 拡張する場合、メモリ割り当ての問題を考慮する必要があります。メモリ割り当ては常に 2^n に割り当てられます。たとえば、32 から 48 の間の 40 バイトを割り当てる必要があります。割り当てられるのは 48 バイトだけです (たとえ番号* 8)

2. ゴルーチンの存在意義は何ですか?

実際には 2 種類のスレッドがあります。

  • 1 つは従来のオペレーティング システム スレッドです (作成と切り替えをカーネルに入力する必要があり、多大なコストがかかります)。
  • 1 つは、プログラミング言語で実装されたユーザー モード スレッドで、コルーチンとも呼ばれます。Go では、ゴルーチンです (一般的なスレッドは、スタック オーバーフローを避けるために最初に比較的大きなスタック メモリを割り当てますが、ゴルーチンにはデフォルトで 2K しかなく、自動的にメモリを割り当てることができます)縮んだり広がったりします。)

したがって、ゴルーチンの存在は、オペレーティング システムのスレッドの欠点の一部 (重すぎる) を別の方法で解決するためにあるはずです。

3. Go クロージャの基礎となる原則について話す

外部ローカル変数を参照する関数はクロージャと呼ばれます。次のコードの defer 関数は、ローカル変数 i を参照します。

クロージャ内で参照される外部ローカル変数は、加算関数が戻ったときにスタックから破棄されません。

import "fmt"

func func1() (i int) {  //i在外部还要使用,所以变量是在堆上申请的
    i = 10
    defer func() {
        i += 1
    }()
    return 5
}
func func2() (int) {
    i := 10
    defer func() {
        i += 1
    }()
    return i    //在返回值写了变量名,返回值是在上级的栈内存申请的,直接赋值该变量,所以func2的i是在外面的栈上
}

func main() {
    closure := func1()
    fmt.Println(closure)
}

(1) クロージャ関数で参照される外部変数がヒープ メモリに適用されるかスタック メモリに適用されるかは、クロージャ関数が関数 Return の後の他の場所で使用されるかどうかによって決まります。使用される場合はヒープに適用されます。そうでない場合は、ヒープに適用されます。スタックに適用します。

(2) クロージャ関数では、参照される外部変数は値のコピーではなく、値へのポインタを格納します。

(3) 関数の戻り値に変数名を記述した場合、その変数は上位スタックメモリに適用され、戻り値は変数に直接代入されます。

4. 遅延変数スナップショットはいつ無効になりますか?

func func1() {
    age := 0
    defer fmt.Println(age) // output: 0
    /*
    defer func() {
        fmt.Println(age)
    }()
    */
    age = 18
    fmt.Println(age)      // output: 18
}


func main() {
    func1()
}

defer の後に単一行の式 (コメントなし) が続く場合、defer の age は func1 関数スタックの defer の前の age の値をコピーするだけです。

defer の後にクロージャー関数 (コメント化されたコンテンツ) が続く場合、defer の age は、func1 関数スタックに age のポインターのみを格納します。

5. Go のプリエンプティブ スケジューリングの理解

go バージョン 1.1 では、コルーチンが CPU リソースを積極的に放棄した場合にのみ、次のコルーチンのスケジューリングをトリガーできます。

バージョン 1.2 では、sysmon 監視スレッドがコルーチンの実行が長すぎることを検出した場合 (gc または stw の場合)、プリエンプション マークを設定します。コルーチンが呼び出し関数で morestack のロジックを取得するときに、プリエンプションマークがあれば、スケジューリングのメインスレッドを切り替えます。

1.14 以降、シグナルベースのプリエンプティブ スケジューリングは、コルーチンが一定時間 (10 秒) を超える限り、CPU の実行権を強制的に奪います。

https://github.com/golang/gofrontend/blob/20e74f9ef8206fb02fd28ce3d6e0f01f6fb95dc9/libgo/go/runtime/proc.go#L4938

6. goスタックスペースの拡張/縮小プロセス

go のコルーチンの初期スタックは 2k で、呼び出しレベルが増加すると、スタック領域の拡張がトリガーされます。

拡張トリガー:

関数を呼び出すと、runtime.morestack がチェックされ、ゴルーチンのメモリが十分であるかどうかがチェックされます。十分でない場合は、runtime.newstack が呼び出され、新しいスタックが作成されます。新しいスタックのサイズは、現在のスタックのサイズの 2 倍です。古いスタック。最大スタック領域は maxstacksize の 1G を超えることはできません。

縮小トリガー:

スタック スペースは、関数が返された後にリサイクルされます。返される量が多すぎる場合は、メモリ使用率が低すぎます。ガベージ コレクション中に、スタック スペースのメモリ使用率がチェックされます。25% 未満の場合は、メモリ使用率が 25% に削減されます。元のサイズの 50% ですが、最小値は少なくとも 2K です。

ただし、拡張でも縮小でも、 runtime.copystack が呼び出されて新しい領域が開かれ、古いスタックのデータが新しいスタックにコピーされ、ポインタの位置が調整されます。

7. GMP の原則について話す

2.7 GMP モデルの原則について話しましょう — Go 言語インタビュー ガイド 1.0.0 ドキュメント (iswbm.com)

G は go が提供する軽量スレッドの Goroutine で、各 goroutine には約 4K のメモリが必要です。

M は Thread (Machine の略称) で、オペレーティング システムのカーネル スレッドに対応し、long の数は 1W を超えません。

P はプロセッサ、プロセッサ、ゴルーチンとスレッドを調整するために使用され、gomaxprocs を通じて設定されます

グローバル キューとローカル キューの 2 つのキュー (各 p に 1 つのキュー)

各 P は M にバインドされており、G を格納するための独自のキューを持っています。キューが空の場合、実行のためにグローバル キューから自動的に G を取得します。グローバル キューが空の場合、他の人のキューから G を取得します。 。

戦略:

スレッドを再利用すると、スレッドの頻繁な作成と破棄を回避できますが、スレッドは再利用されます。

作業盗用メカニズムはスレッドを破壊する代わりに G を盗むことができます

ハンドオフ メカニズムでは、G がシステム コールによってブロックされたときに、P を他の回線制御スレッドに転送して実行できます。

gomaxprocs は CPU コア数/2

他のゴルーチンが餓死するのを防ぐために、ゴルーチンは最大 10 ミリ秒かかります。これは先制スケジューリングです。

8. GMP モデルに P が必要なのはなぜですか?

v1.1ではPが無く、Mはグローバルキューから直接Gを取得して実行しますが、Pを追加するとグローバルキューへの依存度が大幅に軽減され、ワー​​クスチールやハンドオフが実現できます。

9. メモリを割り当てないポインタ型は使用できますか?

できない

package main

import (
    "fmt"
)

func main() {
    var i *int
    i = new(int)  //如果没有分配空间,就会出错
    *i=10
    fmt.Println(*i)
}

10. 型をキャストするときにメモリのコピーを回避する方法

[]byte() と string の切り替え。string の最下層は []byte ポインタです。これを直接変換すると、直接アドレスが割り当てられます。

11.Go の GC の進化

go v1.3 より前は、マークとクリアが使用され、最初に stw、次にすべてのオブジェクトをトラバースし、到達不能なマークが白になり、その後クリーンアップされていましたが、時間がかかりすぎました。

Go 1.5 では 3 色マーキング方式が採用され始めました。

[コレクターズエディション] Golang の 3 色マークと混合ライトバリア GC モードの完全なグラフィックとテキスト分析 - 個人記事 - SegmentFault Sifu

黒: すべてのオブジェクトとプロパティが参照され、走査されます。

白: まだテストされていません

グレー: 検出は参照されていますが、まだ走査されていない属性が存在します。

マーキング中にオブジェクトが参照または逆参照されるのを防ぐために、挿入バリアと削除バリアを使用できます。

挿入バリア: A が B を参照したい場合、B は灰色でマークされます。

バリアの削除: 上級レベルがグレーまたは白の場合、グレーとしてマークします。

12. Go のどのアクションが実行時スケジュールをトリガーしますか?

(1) スリープ、ディスク読み取り、ネットワークリクエストなどのシステムコール。

(2) ロックとチャネルを待機している場合、それらはブロックされているため、スケジューリングもトリガーされます。

(3) runtime.Gosched を呼び出す

原則

1. ローカル変数はスタックまたはヒープに割り当てられますか?

ヒープ メモリ: メモリ アロケータとガベージ コレクタによって再利用されます。

スタックメモリ: コンパイラによって自動的に割り当ておよび解放されます。

プログラムの実行中は複数のスタック メモリが存在する可能性がありますが、ヒープ メモリは確実に 1 つだけです。

各スタック メモリはスレッドまたはコルーチンによって独立して占有されるため、スタックからメモリを割り当てるためのロックは必要なく、関数終了後にスタック メモリは自動的にリサイクルされ、ヒープ メモリよりもパフォーマンスが高くなります。

ヒープメモリについてはどうですか? 複数のスレッドまたはコルーチンが同時にヒープからメモリを申請する可能性があるため、ヒープ内のメモリを申請するには競合を避けるためにロックする必要があり、関数終了後にヒープ メモリには GC (ガベージ コレクション) の介入が必要です。 GC 操作の回数が多いと、プログラムのパフォーマンスが大幅に低下します。

一般に、ローカル変数のスコープは関数内のみです。関数が戻ると、すべてのローカル変数によって占有されていたメモリ領域は再利用されます。そのような変数については、メモリ領域がスタックから割り当てられます。

ただし、ローカル変数が関数の外で使用され続ける場合は、ヒープ上に割り当てられ、Go コンパイラーが自動的に最適化します。

2. 定数、文字列、辞書がアドレス指定できないのはなぜですか?

アドレス不能とは、& を通じてメモリ アドレスを取得できないことを意味します。

定数がアドレス指定可能な場合、定数の値はポインターを介して変更できますが、これは要件を満たしません。

マップがアドレス指定可能な場合、存在しない要素に遭遇してマップが展開されるとアドレスが変更され、アドレス指定は無意味になります。

文字列は変更されるたびに新しいアドレスを指すため、アドレス指定は無意味です。

3. スライス要素はなぜアドレス指定可能ですか?

スライスは匿名配列に要素を格納します。配列の要素はアドレス指定可能であるため、スライスの要素もアドレス指定可能である必要があります。

4. Go のデフォルトのスタック サイズはどれくらいですか? 最大値はいくらですか?

  • v1.0 ~ v1.1 — 最小スタック メモリ スペースは 4KB です。
  • v1.2 — 最小スタック メモリを 8KB に増加しました。
  • v1.3 — 以前のバージョンのセグメント化されたスタックを連続スタックに置き換えます。
  • v1.4 — 最小スタック メモリを 2KB に下げました。
  • この初期値と最大値については、実際には Go のソースコード runtime/stack.go で確認できます。
  • デフォルトのスタック サイズは 2K、最大は約 1G
  • // rumtime.stack.go
    // The minimum size of stack used by Go code
    _StackMin = 2048
    
    var maxstacksize uintptr = 1 << 20 // enough until runtime.main sets it for real

5. Go におけるセグメント化スタックと連続スタックの違いは何ですか?

初期の Go バージョンで使用されるスタックはセグメント化されたスタックであり、Goroutine のレベルが上がると、 runtime.morestack と runtime.newstack が呼び出され、二重リンク リストで接続された新しいスタック領域が作成されます。ただし、関数がループ内で呼び出される場合、割り当てと解放が継続し、膨大な追加のオーバーヘッドが発生します。これは、ホット分類問題と呼ばれます。

現在の Go バージョンは連続スタックを使用しており、古いスタックの 2 倍のサイズの新しいスタックを初期化し、すべての値を新しいスタックに移行します。次の手順があります。

(1) runtime.newstack を呼び出して、メモリ空間内により大きなスタック メモリ空間を割り当てます。

(2) runtime.copystack を使用して、古いスタックのすべての内容を新しいスタックにコピーします。

(3)古いスタックの対応する変数へのポインタを新しいスタックにリダイレクトする。

(4) runtime.stackfree を呼び出して、古いスタックのメモリ領域を破壊して再利用します。

6. メモリ アライメントとメモリ レイアウトとは何ですか?

ワード長とは、CPU が一度にアクセスできるデータの最大長を指し、32 ビット CPU の場合は 4 バイト、64 ビット CPU の場合は 8 バイトになります。

メモリ レイアウトが適切であれば、次のオブジェクトの占有メモリ サイズは 13 (4+8+1) です。2 番目のサイズが 3 ではなく 8 なのはなぜですか? ポインタのメモリ アライメント係数が 8 であるためですが、 Bar.y にアクセスするには、メモリに 2 回アクセスする必要があります

ただし、メモリ レイアウトがワード長の場合、CPU はメモリに 1 回アクセスするだけで済みます。最初の int32 は 4 バイトを占有しますが、64 ビット マシン上で実行されているため、実際には 8 バイトを占有し、最後の bool も同様です8 バイト、最初に int32 bool *Foo を指定した場合、16 バイトのスペースしか使用できません

type Foo struct {
    A int8 // 1
    B int8 // 1
    C int8 // 1
}

type Bar struct {
    x int32 // 4
    y *Foo  // 8
    z bool  // 1
}

7. Go で平等性を比較するにはどうすればよいですか?

Interface{} を比較する場合は、それが所有するフィールドのタイプとデータが比較されます。

(1) 型と値が等しい場合、2 つのインターフェースは等しい。{}

(2) type と data の両方が unset 状態にある場合、インターフェイスは nil であり、この時点でも等しいです。

(3)interface{} と特定の型の変数を比較する場合、interface{} 以外はインターフェイスに変換されて比較され、型と値が比較されます。

(4) nil 自体と比較すると、等しくありません。

package main

import (
    "fmt"
    "reflect"
)

func main()  {
    var a *string = nil
    var b interface{} = a

    fmt.Println(b==nil) // false
}
a转化为interface后是(type=nil, data=nil),但是b实际上是(type=*string, data=nil)这是不一样的

8. すべての T タイプには *T タイプがありますか?

いわゆる *T 型は T 型へのポインタですが、前述したように、アドレス指定できないコンテンツへのポインタを構築することは不可能です。たとえば、定数、マップ値、文字列などです。エラーは次のように報告されます。

package main

import "fmt"

type T string

func (T *T) say() {
    fmt.Println("hello")
}

func main() {
    const NAME T = "iswbm"
    NAME.say()
}

9. スライスに対する配列の利点は何ですか?

配列は固定長であるため、コンパイラはコンパイル中に配列が範囲外であるかどうかをチェックできます。

長さは型の一部であり、リフレクション検査中に、それが正当な配列であるかどうかを直接検証できます。

リフレクト.TypeOf(配列1) == リフレクト.タイプOf(配列2)

同じ型の配列を比較できるため、固定長配列をマップキーとして使用できます。

10. なぜ GMP は G を盗むのにロックする必要がないのですか?

ローカル列から G を取得する GMP の操作は GAS 操作であり、アトミックであり、同時実行性の競合なしにハードウェアによって直接サポートされます。ロックはオペレーティング システム レベルで実装されます。

ただし、CAS の操作は簡単ですが、一定のコストがかかります。

    1. CAS が正常に実行されることを確認するには、for ループが成功するまで試行を続けてから戻る必要があるため、他のハードウェアによる CPU へのアクセスがブロックされ、大きなオーバーヘッドが発生します。
    2. 各 CAS アトミック操作は、1 つの共有変数に対してのみ操作できます。

おすすめ

転載: blog.csdn.net/u013379032/article/details/132417420