Go Notes#
Basics#
Variables#
var variableName type = expression
In which either "type" or "=expression" can be omitted. If the type information is omitted, the variable's type will be inferred from the initialization expression. If the initialization expression is omitted, the variable will be initialized with its zero value. The zero value for numeric types is 0, for boolean types it is false, for string types it is an empty string, and for interface or reference types (including slice, pointer, map, chan, and function) it is nil. The zero value for aggregate types like arrays or structs is that each element or field is initialized to the zero value of that type.
make, new#
i := 0
u := user{}
Both allocate memory (on the heap), but make is only used for initializing slices, maps, and channels (non-zero values); while new is used for memory allocation of types and initializes the memory to zero. The value returned by make is still the reference type itself; while new returns a pointer to the type. New is not commonly used; usually, short variable declarations and struct literals are used to achieve our goals.
range#
In Go, the range keyword is used in for loops to iterate over elements of arrays, slices, channels, or maps. In arrays and slices, it returns the index and the corresponding value, while in maps it returns key-value pairs.
Slices#
// Create a slice of an array with an initial length of 5, initial values are 0:
mySlice1 := make([]int, 5)
// Create a slice of an array with an initial length of 5, initial values are 0, and reserve storage space for 10 elements:
mySlice2 := make([]int, 5, 10)
// Slice literal creates a slice with length 5 and capacity 5; note that do not write the capacity of the array inside [ ], as it will become an array instead of a slice.
mySlice3 := []int{10,20,30,40,50}
Functions#
-
Function syntax format:
func funcName(parameterName type1, parameterName type2) (output1 type1, output2 type2) { // Logic code here // Return multiple values return value1, value2 }
- func: Functions are declared with func.
- funcName: Function name; the function name and parameter list together form the function signature.
- Lowercase: Callable within the same package, not callable from different packages.
- Uppercase: Callable from different packages (via import).
- parameterName type: Parameter list.
- output type: Return values; Go supports returning multiple values from a function.
-
Value passing
-
Reference passing
Reference passing is essentially value passing, but the value is a pointer (address).
In Go, the implementation mechanisms of slice, map, and channel are similar to pointers, so they can be passed directly without taking the address to pass the pointer.
-
defer
Adding a defer statement in a function, defer operates in a last-in-first-out manner.
func ReadWrite() bool { file.Open("file") defer file.Close() }
Pitfalls:
func testDefer() (r int) { x := 1 defer func(xPtr *int) { *xPtr++ }(&x) return x }
return x is not an atomic instruction; in fact, x=r def return.
Defer operates in a last-in-first-out manner; when a goroutine encounters a panic, it traverses the defer chain of that goroutine and executes the defer statements. If recover is not encountered, after traversing the defer chain, it will output panic information to stderr.
-
Methods
Go has both functions and methods. A method is a function that has a receiver, which can be a value of a named type or a struct type, or a pointer.
-
Inheritance
type Human struct { Name string Age int Phone string } type Employee struct { Human Salary int Currency string }
-
Interfaces
// Define an interface Men type Men interface { SayHi() Sing(lyrics string) }
- An interface can be implemented by any object.
- An object can implement multiple interfaces.
- Any type implements an empty interface.
- If a struct implements an interface, then a variable of the interface type i can be assigned a value of the struct type, and we can also extract the struct type value from this variable i.
Errors#
A function or method must return an error as the last value returned.
The method for handling errors: Compare the returned error with nil; a nil value indicates that no error occurred.
goroutine#
The goroutine that encapsulates the main function is called the main goroutine.
- Set the maximum stack space that each goroutine can request.
- Create a special defer statement for necessary cleanup when a goroutine exits.
- Start a goroutine dedicated to cleaning up memory in the background and set the gc available flag.
- Execute the init function in the main package.
- Execute the main function.
- Check if the goroutine has caused a runtime panic.
- Finally, the main goroutine will end itself and the current process.
Channel#
-
Use channels for multiple return values.
-
Use for range to simplify syntax.
-
Sending values to a closed channel will cause a panic.
-
Receiving from a closed channel will continue to get values until the channel is empty.
-
Receiving from a closed channel that has no values will yield the zero value of the corresponding type.
-
Closing an already closed channel will cause a panic.
-
Unbuffered channel (make(chan int)), must have a receiver.
-
Closing a channel
- m receivers, 1 sender, the sender closes the data channel.
func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const MaxRandomNumber = 100000 const NumReceivers = 100 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // Sender go func() { for { if value := rand.Intn(MaxRandomNumber); value == 0 { // The only sender can safely close the channel. close(dataCh) return } else { dataCh <- value } } }() // Receiver for i := 0; i < NumReceivers; i++ { go func() { defer wgReceivers.Done() // Receive data until dataCh is closed or // the data queue of dataCh is empty. for value := range dataCh { log.Println(value) } }() } }
1 receiver, n senders, the receiver closes a signal channel.
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 is a signal channel. // Its sender is the receiver of dataCh. // Its receiver is the sender of dataCh. // Sender for i := 0; i < NumSenders; i++ { go func() { for { // The first select is to try to exit the goroutine as early as possible. // In fact, in this special case, this is not necessary, so it can be omitted. select { case <- stopCh: return default: } // Even if stopCh is closed, if sending to dataCh is not blocked, the first branch in the second select may not execute in some loops. // But in this example, it is acceptable, so the first select code block above can be omitted. select { case <- stopCh: return case dataCh <- rand.Intn(MaxRandomNumber): } } }() } // Receiver go func() { defer wgReceivers.Done() for value := range dataCh { if value == MaxRandomNumber-1 { // The receiver of the dataCh channel is also the sender of the stopCh channel. // It is safe to close the stop channel here. close(stopCh) return } log.Println(value) } }() // ... wgReceivers.Wait() }
m receivers, n senders, one notifies a host to close a signal channel.
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 is a signal channel. // Its sender is the host goroutine below. // Its receiver is all senders and receivers of dataCh. toStop := make(chan string, 1) // The toStop channel is generally used to notify the host to close the signal channel (stopCh). // Its sender is any sender and receiver of dataCh. // Its receiver is the host goroutine below. var stoppedBy string // Host go func() { stoppedBy = <-toStop close(stopCh) }() // Sender for i := 0; i < NumSenders; i++ { go func(id string) { for { value := rand.Intn(MaxRandomNumber) if value == 0 { // Here, a technique is used to notify the host to close the signal channel. select { case toStop <- "sender#" + id: default: } return } // The first select here is to try to exit this goroutine as early as possible. // This select code block has 1 receiving case and a default branch that will be specially optimized as a try-receive operation by the Go compiler. select { case <- stopCh: return default: } // Even if stopCh is closed, if sending to dataCh does not block, the first branch of the second select may not execute in some loops (and theoretically forever). // This is why the first select code block above is necessary. select { case <- stopCh: return case dataCh <- value: } } }(strconv.Itoa(i)) } // Receiver for i := 0; i < NumReceivers; i++ { go func(id string) { defer wgReceivers.Done() for { // Like the sender goroutine, the first select here is to try to exit this goroutine as early as possible. // This select code block has 1 sending case and a default branch that will be specially optimized as a try-send operation by the Go compiler. select { case <- stopCh: return default: } // Even if stopCh is closed, if receiving from dataCh does not block, the first branch of the second select may not execute in some loops (and theoretically forever). // This is why the first select code block is necessary. select { case <- stopCh: return case value := <-dataCh: if value == MaxRandomNumber-1 { // The same technique is used to notify the host to close the signal channel. select { case toStop <- "receiver#" + id: default: } return } log.Println(value) } } }(strconv.Itoa(i)) } // ... wgReceivers.Wait() log.Println("stopped by", stoppedBy) }
JSON#
- First, look for the field with the tag name (for json tag explanation, see the next section) as
Field
. - Then look for the field named
Field
. - Finally, look for fields that match
Field
in a case-insensitive manner. - If none are found, simply ignore this key without throwing an error. This is very convenient for selecting only a portion of data from a large dataset.
Reflect#
var i int = 3
type test struct{}
var t test
v := reflect.ValueOf(i)
v2 := reflect.ValueOf(&t).Elem()
reflect.ValueOf(): returns the value.
reflect.ValueOf().Elem(): the input must be a pointer type.
Struct#
struct{}{}: The purpose of an empty struct.
Since an empty struct does not occupy memory space, it is widely used as a placeholder in various scenarios. One is to save resources, and the other is that an empty struct itself has strong semantics, indicating that no value is needed here, only serving as a placeholder.
The Go standard library does not provide a Set implementation, and maps are usually used instead. In fact, for collections, only the keys of the map are needed, not the values. Even setting the value to a boolean type would occupy an extra byte, so if there are a million entries in the map, it would waste 1MB of space.
Therefore, when using a map as a collection (Set), the value type can be defined as an empty struct, used only as a placeholder.
Go Memory Allocation#
The program requests a block of memory (heap and stack) from the operating system.
The benefit of allocating stack memory: it is released directly when the function returns, which does not trigger garbage collection and has no impact on performance.
Allocating heap memory: triggers garbage collection.
// Example 1: Stack
func F() {
temp := make([]int, 0, 20)
...
}
// Example 2: Heap
func F() []int{
a := make([]int, 0, 20)
return a
}
// Example 3
func F() {
a := make([]int, 0, 20) // Stack
b := make([]int, 0, 20000) // Large capacity, Heap
l := 20
c := make([]int, 0, l) // Variable length, Heap
}
Go Init#
import --> const --> var --> init()
Each init() function in each package will be called, and the order is fixed.
The order of calling init() in the same Go file is from top to bottom.
For different files in the same package, the init() functions are called in "lexicographical order" based on the file names.
For different packages, if they do not depend on each other, the init() functions are called in the order of "first imported, then called" in the main package.
If a package has dependencies, the init() of the earliest dependent package is called first.
unsafe.Pointer#
Most pointer types are written as
*T
, indicating "a pointer to a variable of type T." unsafe.Pointer is a specially defined pointer type (similar to thevoid*
type in C), which can hold the address of any type of variable.
- Sizeof accepts values (expressions) of any type and returns the number of bytes it occupies, which is different from C where the sizeof function's parameter is a type; here it is a value, such as a variable.
- Offsetof: Returns the byte offset of a struct member from the start of the struct; the parameter must be a member of the struct (the address pointed to by the struct pointer is the address of the start of the struct, i.e., the memory address of the first member).
Flag Library#
General steps for using the
flag
library:
- Define some global variables to store the values of options, such as
intflag/boolflag/stringflag
here; - In the
init
method, useflag.TypeVar
to define options; theType
can be basic types likeInt/Uint/Float64/Bool
, or time intervalstime.Duration
. Pass the variable's address, option name, default value, and help information when defining; - In the
main
method, callflag.Parse
to parse options fromos.Args[1:]
. Becauseos.Args[0]
is the executable program path, it will be excluded.
Go -ini Library#
Context Library#
Creating context:
- context.Background()
- context.TODO
With series functions
// Cancel control
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// Timeout control
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// Carrying data
func WithValue(parent Context, key, val interface{}) Context
Sync Library#
-
sync.Map:#
sync.Map.Range() traverses to get the length.
-
sync.Pool: Cache objects#
Reset before Put, reset after Get.
-
sync.RWMutex: Read-write lock#
If a write lock is set, other read threads and write threads cannot obtain the lock. At this time, it functions the same as a mutex lock.
If a read lock is set, other write threads cannot obtain the lock, but other read threads can.
-
sync.Cond:
sync.Cond
is typically used in theproducer-consumer pattern
, where multiplegoroutines
wait for a certain event to occur, and a singlegoroutine
notifies that an event has occurred.
etcd#
etcd is a highly available and strongly consistent key-value store widely used in many distributed system architectures, with its most classic use case being service discovery.
- A strongly consistent, highly available service storage directory.
- A mechanism for registering services and monitoring service health status.
- A mechanism for finding and connecting services.
Process:
Flows from Leader (master node) to Followers;
Users can read and write to all nodes in the etcd cluster.
Micro#
The foundational framework for building microservices in Go.
Example:
Server-side
service guild {
// Query
rpc Find(FindRequest) returns (FindResponse){}
// Query player data
rpc FindUser(FindUserRequest) returns (FindUserResponse){}
}
Guild service:
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 object must implement these interface functions defined in the .proto file.
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 service:
guildServer = guildProto.NewGuildService(fmt.Sprintf("com.jsw.server.guild.%d", setId), ser.Client())
GORM#
// Get the first record (primary key ascending)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// Get a record without specifying sorting fields
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// Get the last record (primary key descending)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
Go Protobuf#
-
The .proto file defines
go_package = "xxx/proto/game;com_jsw_server_game"
; the first part is the path for generating the .pb.go file, and the second part is the alias. -
//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) {} // Simple mode rpc GetUser(SimpleRequest) returns (stream SimpleResponse) {} // Server-side data stream mode rpc GetUser(stream SimpleRequest) returns (SimpleResponse) {} // Client-side data stream mode rpc GetUser(stream GetUserRequest) returns (stream GetUserResponse) {} // Bidirectional mode }
Four modes:
-
Simple RPC
// Client response, err := client.Get(context.Background(), &pb.SimpleRequest{Name: "hello"}) // Server func (s *Simple) Get(ctx context.Context, req *pb.SimpleRequest) (resp *pb.SimpleResponse, error) { return resp }
-
Server-side data stream
// Client user, err := client.GetUser(context.Background(), &pb.SimpleRequest{Name: "hello"}) for { resp, err := user.Recv() // Call Recv to receive if err == io.EOF { break } ... } // Server func (s *ServerSide) GetUser(req *pb.GetUserRequest, stream pb.ServerSide_GetUserServer) error { ... for { stream.Send(&pb.SimpleResponse{}) // Call Send to reply } return nil }
-
Client-side data stream
// Client user, err := client.GetUser(context.Background()) for { err := user.Send(&pb.SimpleRequest{Name: "hello"}) } // Server func (c *ClientSide) GetUser(stream pb.ClientSide_GetUserServer) error { recv, err := stream.Recv() if err == io.EOF { return stream.SendAndClose(&pb.GetUserResponse{}) } }
-
Bidirectional data stream
// Client user, err := client.GetUser(context.Background()) err := user.Send(&pb.SimpleRequest{Name: "hello"}) err = user.CloseSend() // Server func (b *BidirectionalService) GetUser(stream pb.BidirectionalService_GetUserServer) (err error) { req, err := stream.Recv() err = stream.Send(&pb.SimpleRequest{Name: "hello"}) }
-
Go CLI#
cli
is a library for building command-line programs, creating a cli.App structure object.
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 and Usage are both displayed in the help.
Action is the function that is actually executed when this command-line program is called.