Building a gRPC Weather Service
This tutorial guides you through creating a gRPC service with the Gonyx framework, from defining protocol buffers to implementing and registering service handlers.
Prerequisites
Before starting this tutorial, make sure you've:
- Completed the Quick Start tutorial
- Basic understanding of Go programming
- Familiarity with gRPC and Protocol Buffers
- Go 1.23.0 or later installed
- Gonyx CLI tool installed
What We'll Build
We'll build a Weather Service gRPC API that provides:
- Current weather data for a location
- Weather forecasts
- Weather alerts
- Streaming weather updates
- Ability to submit weather reports
This demonstrates various gRPC features including unary calls and server streaming, along with proper integration into the Gonyx framework.
Step 1: Define Protocol Buffer Specification
First, we need to define our service interface using Protocol Buffers. Create a new file in your project's app/proto
directory:
syntax = "proto3";
package weather;
option go_package = "gonyx-user-db/app/proto/weather";
// WeatherService provides weather forecast information
service WeatherService {
// GetCurrentWeather fetches current weather for a location
rpc GetCurrentWeather(LocationRequest) returns (WeatherData) {}
// GetForecast returns a weather forecast for several days
rpc GetForecast(ForecastRequest) returns (ForecastResponse) {}
// GetWeatherAlerts returns active weather alerts for a location
rpc GetWeatherAlerts(LocationRequest) returns (AlertsResponse) {}
// StreamWeatherUpdates provides real-time weather updates
rpc StreamWeatherUpdates(LocationRequest) returns (stream WeatherData) {}
// SubmitWeatherReport allows users to submit local weather reports
rpc SubmitWeatherReport(WeatherReport) returns (SubmissionResponse) {}
}
// LocationRequest specifies a location for weather information
message LocationRequest {
// Location can be specified by coordinates or name
oneof location {
string city_name = 1;
Coordinates coordinates = 2;
}
}
// Coordinates represents precise geographical coordinates
message Coordinates {
double latitude = 1;
double longitude = 2;
}
// WeatherData represents current weather conditions
message WeatherData {
string location_name = 1;
double temperature_celsius = 2;
double feels_like = 3;
double humidity_percent = 4;
double wind_speed_kph = 5;
string wind_direction = 6;
string condition = 7;
string icon_code = 8;
string observation_time = 9;
}
// ForecastRequest requests a weather forecast
message ForecastRequest {
LocationRequest location = 1;
int32 days = 2; // Number of days for forecast (1-10)
}
// DailyForecast represents forecast for a single day
message DailyForecast {
string date = 1;
double min_temp_celsius = 2;
double max_temp_celsius = 3;
double precipitation_chance = 4;
double precipitation_amount = 5;
string condition = 6;
string icon_code = 7;
}
// ForecastResponse contains a multi-day forecast
message ForecastResponse {
string location_name = 1;
repeated DailyForecast daily_forecasts = 2;
string forecast_issued_time = 3;
}
// WeatherAlert represents a severe weather warning
message WeatherAlert {
string alert_id = 1;
string title = 2;
string description = 3;
string severity = 4; // "Minor", "Moderate", "Severe", "Extreme"
string start_time = 5;
string end_time = 6;
repeated string affected_areas = 7;
}
// AlertsResponse contains active weather alerts
message AlertsResponse {
string location_name = 1;
repeated WeatherAlert alerts = 2;
}
// WeatherReport allows users to submit local weather observations
message WeatherReport {
string reporter_id = 1;
LocationRequest location = 2;
string observation_time = 3;
double temperature_celsius = 4;
double precipitation_mm = 5;
string condition = 6;
string notes = 7;
bool is_severe = 8;
}
// SubmissionResponse acknowledges a weather report submission
message SubmissionResponse {
string report_id = 1;
bool success = 2;
string message = 3;
}
This Protocol Buffer definition specifies:
- A
WeatherService
with five RPC methods - Various message types for requests and responses
- A mix of unary and streaming calls
Key Protobuf Concepts
- service: Defines the RPC interface with method declarations
- rpc: Defines an individual remote procedure call
- message: Defines the data structure for requests and responses
- oneof: Allows a field to be one of several types
- repeated: Represents an array or list field
- stream: Indicates a streaming response
Step 2: Compile Protocol Buffers
After defining your proto file, you need to compile it to generate Go code. Gonyx provides a simple command for this:
gonyx compile weather
This command:
- Looks for the
weather.proto
file in yourapp/proto
directory - Generates Go code for the protocol buffer messages and service interface
- Places the generated files in a subdirectory matching the proto name (
app/proto/weather/
)
The generated files include:
weather.pb.go
: Message types and serialization codeweather_grpc.pb.go
: Service interface and client/server code
You can check the generated code to better understand how the protobuf definitions translate to Go types and interfaces.
Step 3: Implement the gRPC Service Controller
Now we'll implement the gRPC service controller in your app/controller.go
file:
3.1: Define the Controller Structure
First, let's define our controller structure that will implement the generated service interface:
// WeatherServiceController - a controller implementing the weather service protobuf
type WeatherServiceController struct {
weather.UnimplementedWeatherServiceServer
// In a real application, this would connect to actual weather data services
weatherData map[string]*weather.WeatherData // Simulated current weather data
forecasts map[string][]*weather.DailyForecast // Simulated forecast data
alerts map[string][]*weather.WeatherAlert // Simulated weather alerts
submittedReports map[string]*weather.WeatherReport // Store for user-submitted reports
}
// NewWeatherServiceController creates a new instance of WeatherServiceController
func NewWeatherServiceController() *WeatherServiceController {
// Initialize with some sample weather data
weatherData := make(map[string]*weather.WeatherData)
forecasts := make(map[string][]*weather.DailyForecast)
alerts := make(map[string][]*weather.WeatherAlert)
submittedReports := make(map[string]*weather.WeatherReport)
// Add sample data for a few cities
cities := []string{"New York", "London", "Tokyo", "Sydney", "Paris"}
// Generate random weather data for each city
for _, city := range cities {
// Current weather
weatherData[city] = generateRandomWeather(city)
// 7-day forecast
forecasts[city] = generateRandomForecast(7)
// Some cities have weather alerts
if rand.Intn(2) == 1 {
alerts[city] = generateRandomAlerts(city, rand.Intn(3)+1)
}
}
return &WeatherServiceController{
weatherData: weatherData,
forecasts: forecasts,
alerts: alerts,
submittedReports: submittedReports,
}
}
3.2: Implement Gonyx Controller Interface
Next, we need to implement methods required by Gonyx to register our gRPC controller:
// GetName returns the controller name for Gonyx framework
func (ctrl *WeatherServiceController) GetName() string {
return "WeatherService"
}
// GetServerNames indicates which gRPC servers this controller should be registered with
func (ctrl *WeatherServiceController) GetServerNames() []string {
return []string{"server1"} // Must match a server defined in the protobuf config
}
These methods are required by the Gonyx framework:
GetName()
: Identifies the controller in the frameworkGetServerNames()
: Specifies which gRPC server configurations to use
3.3: Implement RPC Methods
Now, let's implement each RPC method defined in our service. We'll start with the GetCurrentWeather method:
// GetCurrentWeather fetches current weather for a location
func (ctrl *WeatherServiceController) GetCurrentWeather(ctx context.Context, req *weather.LocationRequest) (*weather.WeatherData, error) {
locationName := getLocationName(req)
if locationName == "" {
return nil, fmt.Errorf("invalid location provided")
}
// Check if we have data for this location
data, exists := ctrl.weatherData[locationName]
if !exists {
// In a real implementation, we would fetch from an external API
// For this example, generate random data for an unknown location
data = generateRandomWeather(locationName)
ctrl.weatherData[locationName] = data
}
return data, nil
}
The GetForecast method implementation:
// GetForecast returns a weather forecast for several days
func (ctrl *WeatherServiceController) GetForecast(ctx context.Context, req *weather.ForecastRequest) (*weather.ForecastResponse, error) {
if req.Location == nil {
return nil, fmt.Errorf("location must be provided")
}
locationName := getLocationName(req.Location)
if locationName == "" {
return nil, fmt.Errorf("invalid location provided")
}
// Limit days to a reasonable range
days := int(req.Days)
if days < 1 {
days = 1
} else if days > 10 {
days = 10
}
// Check if we have forecast data for this location
forecastData, exists := ctrl.forecasts[locationName]
if !exists || len(forecastData) < days {
// Generate random forecast data
forecastData = generateRandomForecast(days)
ctrl.forecasts[locationName] = forecastData
}
// Ensure we return only the requested number of days
if len(forecastData) > days {
forecastData = forecastData[:days]
}
return &weather.ForecastResponse{
LocationName: locationName,
DailyForecasts: forecastData,
ForecastIssuedTime: time.Now().Format(time.RFC3339),
}, nil
}
The GetWeatherAlerts method:
// GetWeatherAlerts returns active weather alerts for a location
func (ctrl *WeatherServiceController) GetWeatherAlerts(ctx context.Context, req *weather.LocationRequest) (*weather.AlertsResponse, error) {
locationName := getLocationName(req)
if locationName == "" {
return nil, fmt.Errorf("invalid location provided")
}
// Check if we have alerts for this location
alertList, exists := ctrl.alerts[locationName]
if !exists {
// No alerts for this location
alertList = []*weather.WeatherAlert{}
}
return &weather.AlertsResponse{
LocationName: locationName,
Alerts: alertList,
}, nil
}
3.4: Implement Streaming Method
Let's implement the streaming method, which demonstrates a more advanced gRPC feature:
// StreamWeatherUpdates provides real-time weather updates
func (ctrl *WeatherServiceController) StreamWeatherUpdates(req *weather.LocationRequest, stream weather.WeatherService_StreamWeatherUpdatesServer) error {
locationName := getLocationName(req)
if locationName == "" {
return fmt.Errorf("invalid location provided")
}
// In a real implementation, this would connect to a real-time data source
// For this example, we'll send current weather and a few updates
// Send initial data
currentWeather, exists := ctrl.weatherData[locationName]
if !exists {
// Generate random data for unknown location
currentWeather = generateRandomWeather(locationName)
ctrl.weatherData[locationName] = currentWeather
}
err := stream.Send(currentWeather)
if err != nil {
return err
}
// In a real implementation, we would continue sending updates as they happen
// For this example, we'll send a few simulated updates (5 updates, 1 second apart)
for i := 0; i < 5; i++ {
// In a real server, we would listen for updates rather than sleeping
time.Sleep(time.Second)
// Generate a slightly modified weather update
update := simulateWeatherChange(currentWeather)
currentWeather = update
err := stream.Send(update)
if err != nil {
return err
}
}
return nil
}
3.5: Submit Weather Report Method
Finally, let's implement the method for submitting a weather report:
// SubmitWeatherReport allows users to submit local weather reports
func (ctrl *WeatherServiceController) SubmitWeatherReport(ctx context.Context, report *weather.WeatherReport) (*weather.SubmissionResponse, error) {
if report == nil {
return nil, fmt.Errorf("report cannot be empty")
}
if report.Location == nil {
return nil, fmt.Errorf("location must be provided")
}
// Validate the report (basic validation for demonstration)
if report.Temperature_celsius < -100 || report.Temperature_celsius > 100 {
return &weather.SubmissionResponse{
Success: false,
Message: "Temperature outside valid range (-100°C to 100°C)",
}, nil
}
// Generate a report ID (in a real system, this would come from a database)
reportID := fmt.Sprintf("report-%d", time.Now().UnixNano())
// Store the report
ctrl.submittedReports[reportID] = report
// In a real system, this report might trigger alerts or be used to improve forecasts
return &weather.SubmissionResponse{
ReportId: reportID,
Success: true,
Message: "Weather report submitted successfully. Thank you for your contribution!",
}, nil
}
Step 4: Register the gRPC Service
Now we need to update our app.go
file to register the gRPC service with the Gonyx framework:
package app
import (
"github.com/Blocktunium/gonyx/pkg/engine"
"google.golang.org/grpc"
"gonyx-user-db/app/proto/weather"
)
// App - application engine structure
type App struct {}
// Init - initialize the app
func (app *App) Init() {
// Register any existing REST controllers
engine.RegisterRestfulController(&SampleController{})
// Create and register the Weather gRPC service
weatherService := NewWeatherServiceController()
engine.RegisterGrpcController(weatherService, func(server *grpc.Server) {
weather.RegisterWeatherServiceServer(server, weatherService)
})
}
The key part of this code is:
engine.RegisterGrpcController(weatherService, func(server *grpc.Server) {
weather.RegisterWeatherServiceServer(server, weatherService)
})
This:
- Takes our controller instance
- Provides a registration function that connects our implementation to the gRPC server
- Uses the generated
RegisterWeatherServiceServer
function from the protobuf compilation
Step 5: Configure the gRPC Server
To complete the setup, we need to configure the gRPC server in a dedicated configuration file. In Gonyx, gRPC server configurations are stored in a specific file. Create or modify your protobuf configuration file:
{
"proto": 3,
"servers": [
"server1"
],
"server1": {
"host": "0.0.0.0",
"port": 7777,
"protocol": "tcp",
"async": true,
"reflection": true,
"configs": {
"maxReceiveMessageSize": 104857600,
"maxSendMessageSize": 1048576000
}
}
}
This configuration specifies:
- A gRPC server named "server1" (matching what we used in
GetServerNames()
) - The server will listen on all interfaces (0.0.0.0) on port 7777
- Enables gRPC reflection for service discovery and testing
- Configures message size limits for large data transfers
- Uses async mode for better performance with multiple concurrent requests
For production environments, you might want to configure TLS for secure communication. This would involve setting up certificates and modifying the protocol configuration in the protobuf.json file.
Testing the gRPC Service
You can test your gRPC service using tools like:
- grpcurl - a command-line tool for interacting with gRPC services
- BloomRPC - a GUI client for gRPC
- gRPC client code - writing a Go client using the generated client code
Here's an example using grpcurl to get current weather:
# List all services
grpcurl -plaintext localhost:7777 list
# List methods for the weather service
grpcurl -plaintext localhost:7777 list weather.WeatherService
# Get current weather for London
grpcurl -plaintext -d '{"city_name": "London"}' localhost:7777 weather.WeatherService/GetCurrentWeather
Conclusion
In this tutorial, you learned how to:
- Define a gRPC service using Protocol Buffers
- Compile proto files to generate Go code
- Implement a gRPC service controller in Gonyx
- Handle different types of gRPC methods (unary and streaming)
- Register and configure a gRPC service in your Gonyx application
gRPC is a powerful tool for building efficient microservices and APIs. Combined with Gonyx's streamlined architecture, you can quickly create robust and performant API services.
Next Steps
- Add authentication and authorization to your gRPC service
- Implement interceptors for logging and error handling
- Create a client application to consume your gRPC service
- Explore bidirectional streaming methods
- Set up production-ready TLS configuration