Golang中如何寻找志愿者帮助新手搭建简单的gRPC MySQL CRUD客户端服务端架构

Golang中如何寻找志愿者帮助新手搭建简单的gRPC MySQL CRUD客户端服务端架构

大家好,

我是编程新手之一,正在尝试学习如何建立一个简单、地道且清晰的配置来练习。

主要想法是创建3个GitHub仓库:/proto、/server、/client
以及本地托管的MariaDB或MySQL数据库。

目标是构建一个功能完整的简单Web应用,包含客户列表,并支持列出、创建、读取(单个客户)、更新和删除操作。可能还会包含其他一些常用方法。

我计划将这个教程公开分享,比如发布在Google Pages上。

技术栈:Go、gRPC、MariaDB、少量HTML和CSS。我希望避免使用任何花哨的外部库,只使用标准库。或许可以例外使用Gorilla Mux。

必须至少实现TLS。或许还可以加入认证拦截器...或通用的请求授权机制,但这可能对新手来说太难了。

有人愿意帮忙吗?我乐于接受任何建议。

更多关于Golang中如何寻找志愿者帮助新手搭建简单的gRPC MySQL CRUD客户端服务端架构的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中如何寻找志愿者帮助新手搭建简单的gRPC MySQL CRUD客户端服务端架构的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是一个基于Go语言的gRPC + MySQL CRUD客户端服务端架构的完整实现方案,包含TLS支持和基础认证拦截器。我将按照你描述的仓库结构进行组织。

1. proto仓库 (/proto)

customer.proto

syntax = "proto3";

package customer;

option go_package = "/proto";

service CustomerService {
  rpc ListCustomers(ListCustomersRequest) returns (ListCustomersResponse);
  rpc GetCustomer(GetCustomerRequest) returns (Customer);
  rpc CreateCustomer(CreateCustomerRequest) returns (Customer);
  rpc UpdateCustomer(UpdateCustomerRequest) returns (Customer);
  rpc DeleteCustomer(DeleteCustomerRequest) returns (DeleteCustomerResponse);
}

message Customer {
  string id = 1;
  string name = 2;
  string email = 3;
  string phone = 4;
}

message ListCustomersRequest {
  int32 page = 1;
  int32 limit = 2;
}

message ListCustomersResponse {
  repeated Customer customers = 1;
  int32 total = 2;
}

message GetCustomerRequest {
  string id = 1;
}

message CreateCustomerRequest {
  string name = 1;
  string email = 2;
  string phone = 3;
}

message UpdateCustomerRequest {
  string id = 1;
  string name = 2;
  string email = 3;
  string phone = 4;
}

message DeleteCustomerRequest {
  string id = 1;
}

message DeleteCustomerResponse {
  bool success = 1;
}

生成Go代码:

protoc --go_out=. --go-grpc_out=. customer.proto

2. server仓库 (/server)

数据库初始化 (database.go)

package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

func initDB() *sql.DB {
	dsn := "username:password@tcp(localhost:3306)/customerdb?parseTime=true"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec(`
		CREATE TABLE IF NOT EXISTS customers (
			id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
			name VARCHAR(100) NOT NULL,
			email VARCHAR(100) UNIQUE NOT NULL,
			phone VARCHAR(20),
			created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
			updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
		)
	`)
	if err != nil {
		log.Fatal(err)
	}

	return db
}

gRPC服务实现 (server.go)

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"database/sql"
	"fmt"
	"log"
	"net"
	"os"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"

	pb "github.com/yourusername/proto"
)

type customerServer struct {
	pb.UnimplementedCustomerServiceServer
	db *sql.DB
}

func (s *customerServer) ListCustomers(ctx context.Context, req *pb.ListCustomersRequest) (*pb.ListCustomersResponse, error) {
	if err := authenticate(ctx); err != nil {
		return nil, err
	}

	page := int(req.GetPage())
	if page < 1 {
		page = 1
	}
	limit := int(req.GetLimit())
	if limit < 1 || limit > 100 {
		limit = 10
	}
	offset := (page - 1) * limit

	rows, err := s.db.Query("SELECT id, name, email, phone FROM customers LIMIT ? OFFSET ?", limit, offset)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}
	defer rows.Close()

	var customers []*pb.Customer
	for rows.Next() {
		var c pb.Customer
		if err := rows.Scan(&c.Id, &c.Name, &c.Email, &c.Phone); err != nil {
			return nil, status.Error(codes.Internal, err.Error())
		}
		customers = append(customers, &c)
	}

	var total int
	err = s.db.QueryRow("SELECT COUNT(*) FROM customers").Scan(&total)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &pb.ListCustomersResponse{
		Customers: customers,
		Total:     int32(total),
	}, nil
}

func (s *customerServer) GetCustomer(ctx context.Context, req *pb.GetCustomerRequest) (*pb.Customer, error) {
	if err := authenticate(ctx); err != nil {
		return nil, err
	}

	var customer pb.Customer
	err := s.db.QueryRow("SELECT id, name, email, phone FROM customers WHERE id = ?", req.GetId()).
		Scan(&customer.Id, &customer.Name, &customer.Email, &customer.Phone)
	if err == sql.ErrNoRows {
		return nil, status.Error(codes.NotFound, "customer not found")
	}
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &customer, nil
}

