banner
lee

lee

Go Notes

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#

  1. 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.
  2. Value passing

  3. 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.

  4. 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.

  5. 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.

  6. Inheritance

    type Human struct {
        Name    string
        Age     int
        Phone   string
    }
    
    type Employee struct {
        Human
        Salary   int
        Currency string
    }
    
  7. 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

    1. 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 the void* 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, use flag.TypeVar to define options; the Type can be basic types like Int/Uint/Float64/Bool, or time intervals time.Duration. Pass the variable's address, option name, default value, and help information when defining;
  • In the main method, call flag.Parse to parse options from os.Args[1:]. Because os.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 the producer-consumer pattern, where multiple goroutines wait for a certain event to occur, and a single goroutine 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.

  1. A strongly consistent, highly available service storage directory.
  2. A mechanism for registering services and monitoring service health status.
  3. 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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.