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}
関数#
-
関数の構文形式:
func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) { //ここは処理ロジックコード //複数の値を返す return value1, value2 }
- func:関数は func で宣言されます。
- funcName:関数名、関数名とパラメータリストが関数シグネチャを構成します。
- 小文字:同じパッケージ内で呼び出せますが、異なるパッケージでは呼び出せません。
- 大文字:異なるパッケージ内で呼び出せます(import パッケージを通じて)。
- parametername type:パラメータリスト。
- output type:戻り値、Go 言語は 1 つの関数が複数の値を返すことをサポートしています。
-
値渡し
-
参照渡し
参照渡しは本質的に値渡しであり、この値はポインタ(アドレス)です。
Go 言語のスライス、マップ、チャネルの 3 つの型の実装メカニズムはポインタに似ています。したがって、アドレスを取得してポインタを渡す必要はありません。
-
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 情報を出力します。
-
メソッド
Go 言語には関数とメソッドが同時に存在します。メソッドは受信者を含む関数であり、受信者は命名型または構造体型の値またはポインタであることができます。
-
継承
type Human struct { Name string Age int Phone string } type Employee struct { Human Salary int Currency string }
-
インターフェース
// インターフェースMenを定義 type Men interface { SayHi() Sing(lyrics string) }
- interface は任意のオブジェクトによって実装できます。
- 1 つのオブジェクトは任意の数の interface を実装できます。
- 任意の型は空の interface を実装しています。
- 構造体が interface を実装している場合、interface 型の変数 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)) は、受信者が必要です。
-
チャネルを閉じる
- m 個の受信者、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 の実装を提供していないため、通常は map を代わりに使用します。実際、集合に対しては map のキーだけが必要で、値は必要ありません。値を bool 型に設定しても、1 バイト余分に占有されます。つまり、map に 100 万件のデータがあると、1MB のスペースが無駄になります。
したがって、map を集合 (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 は高可用性で強い一貫性を持つキー値ストアで、多くの分散システムアーキテクチャで広く使用されており、その最も代表的な使用シーンはサービス発見です。
- 強い一貫性と高可用性のサービスストレージディレクトリ
- サービスを登録し、サービスの健康状態を監視するメカニズム
- サービスを検索し、接続するメカニズム
プロセス:
リーダー(主ノード)からフォロワーへ流れます;
ユーザーは 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 はこのコマンドラインプログラムが実際に実行する関数です。