Getting Started with gRPC: Build a Simple Go Client and Server
This article walks through creating a gRPC client and server in Go, covering proto file creation, option go_package usage, import statements, protoc compilation, ordinary and streaming RPC calls, file transfer via streams, and the underlying HTTP/2 reasons for gRPC's streaming capabilities.
Proto file basics
A minimal proto3 file includes the syntax declaration, package name, option go_package, message definitions, and service RPC definitions. Example:
syntax = "proto3";
package proto;
option go_package = "easydemo/proto/raftpb;raftpb";
message FileContext {
bool islastframe = 1;
bytes context = 2;
string ext = 3;
}
message FileInfoResp {
bool isok = 1;
string name = 2;
}
service File {
rpc sendfile(stream FileContext) returns (FileInfoResp) {}
}Protobuf scalar types map to Go types as follows: double →
float64 float→
float32 int32→
int32 int64→
int64 uint32→
uint32 uint64→
uint64 sint32→
int32 sint64→
int64 fixed32→
uint32 fixed64→
uint64 sfixed32→
int32 sfixed64→
int64 bool→
bool string→
string bytes→
[]byte enum→ Go enum type message → Go struct map → Go map type oneof → pointer field or
interface{} repeated→ Go slice
Interpretation of option go_package
The option has the form output_directory;package_name. In the example above, easydemo/proto/raftpb is the directory where the generated .go files are placed (combined with the --go_out flag), and raftpb is the Go package name. If the package name is omitted, the directory name is used.
Importing another proto file
syntax = "proto3";
package proto;
import "proto/raft.proto"; // path resolved by protoc -I flag
service Hello {
rpc send(proto.RaftMessage) returns (proto.RaftMessage) {}
}Protoc compilation command
The command ties the source .proto files to the generated Go code:
protoc -I . \
--go_out=module=easydemo:. \
--go-grpc_out=module=easydemo:. \
proto/*.protoKey flags: -I, --proto_path: directories searched for imported .proto files; multiple directories can be supplied (e.g., -I . -I ..). --go_out: destination for generated Go code; works together with option go_package. The module=easydemo part removes the module prefix from the output path.
gRPC server implementation
// server/server.go
func Start() {
lis, err := net.Listen("tcp", ":8088")
if err != nil {
fmt.Println("listen failed")
return
}
srv := grpc.NewServer()
raftpb.RegisterRaftServer(srv, &Raft{})
hellopb.RegisterHelloServer(srv, &Hello{})
raftpb.RegisterFileServer(srv, &File{})
fmt.Println("service started...")
if err = srv.Serve(lis); err != nil {
fmt.Println("serve failed")
}
}gRPC client implementation
// client/client.go
type Client struct { conn *grpc.ClientConn }
func NewClient() *Client {
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
conn, err := grpc.NewClient(":8088", opts...)
if err != nil { return nil }
return &Client{conn: conn}
}Ordinary unary RPC call
// client/client.go (method Send)
func (c *Client) Send(ctx context.Context, req *raftpb.RaftMessage) (*raftpb.RaftMessage, error) {
client := hellopb.NewHelloClient(c.conn)
res, err := client.Send(context.Background(), req)
if err != nil { fmt.Println(err); return nil, err }
fmt.Println(res.MsgType.Number(), res.MsgType.String())
// second request with a different message type
req.MsgType = raftpb.MessageType_INSTALL_SNAPSHOT
res, err = client.Send(context.Background(), req)
if err != nil { fmt.Println(err); return nil, err }
fmt.Println(res.MsgType.Number(), res.MsgType.String())
return res, nil
}
// server/server.go (Hello implementation)
type Hello struct{ hellopb.UnimplementedHelloServer }
func (h *Hello) Send(ctx context.Context, r *raftpb.RaftMessage) (*raftpb.RaftMessage, error) {
return &raftpb.RaftMessage{MsgType: r.MsgType}, nil
}Bidirectional streaming RPC
// client/client.go – Consensus (client side)
func (c *Client) Consensus() {
raftClient := raftpb.NewRaftClient(c.conn)
stream, err := raftClient.Consensus(context.Background())
if err != nil { fmt.Println(err); return }
from := uint64(time.Now().UnixMilli())
msg := &raftpb.RaftMessage{From: from, Term: 1}
stream.Send(msg)
for {
resp, err := stream.Recv()
if err == io.EOF { stream.CloseSend(); return }
if err != nil { stream.CloseSend(); return }
fmt.Println("client", from, resp.Term)
stream.Send(resp)
time.Sleep(5 * time.Second)
}
}
// server/server.go – Consensus (server side)
func (r *Raft) Consensus(srv raftpb.Raft_ConsensusServer) error {
for {
msg, err := srv.Recv()
if err == io.EOF { fmt.Println("server EOF"); return nil }
if err != nil { fmt.Println("server error", err); return err }
fmt.Println("server", msg.From, msg.Term)
msg.Term++ // increment term
srv.Send(msg)
time.Sleep(5 * time.Second)
}
}File transfer using a client‑side stream
// proto definition (already shown in section 1)
service File { rpc sendfile(stream FileContext) returns (FileInfoResp) {} }
// client/client.go – SendFile
func (c *Client) SendFile(fileName string) error {
fileClient := raftpb.NewFileClient(c.conn)
stream, err := fileClient.Sendfile(context.Background())
if err != nil { return err }
f, err := os.Open(fileName)
if err != nil { return err }
defer f.Close()
reader := bufio.NewReader(f)
buf := make([]byte, 1024)
for {
n, err := reader.Read(buf)
if err != nil {
if err == io.EOF {
ctx := &raftpb.FileContext{Islastframe: true, Context: buf[:n]}
if pos := strings.LastIndex(fileName, "."); pos != -1 {
ctx.Ext = fileName[pos:]
}
stream.Send(ctx)
}
break
}
ctx := &raftpb.FileContext{Islastframe: false, Context: buf[:n]}
stream.Send(ctx)
}
resp, err := stream.CloseAndRecv()
if err != nil { fmt.Println(resp, err); return err }
fmt.Println(resp)
return nil
}
// server/server.go – Sendfile implementation
type File struct{ raftpb.UnimplementedFileServer }
func (f *File) Sendfile(srv raftpb.File_SendfileServer) error {
tmpName := fmt.Sprintf("%d", time.Now().UnixMilli())
out, err := os.OpenFile(tmpName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil { return err }
var ext string
for {
ctx, err := srv.Recv()
if err == io.EOF { out.Close(); os.Remove(out.Name()); return err }
if err != nil { out.Close(); os.Remove(out.Name()); return err }
out.Write(ctx.Context)
if ctx.Islastframe { ext = ctx.Ext; break }
}
out.Close()
if ext != "" { os.Rename(tmpName, tmpName+ext) }
srv.SendAndClose(&raftpb.FileInfoResp{Isok: true, Name: tmpName + ext})
return nil
}Why gRPC supports streaming
gRPC is built on HTTP/2, which provides native multiplexed streams, binary framing, header compression, and eliminates head‑of‑line blocking present in HTTP/1.1. HTTP/1.1 is strictly request‑response; bidirectional data push required a protocol upgrade (WebSocket). HTTP/2’s stream capability lets gRPC implement both unary and streaming RPCs directly. The article also notes that HTTP/3 (QUIC over UDP) further evolves transport characteristics.
Project repository: https://github.com/gofish2020/easydemo
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Nullbody Notes
Go backend development, learning open-source project source code together, focusing on simplicity and practicality.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
