09 — Your first Go gRPC server
Goal
Start a Go gRPC server, register the generated service, and make a real call.
Step 1: Implement a handler
From field-notes, create server/internal/rpc/task_server.go:
package rpc
import (
"context"
"time"
fieldnotesv1 "github.com/example/field-notes/gen/go/fieldnotes/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
type TaskServer struct {
fieldnotesv1.UnimplementedTaskServiceServer
}
func NewTaskServer() *TaskServer { return &TaskServer{} }
func (s *TaskServer) GetTask(ctx context.Context, req *fieldnotesv1.GetTaskRequest) (*fieldnotesv1.Task, error) {
if req.GetId() == "" {
return nil, status.Error(codes.InvalidArgument, "task id is required")
}
if req.GetId() != "demo-task" {
return nil, status.Error(codes.NotFound, "task not found")
}
now := timestamppb.New(time.Now().UTC())
return &fieldnotesv1.Task{
Id: "demo-task", ProjectId: "demo-project", Title: "Try the Flutter client",
Description: "First Go response", Status: fieldnotesv1.TaskStatus_TASK_STATUS_TODO,
CreatedAt: now, UpdatedAt: now,
}, nil
}
func (s *TaskServer) CreateTask(ctx context.Context, req *fieldnotesv1.CreateTaskRequest) (*fieldnotesv1.CreateTaskResponse, error) {
return nil, status.Error(codes.Unimplemented, "CreateTask is added in the CRUD chapter")
}
Embedding UnimplementedTaskServiceServer makes the handler compatible with generated service evolution. The demo implementation is intentionally temporary; later chapters move behavior behind an application service and repository.
The handler method signature must match the generated interface exactly. The first argument is the request context, which carries a deadline and cancellation signal. A gRPC status error is not just text: it contains a standard code that generated clients can inspect and use to choose whether to retry, ask the user to sign in, or show a validation message.
Step 2: Start the server
Create server/cmd/api/main.go:
package main
import (
"log"
"net"
fieldnotesv1 "github.com/example/field-notes/gen/go/fieldnotes/v1"
"github.com/example/field-notes/server/internal/rpc"
"google.golang.org/grpc"
)
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil { log.Fatalf("listen: %v", err) }
server := grpc.NewServer()
fieldnotesv1.RegisterTaskServiceServer(server, rpc.NewTaskServer())
log.Println("gRPC listening on :50051")
if err := server.Serve(listener); err != nil { log.Fatalf("serve: %v", err) }
}
Run from the repository root:
cd server
go mod tidy
go run ./server/cmd/api
Expected log: gRPC listening on :50051. Keep this terminal running.
net.Listen binds a local TCP address. grpc.NewServer creates the gRPC server; registering the generated service connects the service name and method paths to your implementation. Serve blocks and processes connections until an error or shutdown. Port 50051 is a common development choice; production configuration should come from the environment.
Step 3: Call it with grpcurl
Install grpcurl using its official project instructions. In a second terminal:
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"id":"demo-task"}' localhost:50051 fieldnotes.v1.TaskService/GetTask
The service does not enable reflection yet, so list may not work until a reflection server is registered. The direct call works when the proto is supplied:
grpcurl -plaintext -import-path api -proto fieldnotes/v1/task.proto -d '{"id":"demo-task"}' localhost:50051 fieldnotes.v1.TaskService/GetTask
You should receive task JSON. Try an unknown id and an empty id; grpcurl reports NotFound and InvalidArgument respectively.
grpcurl is useful because it isolates the backend from Flutter. If this call fails, the issue is in the Go server, proto contract, or network address—not in widget state. Plaintext is used only because the development server has no TLS configured yet.
Step 4: Verify Go packages
From the repository root run go test ./... and go vet ./.... Generated packages live at gen/go; the server imports them using the shared module path.
Checkpoint
Change the demo task title and fetch it again. Explain what the generated registration function does and why the handler returns a gRPC status error.
Troubleshooting
cannot find module providing package .../gen/go: run code generation from repo root and checkgo_packageand module path match.unknown service: check the registered service name and the full RPC path.- Connection refused: keep the server running and verify it listens on port 50051.