grpc
- grpc
- grpc-go
grpc seamlessly integrates protobuf
protobuf
- For those of you accustomed to JSON and XML data storage formats, I believe most have never heard of Protocol Buffer.
- Protocol Buffer is actually a lightweight & efficient structured data storage format developed by Google, and its performance is truly much stronger than JSON and XML!
- Protobuf has gone through protobuf2 and protobuf3, with pb3 being much simpler than pb2. The current mainstream version is pb3.
Downloading protoc
Download protoc andgo get github.com/golang/protobuf/protoc-gen-go
syntax = "proto3";
option go_package = "."; //这名不写现在编译不了
message HelloRequest {
string name = 1; //1 是编号不是值
}
protoc --proto_path=. \
--go_out=. \
--go-grpc_out=. \
./helloworld.proto
This will generate a helloworld.pb.go file in the directory.
Comparison of compression ratios
package main
import (
"RpcLearn/protobuf"
"encoding/json"
"fmt"
"github.com/golang/protobuf/proto"
)
// TIP <p>To run your code, right-click the code and select <b>Run</b>.</p> <p>Alternatively, click
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.</p>
type Hello struct {
Name string `json:"name"`
}
func main() {
//TIP <p>Press <shortcut actionId="ShowIntentionActions"/> when your caret is at the underlined text
// to see how GoLand suggests fixing the warning.</p><p>Alternatively, if available, click the lightbulb to view possible fixes.</p>
//s := "gopher"
//fmt.Printf("Hello and welcome, %s!\n", s)
//
//for i := 1; i <= 5; i++ {
// //TIP <p>To start your debugging session, right-click your code in the editor and select the Debug option.</p> <p>We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// // for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.</p>
// fmt.Println("i =", 100/i)
//}
jsonStruct := Hello{
Name: "gopher",
}
jsonReq, _ := json.Marshal(jsonStruct)
fmt.Println(string(jsonReq))
fmt.Println(len(jsonReq))
req := __.HelloRequest{
Name: "gopher",
}
rsp, _ := proto.Marshal(&req)
fmt.Println(rsp)
fmt.Println(len(rsp))
}
Roughly a two-fold difference.
Explanation of Changes in Protobuf Usage in Go
| Tutorial Content | Current Reality |
|---|---|
Previously, there was only one .pb.go file |
Now two files are generated: one .pb.go (data structures) and one _grpc.pb.go (gRPC interfaces) |
| Plugins not separated | Plugins have been split into protoc-gen-go and protoc-gen-go-grpc, requiring separate installation and invocation |
📌 Current Behavior Explanation
Starting from Go's Protobuf plugin v1.20+, gRPC support has been split into a separate plugin, protoc-gen-go-grpc, used for generating service interface-related code. This approach is more modular and provides clearer responsibilities.
Therefore, execute the following command:
protoc --proto_path=. \
--go_out=. \
--go-grpc_out=. \
./your_file.proto
This will generate:
- your_file.pb.go: Contains message structures (e.g., Request, Response)
- your_file_grpc.pb.go: Contains service interface definitions (e.g., YourServiceServer)
It is recommended to keep and synchronize these two files.
A brief overview of the gRPC startup process
- Create stub files and method definitions, then compile
// proto/helloworld.proto
syntax = "proto3";
option go_package = ".;proto";
service Greeter{
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
- Server-side implementation of method calls, note the server implementation (duck typing)
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"google.golang.org/grpc"
"log"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer // 👈 重点:嵌入这个类型
}
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
reply := &proto.HelloReply{
Message: "Hello " + req.Name,
}
return reply, nil
}
func main() {
g := grpc.NewServer() // 创建一个新的gRPC服务器
proto.RegisterGreeterServer(g, &Server{}) // 注册服务
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- Client-side call implementation
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// Create a new gRPC server
//conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
// 使用安全连接
//creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
conn, err := grpc.NewClient("localhost:9090",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
gRPC Stream Mode
- Simple RPC
- Server-side data stream
- Client-side data stream
- Bidirectional
Focused Learning on gRPC
| Protobuf Type | Go Type |
|---|---|
| double | float64 |
| float | float32 |
| int32 | int32 |
| int64 | int64 |
| uint32 | uint32 |
| uint64 | uint64 |
| sint32 | int32 |
| sint64 | int64 |
| fixed32 | uint32 |
| fixed64 | uint64 |
Default Values
Default Value Explanation
In Protobuf, different field types have the following default values:
- String type (string): The default value is an empty string.
- Byte type (bytes): The default value is an empty byte sequence.
- Boolean type (bool): The default value is
false. - Numeric types: The default value is
0. - Enum types: The default value is the first defined enum value, which must be
0. - Message types (message): The default value is unset (i.e.,
null).
Important Notes
- For scalar message fields: Once a message is parsed, it's impossible to distinguish whether a field was set to its default value (e.g., a boolean to
false) or if the field was never set at all. - Recommendation for boolean values: Avoid defining boolean fields with a default value of
false, as this might lead to unintended behavior. - Serialization of scalar messages: If a scalar message field is set to its default value, that value will not be serialized for transmission.
You can refer to the Generated Code Guide for more detailed information on how Protobuf default values are handled.
Enum Type Definition
Enum types can be used when you need to specify a set of "predefined value sequences" for a field in a message type. Each enum value corresponds to an integer value.
Example
Below is an example of an enum definition:
message SearchRequest {
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 1;
}
- Server-side data stream (for real-time data retrieval)
- Client-side data stream (for data reporting)
- Bidirectional data stream
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc GetStream (StreamReqData) returns (stream StreamResData); // 服务端流模式
rpc PutStream (stream StreamReqData) returns ( StreamResData); // 客户端流模式
rpc AllStream (stream StreamReqData) returns (stream StreamResData); // 双向流模式
}
message StreamReqData {
string data = 1;
}
message StreamResData {
string message = 1;
}
Compile to generate Go Protobuf
protoc --proto_path=. \
--go_out=. \
--go-grpc_out=. \
./helloworld.proto
Server-side code
package main
import (
"RpcLearn/stream_grpc_test/proto"
"fmt"
"google.golang.org/grpc"
"log"
"net"
"sync"
"time"
)
const PORT = ":50052"
type server struct {
proto.UnimplementedGreeterServer // 👈 重点:嵌入这个类型
}
func (s server) GetStream(data *proto.StreamReqData, res proto.Greeter_GetStreamServer) error {
//TODO implement me
//panic("implement me")
i := 0
for {
i++
_ = res.Send(&proto.StreamResData{
Message: fmt.Sprintf("%v", time.Now().Unix()),
})
time.Sleep(time.Second)
if i >= 10 {
break
}
}
return nil
}
func (s server) PutStream(cliStr proto.Greeter_PutStreamServer) error {
//TODO implement me
//panic("implement me")//
for {
if a, err := cliStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println(a)
}
}
return nil
}
func (s server) AllStream(allStr proto.Greeter_AllStreamServer) error {
//TODO implement me
//panic("implement me")
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
if a, err := allStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println("客户端发送的消息:", a)
}
}
}()
go func() {
defer wg.Done()
for {
allStr.Send(&proto.StreamResData{
Message: fmt.Sprintf("我是服务端数据:%v\n", time.Now().Unix()),
})
time.Sleep(time.Second)
}
}()
wg.Wait()
return nil
}
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if (!ok) {
// handle missing metadata
}
// do something with metadata
return &pb.SomeResponse{}, nil
}
func main() {
// 创建一个新的gRPC服务器
g := grpc.NewServer()
proto.RegisterGreeterServer(g, &server{}) // 注册服务
listener, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Client-side code
package main
import (
"RpcLearn/stream_grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
"time"
)
func main() {
// Create a new gRPC server
conn, err := grpc.("localhost:50052",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
//res, _ := c.GetStream(context.Background(), &proto.StreamReqData{Data: "慕课网"})
//for {
// r, err := res.Recv()
// if err != nil {
// break
// }
// fmt.Println(r)
//}
//// 客户端流模式
//putS, _ := c.PutStream(context.Background())
//i := 0
//for {
// i++
// putS.Send(&proto.StreamReqData{Data: fmt.Sprintf("慕课网%v", i)})
// time.Sleep(time.Second)
// if i >= 10 {
// break
// }
//}
//双向流模式
allStr, _ := c.AllStream(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
if a, err := allStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println("我是客户端:%v", a)
}
}
}()
go func() {
defer wg.Done()
i := 0
for {
i++
allStr.Send(&proto.StreamReqData{Data: fmt.Sprintf("双向的慕课网:%v", i)})
time.Sleep(time.Second)
if i >= 10 {
break
}
}
}()
wg.Wait()
}
Protobuf Type to Go Type Mapping (with Encoding Recommendations)
.proto Type |
Description | Go Type |
|---|---|---|
double |
Floating-point type | float64 |
float |
Floating-point type | float32 |
int32 |
Uses variable-length encoding, inefficient for negative values; if potentially negative, consider using sint32 instead |
int32 |
int64 |
Uses variable-length encoding | int64 |
uint32 |
Uses variable-length encoding | uint32 |
uint64 |
Uses variable-length encoding | uint64 |
sint32 |
Uses variable-length encoding, more efficient for negative values (better than uint32) |
int32 |
sint64 |
Uses variable-length encoding, more efficient for negative values | int64 |
fixed32 |
Always 4 bytes, suitable for values always larger than 2²²⁸, more efficient than uint32 |
uint32 |
fixed64 |
Always 8 bytes, suitable for values always larger than 2²⁵⁶, more efficient than uint64 |
uint64 |
sfixed32 |
Always 4 bytes | int32 |
sfixed64 |
Always 8 bytes | int64 |
bool |
Boolean value | bool |
string |
Must be UTF-8 or ASCII encoded text | string |
bytes |
Can contain arbitrary sequences of bytes | []byte |
option go_package
Specifying the compilation output directory
How to import other protos in a proto file
import "base.proto"
import "google/protobuf/empty.proto"
Nested messages
Enum types
enum Gender{
MALE = 0;
FEMALE = 1;
}
Map type map<string,string> mp = 4
How to use timestamps google/protobuf/timestamp.proto
message HelloRequest {
string name = 1; // Name, equivalent to documentation
string url = 2;
Gender g = 3;
map<string, string> mp = 4;
google.protobuf.Timestamp addTime = 5;
}
More gRPC Features
gRPC allows us to implement remote calls just like local calls. In each RPC call, there might be some useful data, which can be passed via metadata. Metadata stores data in key-value pairs, where the key is a string type and the value is `[]string`, i.e., a string array type. Metadata enables the client and server to provide each other with information about the current call, similar to RequestHeader and ResponseHeader in an HTTP request. Just as the lifecycle of an HTTP header is one HTTP request, the lifecycle of metadata is one RPC call.
Metadata
Not directly intrusive to business logic
// 新建
type MD mp[string][]string
// 创建
// 第一种方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
// 第二种方式 key 不区分大小写,会被统一转成小写。
md := metadata.Pairs(
"key1", "val1",
"key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
"key2", "val2",
)
Sending metadata
md := metadata.Pairs("key", "val")
// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)
Receiving metadata
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
// handle missing metadata
}
// do something with metadata
return &pb.SomeResponse{}, nil
}
gRPC Interceptors
Server-side and client-side interceptors
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"google.golang.org/grpc"
"log"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer // 👈 重点:嵌入这个类型
}
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
reply := &proto.HelloReply{
Message: "Hello " + req.Name,
}
return reply, nil
}
func main() {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Println("Request: Received a new request", req)
resp, err = handler(ctx, req)
if err != nil {
log.Println("Error:", err)
}
log.Println("Response: Finished processing this new request", resp)
return resp, err
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt) // 创建一个新的gRPC服务器
proto.RegisterGreeterServer(g, &Server{}) // 注册服务
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"time"
)
func main() {
// Create a new gRPC server
//conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
// 使用安全连接
//creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
// 添加拦截器
clientInterceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Println("Time taken", time.Since(start), err)
return err
}
opt := grpc.WithUnaryInterceptor(clientInterceptor)
conn, err := grpc.NewClient("localhost:9090",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
opt,
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
gRPC Login Verification via Metadata
// server
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"log"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer // 👈 重点:嵌入这个类型
}
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
reply := &proto.HelloReply{
Message: "Hello " + req.Name,
}
return reply, nil
}
func main() {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Println("Request: Received a new request", req)
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Println("No metadata")
return resp, status.Error(codes.Unauthenticated, "No metadata")
}
var (
appid string
appkey string
)
if val1, ok := md["appid"]; ok {
appid = val1[0]
}
if val2, ok := md["appkey"]; ok {
appkey = val2[0]
}
if appid != "1010101" || appkey != "123456" {
return resp, status.Error(codes.Unauthenticated, "No token authentication information")
}
resp, err = handler(ctx, req)
if err != nil {
log.Println("Error:", err)
}
log.Println("Response: Finished processing this new request", resp)
return resp, err
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt) // 创建一个新的gRPC服务器
proto.RegisterGreeterServer(g, &Server{}) // 注册服务
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type customCredentials struct {
}
func (c *customCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "1010101",
"appkey": "123456",
}, nil
}
func (c *customCredentials) RequireTransportSecurity() bool {
return false
}
func main() {
// 添加拦截器
//clientInterceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// start := time.Now()
// //生成metadata
// md := metadata.New(map[string]string{
// "appid": "1010101",
// "appkey": "123456",
// })
// ctx = metadata.NewOutgoingContext(ctx, md)
// err := invoker(ctx, method, req, reply, cc, opts...)
// fmt.Println("Time taken", time.Since(start), err)
// return err
//}
//opt := grpc.WithUnaryInterceptor(clientInterceptor
// 另一种方式
opt := grpc.WithPerRPCCredentials(&customCredentials{}) // 有一个专门的metadata的拦截器
conn, err := grpc.NewClient("localhost:9090",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
opt,
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
gRPC (protoc-gen-validate)
protoc-gen-validate (PGV for short) is a Protocol Buffers plugin used to add validation logic for struct fields in generated Go code.
By adding validation rules in .proto files, it automatically generates validation code for each field, saving you from manually writing validation logic.
syntax = "proto3";
import "validate/validate.proto";
message User {
string name = 1 [(validate.rules).string.min_len = 3];
int32 age = 2 [(validate.rules).int32.gte = 18];
}
View the introduction to protoc-gen-validate
grpc_proto_validate
syntax = "proto3";
import "validate.proto";
option go_package = ".;proto";
service Greeter {
rpc SayHello (Person) returns (Person);
}
message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];
string email = 2 [(validate.rules).string.email = true];
string mobile = 3 [
(validate.rules).string = {
pattern: "^1[3-9]\\d{9}$"
}
];
}
Compile this proto by adding
--validate_out="lang=go". First, installgo install github.com/bufbuild/protoc-gen-validate@latest
protoc \
--proto_path=. \
--go_out=. \
--go-grpc_out=. \
--validate_out=lang=go:. \
./helloworld.proto
package main
import (
"RpcLearn/grpc_validate_test/proto"
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"log"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer // 👈 重点:嵌入这个类型
}
func (s *Server) SayHello(ctx context.Context, request *proto.Person) (*proto.Person, error) {
// Simulate some processing
return &proto.Person{
Id: request.Id,
Mobile: request.Mobile,
Email: request.Email,
}, nil
}
type Validator interface {
Validate() error
}
func main() {
//p := new(proto.Person)
//err := p.Validate()
//if err != nil {
// panic(err)
//}
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if v, ok := req.(Validator); ok {
if err := v.Validate(); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
}
return handler(ctx, req)
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt) // 创建一个新的gRPC服务器
proto.RegisterGreeterServer(g, &Server{}) // 注册服务
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package main
import (
"RpcLearn/grpc_validate_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"time"
)
type customCredential struct{}
func main() {
// 拦截器(可选)
clientInterceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Println("Call duration:", time.Since(start), "Error:", err)
return err
}
// 使用新方式连接(推荐)
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()), // 替代 grpc.WithInsecure()
grpc.WithUnaryInterceptor(clientInterceptor),
)
if err != nil {
panic(err)
}
defer conn.Close()
// 构建 gRPC 客户端
c := proto.NewGreeterClient(conn)
// 调用 RPC
rsp, err := c.SayHello(context.Background(), &proto.Person{
Id: 1000,
Email: "bobby@test.com",
Mobile: "13222223333",
})
if err != nil {
panic(err)
}
fmt.Println("Returned ID:", rsp.Id)
}
Exception Handling in gRPC
1. gRPC Status Codes
gRPC Status Codes Documentation
Go Exception Handling
1. Server-side
st := status.New(codes.InvalidArgument, "invalid username")
2. Client-side
st, ok := status.FromError(err)
if !ok {
// Error was not a status error
}
st.Message()
st.Code()
Timeout Mechanism
ctx,_ = content.WithTimeout(context.Background(),time.Second*3)
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6740