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.

Nullbody Notes
Nullbody Notes
Nullbody Notes
Getting Started with gRPC: Build a Simple Go Client and Server

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/*.proto

Key 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GostreaminggRPCprotobufHTTP/2client-server
Nullbody Notes
Written by

Nullbody Notes

Go backend development, learning open-source project source code together, focusing on simplicity and practicality.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.