func (s *customerServer) CreateCustomer(ctx context.Context, req *pb.CreateCustomerRequest) (*pb.Customer, error) {
	if err := authenticate(ctx); err != nil {
		return nil, err
	}

	result, err := s.db.Exec("INSERT INTO customers (name, email, phone) VALUES (?, ?, ?)",
		req.GetName(), req.GetEmail(), req.GetPhone())
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	var customer pb.Customer
	customer.Name = req.GetName()
	customer.Email = req.GetEmail()
	customer.Phone = req.GetPhone()

	id, err := result.LastInsertId()
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	// 获取生成的UUID
	err = s.db.QueryRow("SELECT id FROM customers WHERE id = ?", id).Scan(&customer.Id)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &customer, nil
}

func (s *customerServer) UpdateCustomer(ctx context.Context, req *pb.UpdateCustomerRequest) (*pb.Customer, error) {
	if err := authenticate(ctx); err != nil {
		return nil, err
	}

	result, err := s.db.Exec("UPDATE customers SET name = ?, email = ?, phone = ? WHERE id = ?",
		req.GetName(), req.GetEmail(), req.GetPhone(), req.GetId())
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}
	if rows == 0 {
		return nil, status.Error(codes.NotFound, "customer not found")
	}

	return &pb.Customer{
		Id:    req.GetId(),
		Name:  req.GetName(),
		Email: req.GetEmail(),
		Phone: req.GetPhone(),
	}, nil
}

func (s *customerServer) DeleteCustomer(ctx context.Context, req *pb.DeleteCustomerRequest) (*pb.DeleteCustomerResponse, error) {
	if err := authenticate(ctx); err != nil {
		return nil, err
	}

	result, err := s.db.Exec("DELETE FROM customers WHERE id = ?", req.GetId())
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &pb.DeleteCustomerResponse{
		Success: rows > 0,
	}, nil
}

// 简单的认证拦截器
func authenticate(ctx context.Context) error {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Error(codes.Unauthenticated, "missing credentials")
	}

	auth := md["authorization"]
	if len(auth) == 0 || auth[0] != "Bearer secret-token" {
		return status.Error(codes.Unauthenticated, "invalid credentials")
	}

	return nil
}

func loadTLSCredentials() (credentials.TransportCredentials, error) {
	serverCert, err := tls.LoadX509KeyPair("cert/server-cert.pem", "cert/server-key.pem")
	if err != nil {
		return nil, err
	}

	clientCA, err := os.ReadFile("cert/client-ca-cert.pem")
	if err != nil {
		return nil, err
	}

	certPool := x509.NewCertPool()
	if !certPool.AppendCertsFromPEM(clientCA) {
		return nil, fmt.Errorf("failed to add client CA certificate")
	}

	config := &tls.Config{
		Certificates: []tls.Certificate{serverCert},
		ClientAuth:   tls.RequireAndVerifyClientCert,
		ClientCAs:    certPool,
	}

	return credentials.NewTLS(config), nil
}

func main() {
	db := initDB()
	defer db.Close()

	tlsCredentials, err := loadTLSCredentials()
	if err != nil {
		log.Fatal("cannot load TLS credentials: ", err)
	}

	server := grpc.NewServer(
		grpc.Creds(tlsCredentials),
	)
	pb.RegisterCustomerServiceServer(server, &customerServer{db: db})

	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Server started on :50051")
	if err := server.Serve(lis); err != nil {
		log.Fatal(err)
	}
}

3. client仓库 (/client)

gRPC客户端 (client.go)

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"log"
	"os"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"

	pb "github.com/yourusername/proto"
)

func loadTLSCredentials() (credentials.TransportCredentials, error) {
	clientCert, err := tls.LoadX509KeyPair("cert/client-cert.pem", "cert/client-key.pem")
	if err != nil {
		return nil, err
	}

	serverCA, err := os.ReadFile("cert/server-ca-cert.pem")
	if err != nil {
		return nil, err
	}

	certPool := x509.NewCertPool()
	if !certPool.AppendCertsFromPEM(serverCA) {
		return nil, fmt.Errorf("failed to add server CA certificate")
	}

	config := &tls.Config{
		Certificates: []tls.Certificate{clientCert},
		RootCAs:      certPool,
		ServerName:   "localhost",
	}

	return credentials.NewTLS(config), nil
}

type CustomerClient struct {
	client pb.CustomerServiceClient
	conn   *grpc.ClientConn
}

func NewCustomerClient(serverAddr string) (*CustomerClient, error) {
	tlsCredentials, err := loadTLSCredentials()
	if err != nil {
		return nil, err
	}

	conn, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(tlsCredentials))
	if err != nil {
		return nil, err
	}

	client := pb.NewCustomerServiceClient(conn)
	return &CustomerClient{client: client, conn: conn}, nil
}

