From fd2ba2e9995e2188e87d5dfc0a6830a1c61d5093 Mon Sep 17 00:00:00 2001 From: Simon Malm Date: Wed, 13 Aug 2025 19:12:51 +0200 Subject: [PATCH] feat: first --- .gitignore | 43 +++++++++++++++ README.md | 68 ++++++++++++++++++++++++ buf.gen.yaml | 17 ++++++ buf.yaml | 10 ++++ cmd/server/main.go | 84 ++++++++++++++++++++++++++++++ go.mod | 19 +++++++ go.sum | 38 ++++++++++++++ justfile | 55 +++++++++++++++++++ proto/helloworld/hello_world.proto | 24 +++++++++ 9 files changed, 358 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 justfile create mode 100644 proto/helloworld/hello_world.proto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c88ecf --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Build output +bin/ + +# Generated protobuf files +proto/**/*.pb.go +proto/**/*.pb.gw.go + +# Buf lock file +buf.lock + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce571da --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Go gRPC Gateway Template + +A simplified Go project template for gRPC services with HTTP/JSON gateway support, based on the [Go Standard Project Layout](https://github.com/golang-standards/project-layout). + +## Project Structure + +``` +├── cmd/server/ # Main application entry point +├── internal/ +│ ├── handler/ # gRPC and gateway handlers +│ └── service/ # Business logic +├── pkg/pb/ # Generated protobuf files (auto-generated) +├── api/v1/ # Protocol buffer definitions +└── justfile # Task runner configuration +``` + +## Prerequisites + +- Go 1.21+ +- Protocol Buffers compiler (`protoc`) +- [just](https://github.com/casey/just) task runner +- [nushell](https://www.nushell.sh/) (for justfile execution) + +## Getting Started + +1. Install dependencies: + ```bash + just deps + ``` + +2. Generate protobuf files: + ```bash + just proto + ``` + +3. Build and run the server: + ```bash + just dev + ``` + +## Available Commands + +- `just proto` - Generate protobuf files +- `just build` - Build the server binary +- `just run` - Run the server +- `just dev` - Full development workflow (deps + proto + run) +- `just test` - Run tests +- `just lint` - Run linter +- `just clean` - Clean build artifacts + +## API Endpoints + +The server runs on two ports: +- gRPC server: `:8080` +- HTTP gateway: `:8081` + +### Example HTTP endpoints: +- `GET /v1/examples` - List all examples +- `POST /v1/examples` - Create a new example +- `GET /v1/examples/{id}` - Get a specific example + +## Monorepo Integration + +This template is designed to be integration-friendly for monorepo structures by: +- Excluding shared proto folders +- Using internal packages for service-specific logic +- Minimal external dependencies +- Clear separation of concerns \ No newline at end of file diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..c42e73f --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,17 @@ +version: v2 +plugins: + # generate go structs for protocol buffer definition + - local: protoc-gen-go + out: . + opt: + - paths=source_relative + # generate gRPC stubs in golang + - local: protoc-gen-go-grpc + out: . + opt: + - paths=source_relative + # generate reverse proxy from protocol definitions + - local: protoc-gen-grpc-gateway + out: . + opt: + - paths=source_relative \ No newline at end of file diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..04b85a4 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,10 @@ +version: v2 +name: buf.build/grpc-gateway-template/proto +deps: + - buf.build/googleapis/googleapis +breaking: + use: + - FILE +lint: + use: + - DEFAULT \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..95208be --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "log" + "net" + "net/http" + "sync" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + helloworldpb "go-grpc-gateway-template/proto/helloworld" +) + +type server struct { + helloworldpb.UnimplementedGreeterServer +} + +func (s *server) SayHello(ctx context.Context, req *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) { + return &helloworldpb.HelloReply{ + Message: "Hello " + req.GetName(), + }, nil +} + +func main() { + // Create a listener on TCP port + lis, err := net.Listen("tcp", ":8080") + if err != nil { + log.Fatalln("Failed to listen:", err) + } + + // Create a gRPC server object + s := grpc.NewServer() + // Attach the Greeter service to the server + helloworldpb.RegisterGreeterServer(s, &server{}) + + var wg sync.WaitGroup + wg.Add(2) + + // Serve gRPC server + log.Println("Serving gRPC on 0.0.0.0:8080") + go func() { + defer wg.Done() + if err := s.Serve(lis); err != nil { + log.Fatalln("Failed to serve:", err) + } + }() + + // Create a client connection to the gRPC server we just started + // This is where the gRPC-Gateway proxies the requests + conn, err := grpc.DialContext( + context.Background(), + "0.0.0.0:8080", + grpc.WithBlock(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatalln("Failed to dial server:", err) + } + + gwmux := runtime.NewServeMux() + // Register Greeter + err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn) + if err != nil { + log.Fatalln("Failed to register gateway:", err) + } + + gwServer := &http.Server{ + Addr: ":8090", + Handler: gwmux, + } + + log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090") + go func() { + defer wg.Done() + if err := gwServer.ListenAndServe(); err != nil { + log.Fatalln("Failed to serve gRPC-Gateway:", err) + } + }() + + wg.Wait() +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c2bc682 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module go-grpc-gateway-template + +go 1.23.0 + +toolchain go1.23.1 + +require ( + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 + google.golang.org/grpc v1.74.2 + google.golang.org/protobuf v1.36.6 +) + +require ( + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..926ca3e --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/justfile b/justfile new file mode 100644 index 0000000..1471256 --- /dev/null +++ b/justfile @@ -0,0 +1,55 @@ +#!/usr/bin/env nu + +# Update buf dependencies +buf-deps: + buf dep update + +# Generate protobuf files +proto: buf-deps + buf generate + +# Build the server binary +build: proto + go build -o bin/server cmd/server/main.go + +# Run the server +run: build + ./bin/server + +# Clean build artifacts +clean: + rm -rf bin/ + rm -rf pkg/pb/ + +# Run tests +test: + go test ./... + +# Run linter +lint: + golangci-lint run + +# Install required tools and dependencies +install-deps: + @echo "Installing buf..." + @if ! command -v buf >/dev/null 2>&1; then \ + mkdir -p ~/.local/bin && \ + curl -sSL "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m)" -o "$HOME/.local/bin/buf" && \ + chmod +x "$HOME/.local/bin/buf"; \ + echo "buf installed to ~/.local/bin/buf"; \ + echo "Make sure ~/.local/bin is in your PATH"; \ + else \ + echo "buf already installed"; \ + fi + @echo "Installing protoc plugins..." + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest + +# Download and tidy dependencies +deps: + go mod download + go mod tidy + +# Development workflow +dev: deps proto run diff --git a/proto/helloworld/hello_world.proto b/proto/helloworld/hello_world.proto new file mode 100644 index 0000000..7958873 --- /dev/null +++ b/proto/helloworld/hello_world.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +option go_package = "go-grpc-gateway-template/proto/helloworld"; + +package helloworld; + +import "google/api/annotations.proto"; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/example/echo" + body: "*" + }; + } +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} \ No newline at end of file