banner
lee

lee

Goノート

go ノート#

基礎#

変数#

var 変数名  =

ここで「型」または「= 式」の 2 つの部分のうちの 1 つを省略できます。省略したのが型情報であれば、初期化式に基づいて変数の型情報を推測します。初期化式が省略された場合、その変数はゼロ値で初期化されます。数値型変数のゼロ値は 0、ブール型変数のゼロ値は false、文字列型のゼロ値は空文字列、インターフェースまたは参照型(スライス、ポインタ、マップ、チャネル、関数を含む)変数のゼロ値は nil です。配列や構造体などの集約型のゼロ値は、各要素またはフィールドがその型のゼロ値であることです。

make、new#

i := 0
u := user{}

両者はメモリの割り当て(ヒープ上)ですが、make はスライス、マップ、チャネルの初期化(非ゼロ値)のみに使用されます。一方、new は型のメモリ割り当てに使用され、メモリはゼロに設定されます。make が返すのはこれら 3 つの参照型そのものであり、new が返すのは型へのポインタです。new はあまり使用されず、通常は短い文の宣言や構造体のリテラルを使用して目的を達成します。

range#

Go 言語の range キーワードは、for ループで配列(array)、スライス(slice)、チャネル(channel)、またはマップ(map)の要素を反復処理するために使用されます。配列とスライスでは、要素のインデックスとインデックスに対応する値を返し、集合では key-value ペアを返します。

スライス#

// 初期要素数が5の配列スライスを作成し、要素の初期値は0:
mySlice1 := make([]int, 5) 
// 初期要素数が5の配列スライスを作成し、要素の初期値は0で、10個の要素のストレージスペースを予約:
mySlice2 := make([]int, 5, 10) 
// スライスリテラルで長さ5、容量5のスライスを作成します。注意が必要なのは、[ ]の中に配列の容量を書かないことです。書いた場合、数が書かれた後は配列になり、スライスではなくなります。
mySlice3 := []int{10,20,30,40,50}

関数#

  1. 関数の構文形式:

    func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) {
        // ここは処理ロジックコード
        // 複数の値を返す
        return value1, value2
    }
    
    • func:関数は func で宣言されます
    • funcName:関数名、関数名とパラメータリストが一緒に関数シグネチャを構成します
      • 小文字:同じパッケージ内で呼び出せるが、異なるパッケージでは呼び出せない
      • 大文字:異なるパッケージ内で呼び出せる(import パッケージを通じて)
    • parametername type:パラメータリスト
    • output type:戻り値、Go 言語は 1 つの関数が複数の値を返すことをサポートしています
  2. 値渡し

  3. 参照渡し

    参照渡しは本質的に値渡しであり、この値はポインタ(アドレス)です。

    Go 言語のスライス、マップ、チャネルの 3 つの型の実装メカニズムはポインタに似ているため、アドレスを取得してポインタを渡す必要はありません。

  4. defer

    関数内に defer 文を追加すると、defer は後入れ先出しのモードを採用します。

    func ReadWrite() bool {
        file.Open("file")
        defer file.Close()
    }
    

    落とし穴:

    func testDefer() (r int) {
    	x := 1
    	defer func(xPtr *int) {
    		*xPtr++
    	}(&x)
    	return x
    }
    

    return x は原子命令ではなく、実際には x=r def return です。

    defer は後入れ先出しであり、ゴルーチンが panic に遭遇した場合、そのゴルーチンの defer リストを走査し、defer を実行します。recover に遭遇しなければ、そのゴルーチンの defer リストを走査し終えた後、stderr に panic 情報を出力します。

  5. メソッド

    Go 言語には関数とメソッドが同時に存在します。メソッドは受取人を含む関数であり、受取人は命名型または構造体型の値またはポインタであることができます。

  6. 継承

    type Human struct {
    	Name	string
    	Age		int
    	Phone	string
    }
    
    type Employee struct {
    	Human
    	Salary   int
    	Currency string
    }
    
  7. インターフェース

    // インターフェースMenを定義
    type Men interface {
    	SayHi()
    	Sing(lyrics string)
    }
    
    • インターフェースは任意のオブジェクトによって実装できます
    • 1 つのオブジェクトは任意の数のインターフェースを実装できます
    • 任意の型は空のインターフェースを実装しています
    • 構造体がインターフェースを実装している場合、インターフェース型の変数 i は構造体型に代入でき、またこの変数 i からその構造体型の値を取り出すことができます。

エラー#

関数またはメソッドがエラーを返す場合、それは関数が返す最後の値でなければなりません。

エラー処理の方法:返されたエラーを nil と比較し、nil 値はエラーが発生していないことを示します。