func (c *CustomerClient) Close() {
	c.conn.Close()
}

func (c *CustomerClient) withAuth(ctx context.Context) context.Context {
	return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer secret-token")
}

func (c *CustomerClient) ListCustomers(page, limit int32) (*pb.ListCustomersResponse, error) {
	ctx := c.withAuth(context.Background())
	return c.client.ListCustomers(ctx, &pb.ListCustomersRequest{
		Page:  page,
		Limit: limit,
	})
}

func (c *CustomerClient) GetCustomer(id string) (*pb.Customer, error) {
	ctx := c.withAuth(context.Background())
	return c.client.GetCustomer(ctx, &pb.GetCustomerRequest{Id: id})
}

func (c *CustomerClient) CreateCustomer(name, email, phone string) (*pb.Customer, error) {
	ctx := c.withAuth(context.Background())
	return c.client.CreateCustomer(ctx, &pb.CreateCustomerRequest{
		Name:  name,
		Email: email,
		Phone: phone,
	})
}

func (c *CustomerClient) UpdateCustomer(id, name, email, phone string) (*pb.Customer, error) {
	ctx := c.withAuth(context.Background())
	return c.client.UpdateCustomer(ctx, &pb.UpdateCustomerRequest{
		Id:    id,
		Name:  name,
		Email: email,
		Phone: phone,
	})
}

func (c *CustomerClient) DeleteCustomer(id string) (bool, error) {
	ctx := c.withAuth(context.Background())
	resp, err := c.client.DeleteCustomer(ctx, &pb.DeleteCustomerRequest{Id: id})
	if err != nil {
		return false, err
	}
	return resp.Success, nil
}

func main() {
	client, err := NewCustomerClient("localhost:50051")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	// 测试创建客户
	customer, err := client.CreateCustomer("John Doe", "john@example.com", "123-456-7890")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Created customer: %+v\n", customer)

	// 测试获取客户列表
	listResp, err := client.ListCustomers(1, 10)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Customers: %+v\n", listResp.Customers)
}

Web界面 (web.go)

package main

import (
	"html/template"
	"log"
	"net/http"
	"strconv"

	"github.com/gorilla/mux"
)

type WebServer struct {
	client *CustomerClient
}

func NewWebServer(client *CustomerClient) *WebServer {
	return &WebServer{client: client}
}

func (ws *WebServer) listCustomersHandler(w http.ResponseWriter, r *http.Request) {
	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
	if page < 1 {
		page = 1
	}
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	if limit < 1 {
		limit = 10
	}

	resp, err := ws.client.ListCustomers(int32(page), int32(limit))
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	tmpl := template.Must(template.New("customers").Parse(`
	<!DOCTYPE html>
	<html>
	<head>
		<title>Customers</title>
		<style>
			table { border-collapse: collapse; width: 100%; }
			th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
			th { background-color: #f2f2f2; }
		</style>
	</head>
	<body>
		<h1>Customers (Total: {{.Total}})</h1>
		<table>
			<tr>
				<th>ID</th>
				<th>Name</th>
				<th>Email</th>
				<th>Phone</th>
				<th>Actions</th>
			</tr>
			{{range .Customers}}
			<tr>
				<td>{{.Id}}</td>
				<td>{{.Name}}</td>
				<td>{{.Email}}</td>
				<td>{{.Phone}}</td>
				<td>
					<a href="/customer/{{.Id}}">View</a>
					<form action="/customer/{{.Id}}/delete" method="post" style="display:inline;">
						<button type="submit">Delete</button>
					</form>
				</td>
			</tr>
			{{end}}
		</table>
	</body>
	</html>
	`))

	data := struct {
		Customers []*pb.Customer
		Total     int32
	}{
		Customers: resp.Customers,
		Total:     resp.Total,
	}

	tmpl.Execute(w, data)
}

func (ws *WebServer) getCustomerHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	customer, err := ws.client.GetCustomer(id)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}

	tmpl := template.Must(template.New("customer").Parse(`
	<!DOCTYPE html>
	<html>
	<head>
		<title>Customer {{.Name}}</title>
	</head>
	<body>
		<h1>{{.Name}}</h1>
		<p><strong>ID:</strong> {{.Id}}</p>
		<p><strong>Email:</strong> {{.Email}}</p>
		<p><strong>Phone:</strong> {{.Phone}}</p>
		<a href="/">Back to list</a>
	</body>
	</html>
	`))

	tmpl.Execute(w, customer)
}

func main() {
	client, err := NewCustomerClient("localhost:50051")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ws := NewWebServer(client)

	r := mux.NewRouter()
	r.HandleFunc("/", ws.listCustomersHandler)
	r.HandleFunc("/customer/{id}", ws.getCustomerHandler)

	log.Println("Web server started on :8080")
	log.Fatal(http.ListenAndServe(":8080", r))
}

4. TLS证书生成

创建证书目录并生成自签名证书:

mkdir -p cert
openssl genrsa -out cert/server-key.pem 2048
openssl req -new -x
回到顶部