In this tutorial, we'll walk through building a robust gRPC API in Go that interacts with a PostgreSQL database. We'll structure our application by separating models and services, following best practices to create scalable and maintainable code.
Introduction
gRPC is a high-performance, open-source universal RPC framework that uses Protocol Buffers as its interface definition language (IDL) and data serialization format. It allows you to define services and methods, and it generates code for both the client and server.
In this guide, we'll build a gRPC API for managing books, performing CRUD operations with a PostgreSQL database. We'll separate concerns by organizing our code into models and services.
Prerequisites
Go installed (Download Go)
PostgreSQL installed (Download PostgreSQL)
Protocol Buffers Compiler (protoc) installed (Download protoc)
Basic knowledge of Go programming
A code editor (e.g., VSCode, GoLand)
grpcurl or grpcui for testing the API
Project Setup
Create a new directory for your project:
mkdir grpc-bookstore
cd grpc-bookstore
Initialize a new Go module:
go mod init github.com/yourusername/grpc-bookstore
Replace github.com/yourusername/grpc-bookstore
with your actual GitHub username or module path.
Installing Dependencies
We'll need the following dependencies:
gRPC-Go: The Go implementation of gRPC
Protocol Buffers: For serialization
GORM: ORM library for Go
pq: PostgreSQL driver for Go
Install them using the following commands:
go get -u google.golang.org/grpc
go get -u google.golang.org/protobuf
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
Connecting to PostgreSQL
Setting Up the Database
First, set up the PostgreSQL database.
- Create a database named
bookstore
.
Connect to PostgreSQL and run:
CREATE DATABASE grpcbookstore;
- Create a
books
table.
Connect to the bookstore
database and run:
CREATE TABLE grpcbooks (
id SERIAL PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
description TEXT
);
Configuring the Database Connection
Create a new directory for database configurations:
mkdir database
touch database/database.go
Add the following code to database/database.go
:
package database
import (
"fmt"
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func Connect() {
var err error
dsn := "host=localhost user=yourusername password=yourpassword dbname=bookstore port=5432 sslmode=disable TimeZone=Asia/Shanghai"
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
fmt.Println("Database connection established")
}
Replace yourusername
and yourpassword
with your PostgreSQL username and password.
Defining Protobuf Messages and Services
Create a directory for protobuf files:
mkdir proto
touch proto/book.proto
In proto/book.proto
, define the messages and services:
syntax = "proto3";
package proto;
option go_package = "github.com/yourusername/grpc-bookstore/proto";
message Book {
int32 id = 1;
string title = 2;
string author = 3;
string description = 4;
}
message Empty {}
message BookID {
int32 id = 1;
}
message BookList {
repeated Book books = 1;
}
service BookService {
rpc CreateBook(Book) returns (Book);
rpc GetBook(BookID) returns (Book);
rpc UpdateBook(Book) returns (Book);
rpc DeleteBook(BookID) returns (Empty);
rpc ListBooks(Empty) returns (BookList);
}
Note: Replace github.com/yourusername/grpc-bookstore/proto
with your module path.
Generating Go Code from Protobuf Files
You need to generate Go code from the .proto
file using the Protocol Buffers compiler (protoc
).
First, install the Go plugins for protoc
:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Make sure your $GOPATH/bin
is in your $PATH
so that protoc
can find the plugins.
Generate the code:
protoc --go_out=. --go-grpc_out=. proto/book.proto
This command will generate book.pb.go
and book_grpc.pb.go
in the proto
directory.
Creating Models
Create a new directory for models:
mkdir models
touch models/book.go
In models/book.go
, define the Book
struct:
package models
type Book struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
}
Setting Up Services
Create a directory for services:
mkdir services
touch services/book_service.go
In services/book_service.go
, implement the service methods:
package services
import (
"context"
"errors"
"github.com/yourusername/grpc-bookstore/database"
"github.com/yourusername/grpc-bookstore/models"
"github.com/yourusername/grpc-bookstore/proto"
"gorm.io/gorm"
)
type BookServiceServer struct {
proto.UnimplementedBookServiceServer
}
func (s *BookServiceServer) CreateBook(ctx context.Context, req *proto.Book) (*proto.Book, error) {
book := models.Book{
Title: req.Title,
Author: req.Author,
Description: req.Description,
}
if err := database.DB.Create(&book).Error; err != nil {
return nil, err
}
res := &proto.Book{
Id: int32(book.ID),
Title: book.Title,
Author: book.Author,
Description: book.Description,
}
return res, nil
}
func (s *BookServiceServer) GetBook(ctx context.Context, req *proto.BookID) (*proto.Book, error) {
var book models.Book
if err := database.DB.First(&book, req.Id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("book not found")
}
return nil, err
}
res := &proto.Book{
Id: int32(book.ID),
Title: book.Title,
Author: book.Author,
Description: book.Description,
}
return res, nil
}
func (s *BookServiceServer) UpdateBook(ctx context.Context, req *proto.Book) (*proto.Book, error) {
var book models.Book
if err := database.DB.First(&book, req.Id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("book not found")
}
return nil, err
}
book.Title = req.Title
book.Author = req.Author
book.Description = req.Description
if err := database.DB.Save(&book).Error; err != nil {
return nil, err
}
res := &proto.Book{
Id: int32(book.ID),
Title: book.Title,
Author: book.Author,
Description: book.Description,
}
return res, nil
}
func (s *BookServiceServer) DeleteBook(ctx context.Context, req *proto.BookID) (*proto.Empty, error) {
if err := database.DB.Delete(&models.Book{}, req.Id).Error; err != nil {
return nil, err
}
return &proto.Empty{}, nil
}
func (s *BookServiceServer) ListBooks(ctx context.Context, req *proto.Empty) (*proto.BookList, error) {
var books []models.Book
if err := database.DB.Find(&books).Error; err != nil {
return nil, err
}
var res proto.BookList
for _, book := range books {
res.Books = append(res.Books, &proto.Book{
Id: int32(book.ID),
Title: book.Title,
Author: book.Author,
Description: book.Description,
})
}
return &res, nil
}
Note: Replace github.com/yourusername/grpc-bookstore
with your module path.
Implementing Server Logic
Create the main server file main.go
:
package main
import (
"fmt"
"log"
"net"
"github.com/yourusername/grpc-bookstore/database"
"github.com/yourusername/grpc-bookstore/models"
"github.com/yourusername/grpc-bookstore/proto"
"github.com/yourusername/grpc-bookstore/services"
"google.golang.org/grpc"
)
func main() {
database.Connect()
database.DB.AutoMigrate(&models.Book{})
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
proto.RegisterBookServiceServer(grpcServer, &services.BookServiceServer{})
fmt.Println("Server is running on port 50051...")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Testing the API
Start the Server
Run the application:
go run main.go
Using grpcurl
You can use grpcurl
to test the gRPC API. Install grpcurl
if you haven't already:
For macOS:
brew install grpcurl
For other platforms, download from the official releases.
List Available Services
grpcurl -plaintext localhost:50051 list
List Methods in BookService
grpcurl -plaintext localhost:50051 list proto.BookService
Create a New Book
grpcurl -plaintext -d '{
"title": "The Hitchhiker\'s Guide to the Galaxy",
"author": "Douglas Adams",
"description": "A science fiction comedy series."
}' localhost:50051 proto.BookService/CreateBook
Get All Books
grpcurl -plaintext -d '{}' localhost:50051 proto.BookService/ListBooks
Get a Book by ID
grpcurl -plaintext -d '{ "id": 1 }' localhost:50051 proto.BookService/GetBook
Update a Book
grpcurl -plaintext -d '{
"id": 1,
"title": "Updated Title",
"author": "Douglas Adams",
"description": "An updated description."
}' localhost:50051 proto.BookService/UpdateBook
Delete a Book
grpcurl -plaintext -d '{ "id": 1 }' localhost:50051 proto.BookService/DeleteBook
Conclusion
You've successfully built a gRPC API in Go that interacts with a PostgreSQL database, complete with separated models and services. This architecture is scalable and maintainable, following best practices for building robust applications.
Next Steps
Implement Authentication: Use gRPC interceptors to add authentication to your services.
Error Handling: Implement detailed error codes and messages using gRPC's status package.
Logging and Monitoring: Integrate logging and monitoring tools like Prometheus.
Dockerize the Application: Containerize your app for easy deployment.
By following this guide, you've gained valuable experience in building a real-world gRPC API in Go. Keep exploring and enhancing your application!
Happy Coding!