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

