# Go และ gRPC ในปี 2026: Microservices ประสิทธิภาพสูงและคำถามสัมภาษณ์ > เจาะลึก gRPC กับ Go ในปี 2026 ครอบคลุม Protocol Buffers, RPC แบบ unary และ streaming, interceptor, รูปแบบระดับโปรดักชัน และคำถามสัมภาษณ์ที่พบบ่อยสำหรับ backend engineer - Published: 2026-06-08 - Updated: 2026-06-09 - Author: Anthony Fillion-Maillet - Tags: go, grpc, microservices, backend, interview, protocol-buffers - Reading time: 11 min --- gRPC กลายเป็นเลเยอร์การสื่อสารเริ่มต้นสำหรับ microservices ของ Go ที่ต้องการเวลาแฝงต่ำและสัญญา API ที่เคร่งครัด ด้วย grpc-go v1.81, Go 1.26 และความสมบูรณ์ของระบบนิเวศ [go-grpc-middleware](https://github.com/grpc-ecosystem/go-grpc-middleware) การสร้างบริการ gRPC ระดับโปรดักชันใน Go ทำได้ง่ายกว่าที่เคยเป็นมา และยังคงเป็นหนึ่งในหัวข้อที่ถูกถามมากที่สุดในการสัมภาษณ์งานสาย backend > **gRPC เทียบกับ REST อย่างย่อ** > > gRPC ใช้ HTTP/2 และ Protocol Buffers สำหรับการทำ serialization แบบไบนารี ทำให้ payload เล็กลงได้ถึง 10 เท่าและรองรับ streaming สองทิศทางโดยกำเนิด REST ยังคงเหมาะกว่าสำหรับ API สาธารณะ ขณะที่ gRPC โดดเด่นในการสื่อสารระหว่างบริการที่ประสิทธิภาพและความปลอดภัยของชนิดข้อมูลเป็นเรื่องสำคัญ ## ทำไม gRPC จึงครองการสื่อสาร backend ของ Go สามคุณสมบัติทำให้ gRPC เป็นตัวเลือกที่เป็นธรรมชาติสำหรับ microservices ของ Go ประการแรก Protocol Buffers สร้างโค้ด Go ที่มีชนิดข้อมูลเข้มงวดตั้งแต่ตอนคอมไพล์ จับความไม่ตรงกันของสัญญาก่อนการดีพลอย ประการที่สอง การ multiplexing ของ HTTP/2 ขจัด head-of-line blocking และรองรับ streaming แบบ full-duplex บนการเชื่อมต่อ TCP เดียว ประการที่สาม โมเดล interceptor สอดคล้องกับปรัชญา composition-over-inheritance ของ Go ทำให้เรื่องที่ตัดขวางอย่างการยืนยันตัวตนและ tracing ประกอบเข้าด้วยกันได้ ระบบนิเวศ gRPC ใน Go ได้รวมตัวกันรอบไลบรารีที่ผ่านการพิสูจน์มาแล้วไม่กี่ตัว [grpc-go](https://github.com/grpc/grpc-go) จัดการแกนหลักของ transport และ codegen แพ็กเกจ [go-grpc-middleware v2](https://pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware/v2) มอบ interceptor ระดับโปรดักชันสำหรับ logging, metrics, การยืนยันตัวตน และ recovery [stats handler ของ gRPC จาก OpenTelemetry](https://opentelemetry.io/docs/languages/go/) ครอบคลุม distributed tracing โดยไม่ต้องเขียนโค้ด interceptor เอง ## การนิยามบริการด้วย Protocol Buffers บริการ gRPC ทุกตัวเริ่มต้นจากไฟล์ `.proto` สคีมานิยามสัญญาของบริการ ชนิดของ message และเมธอด RPC ตัวอย่างต่อไปนี้จำลองบริการผู้ใช้ด้วยการค้นหาแบบ unary และเมธอด server-streaming สำหรับฟีดกิจกรรม ```protobuf // user_service.proto syntax = "proto3"; package user.v1; option go_package = "gen/user/v1;userv1"; service UserService { // Unary RPC: single request, single response rpc GetUser(GetUserRequest) returns (GetUserResponse); // Server streaming: single request, stream of responses rpc StreamActivity(StreamActivityRequest) returns (stream ActivityEvent); } message GetUserRequest { string user_id = 1; } message GetUserResponse { string user_id = 1; string email = 2; string display_name = 3; int64 created_at_unix = 4; } message StreamActivityRequest { string user_id = 1; int32 limit = 2; } message ActivityEvent { string event_id = 1; string action = 2; string resource = 3; int64 timestamp_unix = 4; } ``` การรัน `protoc --go_out=. --go-grpc_out=. user_service.proto` จะสร้างไฟล์สองไฟล์ ไฟล์หนึ่งมีชนิดของ message และอีกไฟล์มีอินเทอร์เฟซ client/server ของ gRPC อินเทอร์เฟซ server ที่ถูกสร้างขึ้นคือสิ่งที่การ implement ฝั่ง Go ต้องทำให้สำเร็จ ## การ implement server gRPC ใน Go การ implement server ฝัง struct `UnimplementedUserServiceServer` ที่ถูกสร้างขึ้น ซึ่งให้ความเข้ากันได้ไปข้างหน้า การเพิ่ม RPC ใหม่ลงในไฟล์ proto จะไม่ทำให้โค้ด server เดิมพังจนกว่าจะ implement เมธอดนั้นอย่างชัดเจน ```go // server/user_server.go package server import ( "context" "fmt" "time" userv1 "myapp/gen/user/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type UserServer struct { userv1.UnimplementedUserServiceServer store UserStore // interface for DB access } func NewUserServer(store UserStore) *UserServer { return &UserServer{store: store} } // GetUser handles the unary RPC func (s *UserServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { if req.GetUserId() == "" { return nil, status.Error(codes.InvalidArgument, "user_id is required") } user, err := s.store.FindByID(ctx, req.GetUserId()) if err != nil { return nil, status.Errorf(codes.Internal, "lookup failed: %v", err) } if user == nil { return nil, status.Error(codes.NotFound, "user not found") } return &userv1.GetUserResponse{ UserId: user.ID, Email: user.Email, DisplayName: user.DisplayName, CreatedAtUnix: user.CreatedAt.Unix(), }, nil } // StreamActivity sends activity events as a server stream func (s *UserServer) StreamActivity(req *userv1.StreamActivityRequest, stream userv1.UserService_StreamActivityServer) error { events, err := s.store.GetActivity(stream.Context(), req.GetUserId(), int(req.GetLimit())) if err != nil { return status.Errorf(codes.Internal, "activity fetch failed: %v", err) } for _, evt := range events { if err := stream.Send(&userv1.ActivityEvent{ EventId: evt.ID, Action: evt.Action, Resource: evt.Resource, TimestampUnix: evt.Timestamp.Unix(), }); err != nil { return fmt.Errorf("stream send: %w", err) } } return nil } ``` มีสองรูปแบบที่ควรสังเกต `status.Error` และ `status.Errorf` สร้าง error แบบ gRPC-native พร้อมรหัสสถานะที่ถูกต้อง ซึ่ง client สามารถตรวจสอบได้ในเชิงโปรแกรม การฝัง `UnimplementedUserServiceServer` รับประกันความปลอดภัยตอนคอมไพล์เมื่อ proto มีการเปลี่ยนแปลง ## การ bootstrap server โปรดักชันด้วย interceptor server gRPC เปล่า ๆ จัดการคำขอได้แต่ขาดความสามารถในการสังเกตการณ์ การยืนยันตัวตน และการกู้คืนจาก panic บริการระดับโปรดักชันต้องการห่วงโซ่ interceptor ไลบรารี [go-grpc-middleware v2](https://github.com/grpc-ecosystem/go-grpc-middleware) มอบ interceptor ที่ประกอบเข้าด้วยกันได้ และเชื่อมต่อกันด้วย `grpc.ChainUnaryInterceptor` ```go // cmd/server/main.go package main import ( "log" "net" "os" "os/signal" "syscall" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/ratelimit" userv1 "myapp/gen/user/v1" "myapp/server" ) func main() { // Interceptor order matters: recovery first, then metrics, then auth srv := grpc.NewServer( // OpenTelemetry tracing via stats handler (not interceptor) grpc.StatsHandler(otelgrpc.NewServerHandler()), grpc.ChainUnaryInterceptor( recovery.UnaryServerInterceptor(), // catch panics ratelimit.UnaryServerInterceptor(limiter), // rate limiting logging.UnaryServerInterceptor(logger), // structured logging authInterceptor, // token validation ), grpc.ChainStreamInterceptor( recovery.StreamServerInterceptor(), ratelimit.StreamServerInterceptor(limiter), logging.StreamServerInterceptor(logger), ), ) // Register service implementation userv1.RegisterUserServiceServer(srv, server.NewUserServer(store)) // Enable reflection for grpcurl and debugging reflection.Register(srv) lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("listen: %v", err) } // Graceful shutdown on SIGTERM go func() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT) <-sig log.Println("shutting down gRPC server") srv.GracefulStop() }() log.Printf("gRPC server listening on :50051") if err := srv.Serve(lis); err != nil { log.Fatalf("serve: %v", err) } } ``` ลำดับของ interceptor กำหนดความสำคัญในการประมวลผล recovery ทำงานก่อนเพื่อจับ panic จาก interceptor ใด ๆ ที่อยู่ถัดลงไป rate limiting ทำงานก่อนการยืนยันตัวตนเพื่อปกป้องชั้นยืนยันตัวตนเองจากการถูกใช้งานในทางที่ผิด tracing ของ OpenTelemetry ใช้ `StatsHandler` แทน interceptor ซึ่งเป็น[แนวทางที่แนะนำ](https://grpc.io/docs/guides/interceptors/)ตั้งแต่ grpc-go v1.81 เพราะ stats handler เข้าถึงเหตุการณ์ transport ระดับล่างได้ ## รูปแบบการจัดการ error ของ gRPC ใน Go การจัดการ error ที่เหมาะสมแยกบริการ gRPC ระดับโปรดักชันออกจากต้นแบบ gRPC นิยามชุด[รหัสสถานะมาตรฐาน](https://grpc.io/docs/guides/status-codes/)ที่ client ทุกตัวเข้าใจ แพ็กเกจ `status` ของ Go แมปรหัสเหล่านี้เป็น error รูปแบบสำคัญ ได้แก่ - คืนค่า `codes.InvalidArgument` สำหรับคำขอที่มีรูปแบบผิด (บั๊กของ client) - คืนค่า `codes.NotFound` เมื่อทรัพยากรไม่มีอยู่ - คืนค่า `codes.Internal` สำหรับความล้มเหลวของ server ที่ไม่คาดคิด - คืนค่า `codes.Unauthenticated` เมื่อข้อมูลรับรองตัวตนหายไปหรือไม่ถูกต้อง - คืนค่า `codes.PermissionDenied` เมื่อข้อมูลรับรองตัวตนถูกต้องแต่ไม่เพียงพอ ```go // errors.go — reusable error constructors package rpcerr import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // NotFound wraps a resource-not-found error with a consistent message func NotFound(resource, id string) error { return status.Errorf(codes.NotFound, "%s %q not found", resource, id) } // InvalidArg wraps a validation error func InvalidArg(field, reason string) error { return status.Errorf(codes.InvalidArgument, "%s: %s", field, reason) } // Internal wraps an unexpected error, hiding internals from the client func Internal(err error) error { // Log the real error server-side; return generic message to client return status.Error(codes.Internal, "internal server error") } ``` การห่อ error ในตัวสร้างที่เฉพาะเจาะจงต่อโดเมนช่วยให้รหัสสถานะคงเส้นคงวาตลอดทุก RPC client สามารถใช้ switch บน `status.Code(err)` เพื่อจัดการแต่ละกรณีได้โดยไม่ต้องแยกวิเคราะห์สตริงของ error ## การทดสอบบริการ gRPC ด้วย bufconn การทดสอบแบบ integration ของบริการ gRPC โดยปกติต้องเริ่ม server TCP จริง แพ็กเกจ `bufconn` มอบ listener แบบ in-memory ที่ขจัดการจัดสรรพอร์ตและ overhead ของเครือข่ายไปอย่างสิ้นเชิง ```go // server_test.go package server_test import ( "context" "net" "testing" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" userv1 "myapp/gen/user/v1" "myapp/server" ) const bufSize = 1024 * 1024 func setupServer(t *testing.T) userv1.UserServiceClient { t.Helper() lis := bufconn.Listen(bufSize) // in-memory listener srv := grpc.NewServer() userv1.RegisterUserServiceServer(srv, server.NewUserServer(mockStore{})) go func() { if err := srv.Serve(lis); err != nil { t.Errorf("server exited: %v", err) } }() t.Cleanup(srv.GracefulStop) // Dial the in-memory listener conn, err := grpc.NewClient( "passthrough:///bufconn", grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { t.Fatalf("dial bufconn: %v", err) } t.Cleanup(func() { conn.Close() }) return userv1.NewUserServiceClient(conn) } func TestGetUser_NotFound(t *testing.T) { client := setupServer(t) _, err := client.GetUser(context.Background(), &userv1.GetUserRequest{ UserId: "nonexistent", }) // Verify gRPC status code st, ok := status.FromError(err) if !ok || st.Code() != codes.NotFound { t.Errorf("expected NotFound, got %v", err) } } ``` `bufconn` สร้างการเชื่อมต่อ gRPC จริงพร้อมการ serialization เต็มรูปแบบและการรัน interceptor เพียงแต่ไม่ผ่าน TCP การทดสอบรันเร็วขึ้นและสามารถรันแบบขนานได้โดยไม่ชนกันที่พอร์ต นี่คือรูปแบบมาตรฐานที่ใช้ในตัวรีโพ grpc-go เอง ## การรักษาความปลอดภัย gRPC ด้วย mTLS และการยืนยันตัวตนด้วยโทเคน บริการ gRPC ระดับโปรดักชันต้องการความปลอดภัยของ transport มีสองแนวทางที่โดดเด่น ได้แก่ mTLS สำหรับการยืนยันตัวตนระหว่างบริการที่ทั้งสองฝั่งแสดงใบรับรอง และการยืนยันตัวตนด้วยโทเคน (JWT หรือ API key) สำหรับตัวตนของ client ภายในช่องทางที่ปลอดภัยอยู่แล้ว การตั้งค่า mTLS โหลดใบรับรองตอน server เริ่มทำงาน ```go // tls.go — mTLS server credentials package main import ( "crypto/tls" "crypto/x509" "os" "google.golang.org/grpc/credentials" ) func loadTLSCredentials(certFile, keyFile, caFile string) (credentials.TransportCredentials, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err } caPool := x509.NewCertPool() caPEM, err := os.ReadFile(caFile) if err != nil { return nil, err } caPool.AppendCertsFromPEM(caPEM) tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, // enforce mTLS ClientCAs: caPool, MinVersion: tls.VersionTLS13, // TLS 1.3 minimum in 2026 } return credentials.NewTLS(tlsCfg), nil } ``` TLS 1.3 เป็นเส้นฐานสำหรับบริการ gRPC ในปี 2026 ให้ handshake ที่เร็วขึ้น (1-RTT) และชุด cipher ที่แข็งแกร่งกว่า TLS 1.2 สำหรับการยืนยันตัวตนด้วยโทเคนที่วางซ้อนบน TLS interceptor แบบ unary จะดึงโทเคนออกจาก metadata ของ gRPC และตรวจสอบความถูกต้องก่อนที่ handler จะทำงาน > **การหมุนเวียนใบรับรอง mTLS** > > เส้นทางใบรับรองที่ฮาร์ดโค้ดทำให้ต้องรีสตาร์ท server เพื่อหมุนเวียน การดีพลอยระดับโปรดักชันควรใช้ `tls.Config.GetCertificate` หรือ sidecar อย่าง Envoy เพื่อจัดการการต่ออายุใบรับรองอัตโนมัติโดยไม่มี downtime ## คำถามสัมภาษณ์: gRPC และ microservices ของ Go คำถามต่อไปนี้ปรากฏบ่อยในการสัมภาษณ์งาน backend engineering ที่เน้น Go และระบบกระจาย แต่ละคำตอบมุ่งไปที่ระดับความลึกที่คาดหวังในระดับ senior **RPC ทั้งสี่ชนิดใน gRPC มีอะไรบ้าง และแต่ละชนิดเหมาะกับเมื่อใด** Unary (หนึ่งคำขอ หนึ่งคำตอบ) ครอบคลุมการทำงาน CRUD ส่วนใหญ่ Server streaming เหมาะกับสถานการณ์ที่ server ผลักผลลัพธ์หลายรายการ เช่น ฟีดกิจกรรม ผลการค้นหา การอัปเดตแบบเรียลไทม์ Client streaming ใช้กับสถานการณ์อัปโหลดหรือการเขียนแบบกลุ่ม ที่ client ส่งหลาย message ก่อน server จะตอบกลับ Bidirectional streaming ตอบโจทย์แชต การแก้ไขร่วมกัน หรือโปรโตคอลใด ๆ ที่ทั้งสองฝั่งส่งอย่างเป็นอิสระ **gRPC จัดการความเข้ากันได้ย้อนหลังอย่างไรเมื่อ field ของ proto ถูกลบ** Proto3 ไม่เคยใช้หมายเลข field ซ้ำ การลบ field หมายความว่า server จะเพิกเฉยต่อค่านั้นหาก client เก่ายังส่งมาอยู่ และ client เก่าที่อ่านคำตอบจะเห็นเพียงค่า zero สำหรับ field ที่ถูกลบ คีย์เวิร์ด `reserved` ป้องกันการนำหมายเลขหรือชื่อ field กลับมาใช้ซ้ำโดยไม่ตั้งใจ สิ่งนี้แตกต่างโดยพื้นฐานจาก API แบบ JSON ที่การลบ field อาจทำให้ deserialization ล้มเหลว > **กฎความเข้ากันได้ของ wire ใน Protobuf** > > อย่าเปลี่ยนชนิดหรือหมายเลขของ field ที่มีอยู่แล้ว เพิ่ม field ใหม่ด้วยหมายเลขใหม่ ใช้ `reserved` เพื่อปลดระวางหมายเลขเก่า กฎนี้ใช้ได้กับทุกภาษาและรับประกันการดีพลอยโดยไม่มี downtime **อธิบายบทบาทของ interceptor ในบริการ gRPC ระดับโปรดักชัน** interceptor คือเลเยอร์ middleware ของ gRPC พวกมันทำงานก่อนและหลัง handler ตามลำดับที่ลงทะเบียน ห่วงโซ่โปรดักชันทั่วไป ได้แก่ recovery (จับ panic) > rate limiting > logging > การยืนยันตัวตน > validation ไลบรารี [go-grpc-middleware v2](https://github.com/grpc-ecosystem/go-grpc-middleware) มอบ interceptor ที่ประกอบกันได้ตามรูปแบบนี้ interceptor ไม่สามารถแก้ไขเลเยอร์ transport (TLS, พอร์ต) ได้ มันทำงานในระดับ RPC เท่านั้น **ความแตกต่างระหว่าง `grpc.StatsHandler` กับ interceptor คืออะไร** interceptor ห่อ handler ของ RPC และทำงานในระดับแอปพลิเคชัน stats handler รับเหตุการณ์ระดับ transport เช่น การเริ่มเชื่อมต่อ การส่ง/รับ message การเสร็จสิ้นของ RPC OpenTelemetry ใช้ stats handler เพราะมันจับ metric ที่ interceptor มองไม่เห็น เช่น จำนวนไบต์และเหตุการณ์วงจรชีวิตของการเชื่อมต่อ ตั้งแต่ grpc-go v1.66+ [คำแนะนำอย่างเป็นทางการ](https://grpc.io/docs/guides/interceptors/)คือใช้ stats handler สำหรับการสังเกตการณ์ และ interceptor สำหรับตรรกะทางธุรกิจ **คุณจะ implement graceful shutdown สำหรับ server gRPC อย่างไร** `GracefulStop()` หยุดรับการเชื่อมต่อใหม่และรอให้ RPC ที่กำลังทำงานเสร็จสิ้น รูปแบบคือ ฟัง SIGTERM ใน goroutine เรียก `GracefulStop()` จากนั้นการเรียก `Serve()` จะคืนค่ากลับ สำหรับ RPC แบบ streaming ที่อาจทำงานไม่จำกัดเวลา ให้กำหนด deadline ด้วยการยกเลิก context หรือใช้ health check ที่หยุดคืนค่า `SERVING` ก่อนเริ่ม shutdown เพื่อให้ load balancer มีเวลาระบายทราฟฟิก **ทีมควรเลือก gRPC แทน REST สำหรับบริการภายในเมื่อใด** gRPC เป็นตัวเลือกที่แข็งแกร่งกว่าเมื่อ บริการต้องการสัญญา API ที่เคร่งครัดและบังคับใช้ตอนคอมไพล์ ระบบต้องการ streaming (server push, สองทิศทาง) เวลาแฝงสำคัญและการ serialization แบบไบนารีช่วยลดขนาด payload ทีมดูแลทั้งโค้ด client และ server REST เหมาะกว่าสำหรับ API สาธารณะที่บริโภคโดยเบราว์เซอร์ (JSON โดยกำเนิด ไม่ต้องใช้ proxy) API ที่มีการ caching หนักผ่านความหมายของ HTTP หรือทีมที่ต้องการความเข้ากันได้ของ tooling สูงสุด สถาปัตยกรรมจำนวนมากใช้ทั้งสองอย่าง [gRPC ภายในพร้อมเกตเวย์ REST](https://grpc.io/docs/languages/go/quickstart/)สำหรับผู้บริโภคภายนอก ## บทสรุป - Protocol Buffers บังคับใช้สัญญา API ตอนคอมไพล์ การเปลี่ยนแปลงที่ทำให้พังจะปรากฏตอนสร้างโค้ด ไม่ใช่ตอน runtime - รูปแบบ `UnimplementedServer` ให้ความเข้ากันได้ไปข้างหน้าเมื่อเพิ่ม RPC ใหม่ลงในบริการที่กำลังพัฒนา - ลำดับ interceptor สำคัญ recovery ก่อน แล้วจึง rate limiting แล้วจึงการยืนยันตัวตน ไลบรารี [go-grpc-middleware v2](https://github.com/grpc-ecosystem/go-grpc-middleware) จัดการการเชื่อมต่อให้ - ใช้ `grpc.StatsHandler` กับ OpenTelemetry สำหรับ tracing สงวน interceptor ไว้สำหรับตรรกะทางธุรกิจอย่างการยืนยันตัวตนและ validation - `bufconn` ช่วยให้การทดสอบ integration เร็วและไม่ต้องใช้พอร์ต โดยรันทั้งสแตก gRPC รวมถึงการ serialization และ interceptor - mTLS กับ TLS 1.3 เป็นเส้นฐานปี 2026 สำหรับความปลอดภัยระหว่างบริการ วางการยืนยันตัวตนด้วยโทเคนซ้อนทับเพื่อระบุตัวตนภายใน mesh - เตรียมตัวสำหรับ[คำถามสัมภาษณ์ gRPC](/technologies/go/interview-questions/grpc)ด้วยการเข้าใจ RPC ทั้งสี่ชนิด กฎความเข้ากันได้ย้อนหลังของ proto และความแตกต่างระหว่าง interceptor กับ stats handler --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/go/go-grpc-microservices-2026