Go 客户端基础
etcd 提供了官方的 Go 客户端库,本章将学习如何使用 Go 连接和操作 etcd。
客户端概述
客户端类型
etcd 提供两种客户端包:
client/v3:gRPC 客户端(推荐)client/v2:HTTP 客户端(已弃用)
安装
bash
go get go.etcd.io/etcd/client/v3@v3.5.17连接客户端
基本连接
go
package main
import (
"context"
"fmt"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
panic(err)
}
defer cli.Close()
fmt.Println("Connected to etcd")
}配置选项
go
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"},
DialTimeout: 5 * time.Second,
DialKeepAliveTime: 10 * time.Second,
DialKeepAliveTimeout: 3 * time.Second,
MaxCallSendMsgSize: 10 * 1024 * 1024, // 10MB
MaxCallRecvMsgSize: 10 * 1024 * 1024, // 10MB
Username: "root",
Password: "password",
})TLS 连接
go
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"https://localhost:2379"},
DialTimeout: 5 * time.Second,
TLS: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
},
})客户端结构
主要组件
go
type Client struct {
KV // KV 操作
Lease // 租约操作
Watch // 监听操作
Cluster // 集群管理
Auth // 认证管理
Maintenance // 维护操作
}使用示例
go
// KV 操作
resp, err := cli.KV.Get(ctx, "/foo")
// 租约操作
leaseResp, err := cli.Lease.Grant(ctx, 60)
// 监听操作
watchChan := cli.Watch(ctx, "/foo")
// 集群管理
memberList, err := cli.Cluster.MemberList(ctx)
// 认证管理
authResp, err := cli.Auth.Enable(ctx)
// 维护操作
statusResp, err := cli.Maintenance.Status(ctx)Context 使用
etcd 客户端的所有操作都接受 context.Context,用于控制超时和取消。
超时控制
go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
resp, err := cli.Get(ctx, "/foo")
cancel()
if err == context.DeadlineExceeded {
fmt.Println("操作超时")
}取消操作
go
ctx, cancel := context.WithCancel(context.Background())
// 开始监听
watchCh := cli.Watch(ctx, "/foo")
// 取消监听
cancel()连接管理
连接池
etcd 客户端内部维护 gRPC 连接池,自动管理连接复用。
┌──────────────────────────────────────────────────────┐
│ Client │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ gRPC Connection Pool │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Conn 1 │ │Conn 2 │ │Conn 3 │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 自动负载均衡到多个 Endpoints │
└──────────────────────────────────────────────────────┘多节点连接
go
// 连接多个节点,客户端自动负载均衡
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{
"node1:2379",
"node2:2379",
"node3:2379",
},
DialTimeout: 5 * time.Second,
})
// 客户端会:
// 1. 随机选择一个节点连接
// 2. 节点失败时自动切换到其他节点
// 3. 定期检查节点健康状态获取实际连接节点
go
// 查询实际连接的节点
statusResp, err := cli.Maintenance.Status(ctx, "node1:2379")
fmt.Printf("Leader: %s\n", statusResp.Leader)重试策略
自动重试
etcd 客户端内置重试机制:
go
// 默认重试策略
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
// 默认重试次数:无限制(针对网络错误)
// 默认重试间隔:基于 gRPC backoff 算法
})自定义重试
go
import "github.com/grpc-ecosystem/go-grpc-middleware/retry"
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
DialOptions: []grpc.DialOption{
grpc.WithUnaryInterceptor(
grpc_retry.UnaryClientInterceptor(
grpc_retry.WithMax(3),
grpc_retry.WithPerRetryTimeout(2*time.Second),
),
),
},
})错误处理
错误类型
go
import (
"errors"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
)
func handleError(err error) {
switch {
case err == context.Canceled:
fmt.Println("请求被取消")
case err == context.DeadlineExceeded:
fmt.Println("请求超时")
case err == rpctypes.ErrKeyNotFound:
fmt.Println("键不存在")
case err == rpctypes.ErrLeaseNotFound:
fmt.Println("租约不存在")
case err == rpctypes.ErrCompacted:
fmt.Println("请求的 revision 已被压缩")
case errors.Is(err, rpctypes.ErrGRPCEmptyKey):
fmt.Println("空键")
case errors.Is(err, rpctypes.ErrGRPCTooManyOps):
fmt.Println("事务操作过多")
default:
fmt.Printf("其他错误: %v\n", err)
}
}错误示例
go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := cli.Get(ctx, "/nonexistent-key")
if err != nil {
if err == rpctypes.ErrKeyNotFound {
fmt.Println("键不存在(正常情况)")
} else {
handleError(err)
}
}线性一致性读
etcd 默认提供线性一致性读,但可以通过配置优化性能。
线性一致性读(默认)
go
// 默认:线性一致性读(强一致性)
resp, err := cli.Get(ctx, "/foo")
// 等价于
resp, err := cli.Get(ctx, "/foo", clientv3.WithSerializable())串行化读(性能优化)
go
// 串行化读:可能读到旧数据,但性能更高
resp, err := cli.Get(ctx, "/foo", clientv3.WithSerializable())适用场景:
- 对一致性要求不高
- 读取频繁,需要优化性能
- 可以接受短暂的数据延迟
实用工具
打印响应
go
import (
"encoding/json"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
)
func printResponse(resp *clientv3.GetResponse) {
data, _ := json.MarshalIndent(resp, "", " ")
fmt.Println(string(data))
}
// 示例输出
{
"header": {
"cluster_id": 123456789,
"member_id": 987654321,
"revision": 100,
"raft_term": 2
},
"kvs": [
{
"key": "/foo",
"create_revision": 1,
"mod_revision": 100,
"version": 10,
"value": "bar"
}
],
"count": 1
}检查连接
go
func checkConnection(cli *clientv3.Client) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := cli.Get(ctx, "health-check-key")
if err != nil && err != rpctypes.ErrKeyNotFound {
return err
}
return nil
}获取集群状态
go
func getClusterStatus(cli *clientv3.Client) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for _, endpoint := range cli.Endpoints() {
resp, err := cli.Maintenance.Status(ctx, endpoint)
if err != nil {
fmt.Printf("%s: %v\n", endpoint, err)
continue
}
fmt.Printf("%s: Leader=%s, DBSize=%d, Version=%s\n",
endpoint,
resp.Leader,
resp.DbSize,
resp.Version,
)
}
}完整示例
基础客户端封装
go
package etcdutil
import (
"context"
"fmt"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
type Client struct {
cli *clientv3.Client
}
func NewClient(endpoints []string) (*Client, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("failed to connect etcd: %w", err)
}
return &Client{cli: cli}, nil
}
func (c *Client) Close() error {
return c.cli.Close()
}
func (c *Client) Put(ctx context.Context, key, value string) error {
_, err := c.cli.Put(ctx, key, value)
return err
}
func (c *Client) Get(ctx context.Context, key string) (string, error) {
resp, err := c.cli.Get(ctx, key)
if err != nil {
return "", err
}
if len(resp.Kvs) == 0 {
return "", fmt.Errorf("key not found")
}
return string(resp.Kvs[0].Value), nil
}
func (c *Client) Delete(ctx context.Context, key string) error {
_, err := c.cli.Delete(ctx, key)
return err
}使用封装
go
func main() {
client, err := etcdutil.NewClient([]string{"localhost:2379"})
if err != nil {
panic(err)
}
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Put
if err := client.Put(ctx, "/config/app", "production"); err != nil {
panic(err)
}
// Get
value, err := client.Get(ctx, "/config/app")
if err != nil {
panic(err)
}
fmt.Printf("Value: %s\n", value)
// Delete
if err := client.Delete(ctx, "/config/app"); err != nil {
panic(err)
}
}最佳实践
连接管理
- 使用 defer 关闭客户端
- 复用客户端连接,避免频繁创建
- 连接多个节点实现高可用
超时控制
- 所有操作都设置合理的超时
- 避免使用无限超时的 context
错误处理
- 区分预期错误(如键不存在)和异常错误
- 网络错误时重试或切换节点
性能优化
- 读多场景使用串行化读
- 批量操作使用事务
- 避免频繁创建租约
资源管理
- 及时关闭 Watch channel
- 及时撤销不需要的租约
- 避免 context 泄漏
总结
本章学习了:
- 客户端连接和配置
- Context 超时控制
- 连接池和负载均衡
- 重试和错误处理
- 线性一致性读
- 实用工具和封装
掌握客户端基础是进行 etcd 开发的前提。在下一章中,我们将深入学习 KV 操作的各种用法。