goroutine#

main 関数をカプセル化した goroutine は主 goroutine と呼ばれます。

  • 各 goroutine が申請できるスタックスペースの最大値を設定します。
  • goroutine が終了する際に必要な後処理を行うための特別な defer 文を作成します。
  • バックグラウンドでメモリのゴミを掃除するための goroutine を起動し、gc が使用可能であることを示すフラグを設定します。
  • main パッケージ内の init 関数を実行します。
  • main 関数を実行します。
  • goroutine が実行時 panic を引き起こしているかどうかを確認します。
  • 最後に主 goroutine は自分自身と現在のプロセスの実行を終了します。

チャネル#

  • チャネルを使用して複数の戻り値を返します。

  • for range を使用して構文を簡素化します。

  • 閉じたチャネルに値を送信すると panic が発生します。

  • 閉じたチャネルから受信すると、チャネルが空になるまで値を取得し続けます。

  • 値のない閉じたチャネルから受信操作を実行すると、対応する型のゼロ値が得られます。

  • すでに閉じたチャネルを閉じると panic が発生します。

  • バッファなしのチャネル (make (chan int)) は、受信者が必要です。

  • チャネルを閉じる

    1 つの受信者、1 つの送信者、送信者はデータチャネルを閉じます。

    func main() {
        rand.Seed(time.Now().UnixNano())
        log.SetFlags(0)
    
        // ...
        const MaxRandomNumber = 100000
        const NumReceivers = 100
    
        wgReceivers := sync.WaitGroup{}
        wgReceivers.Add(NumReceivers)
    
    // 送信者
    go func() {
        for {
            if value := rand.Intn(MaxRandomNumber); value == 0 {
                // 唯一の送信者は安全にチャネルを閉じることができます。
                close(dataCh)
                return
            } else {            
                dataCh <- value
            }
        }
    }()
    
    // 受信者
    for i := 0; i < NumReceivers; i++ {
        go func() {
            defer wgReceivers.Done()
    
            // dataChが閉じられるか、dataChのデータキャッシュキューが空になるまでデータを受信します。
            for value := range dataCh {
                log.Println(value)
            }
        }()
    }
    
    

    1 つの受信者、n 個の送信者、受信者は信号チャネルを閉じます。

    package main
    
    import (
        "time"
        "math/rand"
        "sync"
        "log"
    )
    
    func main() {
        rand.Seed(time.Now().UnixNano())
        log.SetFlags(0)
    
        // ...
        const MaxRandomNumber = 100000
        const NumSenders = 1000
    
        wgReceivers := sync.WaitGroup{}
        wgReceivers.Add(1)
    
        // ...
        dataCh := make(chan int, 100)
        stopCh := make(chan struct{})
            // stopChは信号チャネルです。
            // その送信者はdataChの受信者です。
            // その受信者はdataChの送信者です。
    
        // 送信者
        for i := 0; i < NumSenders; i++ {
            go func() {
                for {
                    // 最初のselectは、goroutineをできるだけ早く終了させるためのものです。
                    // 実際には、この特別な例では、これは必要ないので省略できます。
                    select {
                    case <- stopCh:
                        return
                    default:
                    }
    
                    // stopChが閉じられていても、dataChに送信する操作がブロックされていなければ、2番目のselectの最初の分岐は、いくつかのループの中で実行されない可能性があります。
                    // しかし、ここでの例では許容されるため、上記の最初のselectコードブロックは省略できます。
                    select {
                    case <- stopCh:
                        return
                    case dataCh <- rand.Intn(MaxRandomNumber):
                    }
                }
            }()
        }
    
        // 受信者
        go func() {
            defer wgReceivers.Done()
    
            for value := range dataCh {
                if value == MaxRandomNumber-1 {
                    // dataChチャネルの受信者もstopChチャネルの送信者です。
                    // ここで停止チャネルを閉じるのは安全です。
                    close(stopCh)
                    return
                }
    
                log.Println(value)
            }
        }()
    
        // ...
        wgReceivers.Wait()
    }
    

    m 個の受信者、n 個の送信者、1 つの受信者がホストに通知して信号チャネルを閉じます。

    package main
    
    import (
        "time"
        "math/rand"
        "sync"
        "log"
        "strconv"
    )
    
    func main() {
        rand.Seed(time.Now().UnixNano())
        log.SetFlags(0)
    
        // ...
        const MaxRandomNumber = 100000
        const NumReceivers = 10
        const NumSenders = 1000
    
        wgReceivers := sync.WaitGroup{}
        wgReceivers.Add(NumReceivers)
    
        // ...
        dataCh := make(chan int, 100)
        stopCh := make(chan struct{})
            // stopChは信号チャネルです。
            // その送信者は以下のホストgoroutineです。
            // その受信者はdataChのすべての送信者と受信者です。
        toStop := make(chan string, 1)
            // toStopチャネルは通常、ホストに信号チャネル(stopCh)を閉じるように通知するために使用されます。
            // その送信者はdataChの任意の送信者と受信者です。
            // その受信者は以下のホストgoroutineです。
    
        var stoppedBy string
    
        // ホスト
        go func() {
            stoppedBy = <-toStop
            close(stopCh)
        }()
    
        // 送信者
        for i := 0; i < NumSenders; i++ {
            go func(id string) {
                for {
                    value := rand.Intn(MaxRandomNumber)
                    if value == 0 {
                        // ここで、信号チャネルを閉じるようにホストに通知するためのトリックです。
                        select {
                        case toStop <- "sender#" + id:
                        default:
                        }
                        return
                    }
    
                    // ここでの最初のselectは、できるだけ早くこのgoroutineを終了させるためのものです。
                    // このselectコードブロックには1つの受信行為のcaseと、Goコンパイラのtry-receive操作として特別に最適化されるデフォルト分岐があります。
                    select {
                    case <- stopCh:
                        return
                    default:
                    }
    
                    // stopChが閉じられていても、dataChからデータを受信する操作がブロックされなければ、2番目のselectの最初の分岐は、いくつかのループ(理論的には永遠に)実行されない可能性があります。
                    // これが、上記の最初のselectコードブロックが必要な理由です。
                    select {
                    case <- stopCh:
                        return
                    case dataCh <- value:
                    }
                }
            }(strconv.Itoa(i))
        }
    
        // 受信者
        for i := 0; i < NumReceivers; i++ {
            go func(id string) {
                defer wgReceivers.Done()
    
                for {
                    // 送信者goroutineと同様に、ここでの最初のselectはできるだけ早くこのgoroutineを終了させるためのものです。
                    // このselectコードブロックには1つの送信行為のcaseと、Goコンパイラのtry-send操作として特別に最適化されるデフォルト分岐があります。
                    select {
                    case <- stopCh:
                        return
                    default:
                    }
    
                    // stopChが閉じられていても、dataChからデータを受信する操作がブロックされなければ、2番目のselectの最初の分岐は、いくつかのループ(理論的には永遠に)実行されない可能性があります。
                    // これが、上記の最初のselectコードブロックが必要な理由です。
                    select {
                    case <- stopCh:
                        return
                    case value := <-dataCh:
                        if value == MaxRandomNumber-1 {
                            // 同様のトリックを使用してホストに信号チャネルを閉じるように通知します。
                            select {
                            case toStop <- "receiver#" + id:
                            default:
                            }
                            return
                        }
    
                        log.Println(value)
                    }
                }
            }(strconv.Itoa(i))
        }
    
        // ...
        wgReceivers.Wait()
        log.Println("stopped by", stoppedBy)
    }
    

json#

  • 最初に tag 名(json tag の説明は次のセクションを参照)をFieldとするフィールドを探します。
  • 次に、名前がFieldのフィールドを探します。
  • 最後に、名前がFieldの大文字小文字を区別しない一致フィールドを探します。
  • どれも見つからない場合、その key は無視され、エラーは報告されません。これは、多くのデータの中から一部だけを選択して使用するのに非常に便利です。

reflect#

var i int = 3
type test struct{}
var t test
v := reflect.ValueOf(i)
v2 := reflect.ValueOf(&t).Elem()

reflect.ValueOf ():値を返します。

reflect.ValueOf ().Elem ():渡す必要があるのはポインタ型です。

struct#

struct {}{}:空の構造体の役割

空の構造体はメモリスペースを占有しないため、さまざまなシーンでプレースホルダーとして広く使用されています。一つはリソースを節約するため、もう一つは空の構造体自体が非常に強い意味を持ち、ここでは値は必要なく、プレースホルダーとしてのみ使用されます。

Go 言語の標準ライブラリは Set の実装を提供していないため、通常はマップを代わりに使用します。実際、集合に対しては、マップのキーだけが必要で、値は必要ありません。値を bool 型に設定しても 1 バイト余分に占有されるため、マップに 100 万件のデータがあると、1MB のスペースが無駄になります。

したがって、マップを集合 (Set) として使用する場合、値の型を空の構造体として定義し、プレースホルダーとしてのみ使用できます。

go メモリ申請#

プログラムはオペレーティングシステムにメモリのブロック(ヒープとスタック)を申請します。

スタックメモリを申請する利点:関数が戻ると直接解放され、ガーベジコレクションを引き起こさず、パフォーマンスに影響を与えません。

ヒープメモリを申請すると、ガーベジコレクションが引き起こされます。

// インスタンス1:スタック
func F() {
    temp := make([]int, 0, 20)
    ...
}
// インスタンス2:ヒープ
func F() []int{
    a := make([]int, 0, 20)
    return a
}
// インスタンス3
func F() {
    a := make([]int, 0, 20)		// スタック
    b := make([]int, 0, 20000)	// 容量が比較的大きい、ヒープ
    l := 20
    c := make([]int, 0, l)		// 不定長、ヒープ
}

go init#

import --> const --> var --> init()

各パッケージ内の各 init () 関数は呼び出され、順序は固定です。

同じ go ファイルの init () 呼び出しの順序は上から下です。
同じパッケージ内の異なるファイルは、ファイル名の文字列を比較して「小から大」の順序で各ファイルの init () 関数を呼び出します。
異なるパッケージが相互に依存しない場合、main パッケージ内の「先に import された後に呼び出される」順序でそのパッケージ内の init () を呼び出します。
パッケージが依存している場合、最初に依存されているパッケージの init () が呼び出されます。

unsafe.Pointer#

大多数のポインタ型は*Tとして書かれ、「T 型変数へのポインタを指す」と示します。unsafe.Pointer は特別に定義されたポインタ型(訳注:C 言語のvoid*型のポインタに似ています)であり、任意の型の変数のアドレスを含むことができます。

  • Sizeof は任意の型の値(式)を受け取り、その占有するバイト数を返します。これは C 言語とは異なり、C 言語では sizeof 関数の引数は型ですが、ここでは値、例えば変数です。
  • Offsetof:構造体メンバーがメモリ内で構造体の開始位置からのバイト数を返します。渡すパラメータは構造体のメンバーでなければなりません(構造体ポインタが指すアドレスは構造体の開始位置のアドレス、つまり最初のメンバーのメモリアドレスです)。

flag ライブラリ#

flagライブラリを使用する一般的な手順:

  • オプションの値を保存するためのいくつかのグローバル変数を定義します。ここではintflag/boolflag/stringflag
  • initメソッド内でflag.TypeVarメソッドを使用してオプションを定義します。ここでのTypeは基本型Int/Uint/Float64/Bool、または時間間隔time.Durationです。定義時に変数のアドレス、オプション名、デフォルト値、ヘルプ情報を渡します;
  • mainメソッド内でflag.Parseを呼び出してos.Args[1:]からオプションを解析します。os.Args[0]は実行可能プログラムのパスであり、除外されます。

go -ini ライブラリ#

context ライブラリ#

コンテキストを作成:

  • context.Backgroud()
  • context.TODO

With 系列関数

// キャンセル制御
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// タイムアウト制御
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// データを持つ
func WithValue(parent Context, key, val interface{}) Context		

sync ライブラリ#

  • sync.map:#

    sync.map.Range () で長さを遍歴します。

  • sync.pool:キャッシュオブジェクト#

    Put の前にリセットし、Get の後にリセットします。

  • sync.RWMutex:読み書きロック#

    書き込みロックが設定されている場合、他の読み取りスレッドと書き込みスレッドはロックを取得できません。このとき、相互排他ロックと同じ効果があります。

    読み取りロックが設定されている場合、他の書き込みスレッドはロックを取得できませんが、他の読み取りスレッドはロックを取得できます。

  • sync.Cond:

    sync.Cond の典型的な使用シーンは「生産者 - 消費者モデル」であり、複数の goroutine が特定のイベントが発生するのを待ち、単一の goroutine がそのイベントが発生したことを通知します。

etcd#

etcd は高可用性で強い一貫性を持つキー値ストアであり、多くの分散システムアーキテクチャで広く使用されています。その最も典型的な使用シーンはサービス発見です。

  1. 強い一貫性と高可用性を持つサービスストレージディレクトリ
  2. サービスを登録し、サービスの健康状態を監視するメカニズム
  3. サービスを検索し、接続するメカニズム

プロセス:

リーダー(主ノード)からフォロワーへ流れます;

ユーザーは etcd クラスター内のすべてのノードに対して読み書きできます。

micro#

go 言語でマイクロサービスを構築するための基本フレームワーク

例:

サーバー側

service guild {
  //クエリ
  rpc Find(FindRequest) returns (FindResponse){}
  //プレイヤーデータをクエリ
  rpc FindUser(FindUserRequest) returns (FindUserResponse){}
}

guild サービス:

main.go

func main() {
    reg := etcd.NewRegistry(func(op *registry.Options) {
		op.Addrs = settings.GetEtcdSysAddr()
	})
	uuid := utils.GenUUID()

	s := grpc.NewService(
		micro.Server(grpc2.NewServer(func(options *server.Options) {
			options.Id = uuid
		})),
		micro.Name(fmt.Sprintf("com.jsw.server.guild.%d", settings.GetSetId())),
		micro.Registry(reg),
		micro.RegisterInterval(time.Second*2),
		micro.RegisterTTL(time.Second*5),
		micro.WrapHandler(LogHandler),
	)
    // GuildHandlerオブジェクトは.protoファイルで定義されたこれらのインターフェース関数を実装する必要があります。
	registerErr := guildProto.RegisterGuildHandler(s.Server(), &handler.GuildHandler{})
    s.Init()
    s.Run()
}

guild.go

type GuildHandler struct {
}
func (m *GuildHandler) Find(ctx context.Context, request *guildProto.FindRequest, response *guildProto.FindResponse) error {}
func (m *GuildHandler) FindUser(ctx context.Context, request *guildProto.FindUserRequest, response *guildProto.FindUserResponse) error {}

game サービス:

guildServer = guildProto.NewGuildService(fmt.Sprintf("com.jsw.server.guild.%d", setId), ser.Client())

gorm#

// 最初のレコードを取得(主キー昇順)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// レコードを1件取得、ソートフィールドを指定しない
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 最後のレコードを取得(主キー降順)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

go protobuf#

  • .proto ファイルでgo_package = "xxx/proto/game;com_jsw_server_game"を定義します。;前の部分は生成された.pb.go ファイルのパス、;後の部分はエイリアスです。

  • //go:generate protoc --proto_path=. --proto_path=../ --proto_path=../public --proto_path=../game --gofast_out=. --micro_out=. common.proto
    
  • grpc

    message SimpleRequest {
      string name = 1;
    }
    message SimpleResponse {
      string message = 1;
    }
    
    service SimpleService {
      rpc Get(SimpleRequest) returns (SimpleResponse) {}	// 簡単なモード
      rpc GetUser(SimpleRequest) return (stream SimpleResponse) {}	// サーバーデータストリームモード
      rpc GetUser(stream SimpleRequest) return (SimpleResponse) {}	// クライアントデータストリームモード
      rpc GetUser(stream GetUserRequest) returns (stream GetUserResponse) {} // 双方向モード
    }
    

    4 つのモード:

    • 簡単 RPC

      // c
      resopnse, err := client.Get(contect.Background(), &pb.SimpleRequest{Name: "hello"})
      // s
      func (s *Simple) Get(ctx context.Context, req *pb.SimpleRequest) (resp *pb.SimpleResponse, error) {
          return resp
      }
      
    • サーバーデータストリーム

      // c
      user, err := client.GetUser(context.Background(), &pb.SimpleRequest{Name: "hello"})
      for {
          resp, err := user.Recv()	// Recvを呼び出して受信
          if err == io.EOF {
              break
          }
          ...
      }
      // s
      func (s *ServerSide) GetUser(req *pb.GetUserRequest, stream pb.ServerSide_GetUserServer) error {
          ...
          for {
              stream.Send(&pb.SimpleResponse{})	// Sendを呼び出して応答
          }
          return nil
      }
      
    • クライアントデータストリーム

      // c
      user, err := client.GetUser(context.Background())
      for {
         err := user.Send(&pb.SimpleRequest{Name: "hello"})
      }
      // s
      func (c *ClientSide) GetUser(stream pb.ClientSide_GetUserServer) error {
          recv, err := stream.Recv()
          if err == io.EOF {
              return stream.SendAndClose(&pb.GetUserResponse{})
          }
      }
      
    • 双方向データストリーム

      // c
      user, err := client.GetUser(context.Background())
      err := user.Send(&pb.SimpleRequest{Name: "hello"})
      err = user.CloseSend()
      // s
      func (b *BidirectionalService) GetUser(stream pb.BidirectionalService_GetUserServer) (err error) {
          req, err := stream.Recv()
          err = stream.Send(&pb.SimpleRequest{Name: "hello"})
      }
      

go cli#

cliはコマンドラインプログラムを構築するためのライブラリで、cli.App 構造体オブジェクトを作成します。

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "hello world example",
    Action: func(c *cli.Context) error {
      fmt.Println("hello world")
      return nil
    },
  }

  err := app.Run(os.Args)
}

Name と Usage はヘルプに表示されます。

Action はこのコマンドラインプログラムが実際に実行する関数です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。