# days7 **Repository Path**: pardon110/days7 ## Basic Information - **Project Name**: days7 - **Description**: 手搓分布式缓存,RPC, ORM, Web框架,涉及一致性哈希,消息编码,反射操作,中间件等相关技术 - **Primary Language**: Go - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2024-03-18 - **Last Updated**: 2024-04-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Days7 web框架,分布式缓存LRU,一致性哈希,分布式节点,防止缓存击穿, 性能分析,go 程序设计 ## Web - base 1. 实现http.Handler 负责匹配逻辑 2. 维护一个私有的map, 该map负责pattern 到 HandlerFunc的映射 3. 提供一个私有路由注册方法 addRoute - context - engine - 入口 - 负责 http verb 方法抽象注册,创建并持有路由实例 - 监听原始的http服务 启动 - 通过 ServerHTTP 获取原始的r,w对象 创建上下文,并负责对自定义的HandlerFunc 调用 - router - 负责路由注册,匹配逻辑等具体事务 - context - 每次请求重新创建一次上下文 http.Handler - 聚合原始的请求和响应对象,并提供一些相关的字段及快捷方法 - 思路 ```mermaid graph A[start] --> B[创建Engine实例] B --> F[创建router] B --> C[添加GET/POST路由] B --> E[启动HTTP服务器] B --> D[ServeHTTP] D --> L(创建Context) L --> M(调用路由处理) ``` - router 内部持有不对外开放 - context 多实例 - trie router - 解决路由映射 1. 动态参数 :name 2. 静态资源路由 *filepath - 获取路径参数 - 注册parttern, 基于pattern的映射handler构建 - 访问path, 基于path的分割,node.pattern比对 - 访问流程 r.path -> node.pattern -> handler ```mermaid graph A[engine.ServeHTTP] --1.context compose--> B[router.handle] B --2 r.Path & r.Method --> F[router.getRoute] F --3 Tire node & params-->B B --> C{4. node exists ?} C -- yes(n.pattern - key) --> D[call processor] C -- no --> E[out notFound] ``` - 构建不同 http verb 的路由前缀树 - group - engine - 增加嵌套类型 *RouterGroup []**RouterGroup - 同层支持横向多个分组 - RouterGroup - 支持纵向向上访问的能力 - 向上承接engine的路由注册,向下给路由实例派发 - 所有分组实例共享同一 engine 实例 - 核心: - 增加一个路由注册代理转发 Group实例对象 - middleware - 全局中间件,分组中间作 - 中间件实现前后置操作,如 part1 -> part3 -> Handler -> part 4 -> part2 - html/template - 实现静态资源服务(Static Resource) Server-Side Rendering - 支持HTML模板渲染,模板创建,加载,模板解析 - 静态服务器映射 原生库已实现 http.FileServer 返回http.Handler - recover - defer -> recover ## 语言特性 - 标准库没有提供直接支持解析application/json类型的POST请求体的方法,需要手动处理请求体中的数据 - 引用 vs 指针 - 显式与隐式 - 指针是显式的,你需要使用&和*操作符来创建和解引用 - 引用是隐式的,切片、映射和通道作为引用类型,它们的引用行为是自动处理 - 内存管理 - 指针允许直接操作内存,而引用类型由Go语言的运行时管理 - 可变性 - 通过指针,可以修改它的值,而引用类型不能直接修改引用本身 - 类型安全 - 指针可以是任何类型的地址,引用类型通常是Go语言的几种内置类型 - 使用场景 - 指针通常用于大型数据结构场景,避免复制 - 引用类型则用于共享可变数据,如数据共享切片或映射 - 切片(slice)中存储的是指向底层数组的指针、切片的长度和容量。切片本身并不存储实际的元素值。 - Testing - func reflect.DeepEqual(x any, y any) bool - 深度递归比较中所有对应的字段或元素是否都相等 - 只会比较接口值时会比较接口的动态值和动态类型,不会比较接口的静态类型 - 适用对于比较结构体、数组、切片、映射、通道、函数 - go test -count=1 命令来禁用缓存测试 - 接口类型变量 - 当我们定义一个接口类型时,实际上是在定义一个指向动态类型和动态值的指针。 - 因此,**不需要显式地将接口类型声明为指针类型**。 - 接口类型在Go中是一种引用类型,所以它本身就是一个指针。 - 指针变量比较规则 - 不同类型的指针变量无法直接比较 - 若两个同类型的指针变量指向的是同一个地址,则它们相等 - 两个同类型的指针变量都是nil, 则它们可比较相等 - **接口型函数** - 函数类型实现某一个接口,称之为接口型函数 - 方便调用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数!!! - 嵌套结构体,如果内嵌类型是指针类型,需要对该指针进行初始化,否则它会被初始化为nil - 在Go语言中,函数名并不属于函数签名的一部分 - defer 语句中的参数 - **defer语句执行时在 deferred 函数调用之前进行** - 该deferred函数参数在 defer 语句执行时会被立即求值,并且结果会被保存起来 - 普通函数的参数是在调用时求值, defer语句中的deferred函数参数在defer语句执行时就会被求值 - defer f.func(a).func(b) - func(a) 顺序执行, func(b)则延迟调用, 其中参数 b 值在defer语句执行时已求值固定 - defer 语句执行时,会将需要延迟调用的函数和参数保存起来 - defer 延迟调用时,需要保存函数指针和参数,链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行 - import –> const –> var –> init() –> main() - 一个T类型的值可以调用为*T类型声明的方法,但是仅当此T的值是可寻址(addressable) 的情况下 # 分布式缓存 ## LRU 缓存 >>> 商业世界现金为王;架构眼里缓存为王 - 问题 - 内存不够 优先删除最近最少访问的数据 - 并发写入冲突 并发场景修改操作需要加锁 - 单机性能不够 分布式水平扩展,增加单个节点的垂直扩展 - 缓存淘汰策略 - FIFO (First In First Out) 先进先出 - 缺点:对于常访问数据缓存途中率低 - LFU (Least Frequently Used) - 最少使用,即淘汰缓存中访问频率最低的记录 - 缺点:对内存消耗高,受历史访问影响大 - LFU 的实现需要维护一个按照访问次数排序的队列, - 每次访问,访问次数加1,队列重新排序,淘汰时选择访问次数最少的即可 - LRU(Least Recently Used) - 思想 - 核心是基于最近访问的时间来淘汰最长时间未被访问的数据 - 当缓存空间不足时,会优先淘汰最近最少被使用的数据。 - 实现 1. 用一个数据结构(如双向链表和哈希表结合)来维护缓存中数据的访问顺序 2. 当一个数据被访问时,会将其移动到数据结构的头部(表示最近访问过) 3. 而当需要淘汰数据时,会选择数据结构的尾部的数据(表示最近最少访问)进行淘汰 - 理由 - 哈希表存储数据的键值对 - 哈希表的键为数据的键,**值为指向对应链表节点的指针** - 双向链表来维护数据的访问顺序 - 链表头部表示最近访问的数据,链表尾部表示最久未访问的数据 - 核心数据结构图 ![核心数据结构 图片描述](./cache/lru/lru.jpg) ## 单机并发缓存 - sync.Mutex 互斥锁的使用,并实现 LRU 缓存的并发控制 - 核心结构 - ByteView 缓存值的抽象与封装,存字节值 - cache 实例化 lru.cache,封装 get 和 add 方法,并添加互斥锁 mu - group 负责与用户交互,控制缓存值存储和获取的主流程 - 回调设计 Getter - 当缓存不存在时,调用这个函数,得到源数据 - 缓存不存在,应从数据源(文件,数据库等)获取数据并添加到缓存中 ## HTTP服务端 - 分布式缓存需要实现节点通信,基于HTTP通信 - HTTPPool 结构 - self 一个记录自己的地址 - basePath 作为节点通讯地址的前缀 - `///` 约定为访问路径 - 通过 groupname 得到 group 实例,再使用 group.Get(key) 获取缓存数据 ## 一致性哈希 - 分布式缓存问题 1. 多节点访问谁? 2. 节点数量发生了变化,产生缓存雪崩 >>> 缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。常因为缓存服务器宕机,或缓存设置了相同的过期时间引起 - 哈希函数 - 特性: - 对于同样的输入,总得到同样的输出 - 输入相同它的哈希值也必定相同,称之为哈希函数的确定性或一致性 - 哈希碰撞 Hash Collision - 不同的数据可能产生相同的输出,其原因哈希值有限,输入无穷 - 一致哈希性算法 - 思想 1. 将数据和节点通过哈希函数映射到一个固定大小的哈希空间中, 2. 然后按顺时针方向(*哈希值有序环*)将数据映射到哈希空间的位置 3. 算法的核心思想是将哈希空间组织成一个虚拟的圆环,并将数据和节点都映射到这个环上,从而实现节点和数据的动态负载均衡 - 将整个哈希空间组织成一个虚拟的圆环,这个圆环上的每一个点都代表一个哈希值 - 工作 1. 通常使用哈希函数将节点和数据映射为一个32位或64位的哈希值 2. 将哈希空间视为一个环状空间,即哈希空间的两端连接在一起,形成一个环 3. 当需要确定数据存储位置时,使用相同的哈希函数将数据映射为一个哈希值,并沿着环状空间顺时针方向寻找最近的节点位置 4. 数据存储在找到的节点上, 并且当节点发生变化时,只有少量数据需要重新映射,而大部分数据仍然映射到原来的节点上 - 数据在系统中均匀分布和动态调整的过程 - 一致性哈希算法会找到离数据最近的节点位置 - 当系统中的节点发生变化(比如节点加入或离开系统)时,哈希空间上的节点位置也会相应调整 - 优点 - 当节点加入或离开系统时,只有少量数据需要重新映射,大部分数据仍然可以在原来的位置上找到,从而减少了数据的迁移量 - 使用场景 - 主要用于解决分布式缓存问题,特别是在分布式系统中节点(服务器)的动态加入和移除对数据分布的影响问题 1. 每个节点(服务器)通过哈希函数映射到环上的一个位置 2. 当需要存储或查找数据时,通过哈希函数计算数据的哈希值,并将其映射到环上的一个位置。 3. 算法会顺时针查找**第一个大于等于该数据哈希值位置的节点**,并将数据存储在该节点上 - 使用虚拟节点扩充节点的数量,解决节点较少的情况下数据容易倾斜的问题 ## 分布式节点 - peer节点 - 通常指的是对等节点,即相互连接并具有相同或类似功能的节点之间的关系 - peer节点通常指除了节点自身之外的对等其他节点,它们之间相互连接并共享资源或信息 - 实现 1. 注册节点(Register Peers), 借助一致性哈希算法选择节点 2. 实现HTTP客户端,与远程节点的服务通信。 - 流程图 ```mermaid graph TD; A[接收 key] --> B{检查是否被缓存}; B -- 是 --> C[返回缓存值 ⑴]; B -- 否 --> D{是否存在Peer对等级节点}; D -- 是 --> K[使用一致性哈希选择节点] K -- 是 --> I{是否是远程节点}; I -- 是 --> J[HTTP 客户端访问远程节点]; J --> E{成功?}; E -- 是 --> F[服务端返回返回值]; D -- 否 --> G[回退到本地节点处理] I -- 否 --> G[回退到本地节点处理]; G -- 调用`回调函数`,获取值并添加到缓存 --> H[返回缓存值 ⑶]; ## 防止缓存击穿 - 概念 - 缓存雪崩 - 缓存在同一时刻全部失效,造成瞬时DB请求量大,压力骤增,引起雪崩 - 产生原因:缓存服务器宕机,缓存的key设置了相同的过期时间等引起 - 缓存击穿 - 一个存在的key,在缓存过期的一刻,同时有大量的请求 - 这些请求都会击穿到DB,造成瞬时DB请求量大,压力骤增 - 缓存穿透 - 查询一个不存在的key,因为不存在则不会写到缓存中,所以每次都会请求DB - 如果瞬间流量过大,穿透到DB,导致宕机 - 延迟初始化 - 性能优化 - 资源节约 - 避免不必要的开销 - 解决循环依赖 - 并发协程之间不需要消息传递,非常适合 sync.WaitGroup - 在golang中,对 interface{} 类型变量进行比较时,实际上比较的是存储在接口值中的具体值 - singleflight - 一种用于避免重复执行相同函数的并发控制机制,可以确保在并发环境下,只有一个gorouine执行特定的函数 - 使用场景 - 缓存场景 避免缓存击穿问题 - 网络请求场景 避免重复请求 - 资源加载场景 避免多次加载或初始化相同的资源 - 使用 singleflight - 并发场景下若 缓存已经向其他节点/源获取数据了,那么就加锁阻塞其他相同的请求,等待请求结果,防止其他节点/源压力猛增被击穿 - 作用: 限制了同一时刻只有一个请求在执行,即并发永远为1,后续相同请求必须等待前一请求结束 ## 使用 Protobuf 通信 - protobuf是一个专门用于数据序列化的框架 - python pickle - golang gob - php serialize - 使用 protobuf 进行节点间通信,编码报文,提高效率 1. 在 .proto 文件中定义数据结构,并使用 protoc 生成目标语言代码 2. 在项目代码中引用生成的代码 - 其它 - `cache/zprotobuf` 可独立布署 - 本示例只使用了protobuf 序列化进行 http 通信 - proto.Marshal / proto.Unmarshal - 生成 `protoc --go_out=. *.proto` # RPC ## 服务端与消息编码 - RPC 解决的问题 - 不同应用程序间的通信 1. 采用的传输协议 TCP/HTTP/UnixSocket 2. 确定报文的编码格式 JSON/XML/protobuf 3. 解决可用性问题 - 连接超时 - 异步请求和并发支持 - grpc, rpcx, go-micro, go-zero - 消息序列化与反序列化 - net/rpc 官方标准库 - 使用 encoding/gob 实现golang结构消息的编解码(序列化与反序列化) - 实现一个简易的服务端,仅接受消息,不处理 - RPC 协议设计,通信过程定义 - 服务端通过解析 header 就能够知道如何从 body 中读取需要的信息 - HTTP 报文协商内容 1. 序列化方式 2. 压缩方式 3. header 长度 4. body 长度 - 报文形式 ``` Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod ...} | Body interface{} | | <------ 固定 JSON 编码 ------> | <------- 编码方式由 CodeType 决定 ------->| | Option | Header1 | Body1 | Header2 | Body2 | ... ``` - 定义消息格式(消息头和消息体),序列化和反序列化,通信规则 - serveCodec 1. 读取请求 readRequest 2. 处理请求 handleRequest 3. 回复请求 sendResponse ## RPC 高性能客户端 - Call 设计 - 封装结构体 Call 来承载一次 RPC 调用所需要的信息 - `func (t *T) MethodName(argType T1, replyType *T2) error` - 支持异步和并发 - RPC 客户端 - 实现 创建连接/接收响应/发送请求 - 概念 - 内容协商(Content Negotiation) - 是指在客户端和服务器之间协商传输的数据内容格式的过程 - 内容协商的信息通常是通过HTTP请求和响应的头部(Header)来传递 - Content-Type:服务器通过设置Content-Type响应头来告知客户端返回的响应内容的数据类 - Accept:客户端通过设置Accept请求头来告知服务器它可以接受哪些媒体类型(MIME类型 - defer 运行机制 - 在 return 语句之后,函数退出之前执行 - defer 在函数执行完毕后,但在函数返回之前按照后进先出的顺序执行 - 在Go语言中,函数的执行完毕指的是函数内的*所有语句都已经执行完毕*,包括defer语句 ``` func test() (ans int) { defer func() { fmt.Println(ans) // 打印 10 ans-- }() return 10 // 存在一个隐式赋值语句 ans = 10 } test() // 返回 9 ``` ## 服务注册 - 实现 - 通过反射实现服务注册功能 - 在服务端实现服务调用 - 将结构体的方法映射为服务条件 - `func (t *T) MethodName(argType T1, replyType *T2) error` 1. 方法所属类型是导出的 2. 方式是导出的 3. 两个入参,均为导出或内置类型 4. 第二个入参必须是一个指针 5. 返回值为 Error 类型 - 通过反射实现结构体与服务的映射关系 - reflec.Elem() 只能解开一层指针,获取指针指向的元素的值 - reflect.Indirect 在解开指针时没有明确的层次限制,它会一直(递归)解开直到遇到非指针类型的值或nil 指针 - field Func reflect.Value 通过反射值来调用方法 - func with receiver as first argument - method.Type.NumIn() 反射时获取的方法参数列表包含了接收者(receiver)作为第一个参数 - 执行流程 1. 定义结构体和方法 2. 注册带方法的结构体到Server中,并启动RPC服务 3. 构造参数,发送RPC请求,并打印结果 ## 超时处理 - 必要性 - RPC框架缺少超时处理机制,会容易因为网络或其他错误导致挂死,耗尽资源 - 客户端处理超时地方 - 与服务端建立连接 - 发送请求到服务端,写报文导致的超时 - 等待服务端处理导致的超时,比如服务端挂死,迟迟不响应 - 从服务端接收响应时,读报文导致的超时 - 服务端处理超时场景 - 读取客户端请求报文 - 发送响应报文时,写报文时 - 调用映射服务的方法时,处理报文导致的超时 - 实现 - 创建连接超时 设定置于 Option - Client.Call 超时处理机制,使用context包,控制权交给用户 - 服务端处理超时 time.After() 结合 select + chan - go语言函数与基本类型别名区别 - go 函数类型别名并不会创建新类型 - `type MyFunc func(int) int` 符合函数签名的函数与 MyFunc 函数完全等价 - 基于基本类型的别名实际上是在定义新类型 - `type MyInt int` 类型别名 MyInt 会在编译时被视为不同的类型 - 测试 - 连接超时 - 处理超时 - *testing.T / t.Parallel / t.Run - t.Parallel()方法 标记测试函数可以并行执行,测试框架会据此并行执行启动的goroutine - 建立连接 -> - goroutine 自动关闭或结束场景 - 函数执行完毕 - 主goroutine结束 - 通道关闭 - context.Context 取消, 与之相关的goroutine也会自动关闭 - select 语句中的通道关闭 ## 支持HTTP协议 - 协议兼容性 connect 请求 - RPC 消息格式与标准的 HTTP 协议并不兼容 - HTTP 协议的 CONNECT 方法 - 用于建立与目标服务器的隧道连接(tunnel connection) - 隧道连接 1. 在两个节点之间建立直接通信通道的方法 2. 透明传输:在隧道连接中中间节点(如代理服务器)只是起到传输数据的作用 3. 加密安全性 4. 端到端连接 中间节点只是起到传输数据的中转作用 - 主要用于代理服务器与目标服务器之间建立加密通道,通常用于HTTPS请求 1. 代理服务器在收到CONNECT请求后, 2. 会建立与目标服务器的TCP连接,并返回HTTP 200状态码表示连接建立成功 3. 代理服务器只需透传客户端和目标服务器之间的加密数据包,无需解析HTTPS报文 - CONNECT 请求中的连接信息通常是包含在请求头中 - 实现逻辑 1. 浏览器通过代理服务器与网站建立安全连接, 2. 代理服务器转换HTTP协议为HTTPS协议。 3. HTTPS握手和加密数据交换在浏览器和网站间进行,代理服务器仅传输数据包。 4. RPC服务端需将HTTP转为RPC协议,客户端需新增通过HTTP CONNECT请求创建连接的逻辑。 - http..Hijack()方法 - http 连接 -----> tcp 连接 - 允许用户接管HTTP连接,绕过库的默认行为,直接与底层的TCP连接进行交互。 - 当调用.Hijack()方法时,HTTP库将不再处理连接的读写操作,而是将底层的TCP连接交给用户自己处理 - 用户可以直接读取和写入TCP连接的数据,实现自定义的协议逻辑,如处理WebSocket连接、自定义实现等 ```mermaid sequenceDiagram Browser->>Proxy: CONNECT www.example.com:443 HTTP/1.0 Proxy-->>Browser: HTTP/1.0 200 Connection Established - 支持 HTTP协议 1. 接收客户端发送的 CNNECT 请求 - 识别 r.Method == "CONNECT" 2. 返回 HTTP 200 状态码表示建立连接 ## 负载均衡 - 常见策略 - 随机 - 轮询 Round Robin - 加权轮询 Weight Round Robin - 哈希/一致性哈希 - 最少连接数 - 服务发现 - 负载均衡的前提是有多个服务实例 - SelectMode 负载均衡策略 - iota - Disocvery 服务所有需要的最基本接口 - Broadcast 广播 - 将请求广播到所有的服务实例, - 若任意一个实例发生错误,则返回其中一个错误;如果调用成功,则返回其中一个的结果 1. 为了提升性能,请求是并发 2. 并发情况下需要使用互斥锁保证 error 和 reply 能被正确赋值 3. 借助 context.WithCancel 确保有错误发生时,快速失败 ## 服务发现与注册中心 客户端和服务端都只需要感知注册中心的存在,而无需感知对方的存在 - 应具备的基本功能 1. 服务注册与注销 2. 服务发现与查询 3. 负载均衡 4. 动态更新 - 实现 - 注册中心支持服务注册,接收心跳等功能 - 客户端实现基于注册中心的服务发现机制,定期向注册中心发送心跳保活 - 服务发现 服务消费者能够动态地查找和定位到所需的服务提供者 - 注册中心通常被动接收服务实例发送过来的心跳消息,然后根据这些消息来进行相应的管理和调度 # ORM 使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库 - reflect.ValueOf() vs reflect.indirect () - ValueOf(i any) reflect.Value - ValueOf returns a new Value initialized to the concrete value stored in the interface i. - ValueOf(nil) returns the zero Value. - Indirect(v reflect.Value) reflect.Value - Indirect returns the value that v points to. - If v is a nil pointer, Indirect returns a zero Value. - If v is not a pointer, Indirect returns v. - 反射机制(reflect): - Go 语言常用ORM框架 - gorm 实现了关联关系(一对一,一对多等),回调插件等 - xorm 实现了读写分离(支持配置多个数据库),数据同步,导入导出等 ## database/sql - sqlite 轻量级的,遵守 ACID 事务原则的关系型数据库 - .help - sqlite3 dbname - .head on - .table - .schema tablename - database/sql 用于和数据库的交互 - log 库 - 日志实例分级 - 核心结构 - session 实现与数据库的交互 - raw 直接调用与原生SQL交互 - engine 与用户交互的入口 - 测试 - *testing.M 执行一些初始化和清理操作,以及控制测试的运行 - TestMain 函数名称必须 m.Run()来运行测试函数 ``` func TestMain(m *testing.M) { TestDB, _ = sql.Open("sqlite3", "../orm.db") code := m.Run() _ = TestDB.Close() os.Exit(code) } ``` ## 对象表结构映射 1. Dialect 抽象数据库差异 - Go 语言基本类型 -> 数据库类型 - 不同数据库在SQL语句上的表达差异 2. Schema 对象和表转换 - table name -> struct name - 字段名和字段类型 -> 成员变量和类型 - 额外的约束条件(如非空,主键等) -> 成员变量的Tag - Go 语言通过Tag实现,Java,Python 等语言通过注解实现 - 利用 RefTable() 返回的数据库表和字段的信息,拼接出 SQL 语句,调用原生 SQL 接口执行 - 设计 Schema 利用反射(reflect)完成结构体和数据库表结构的映射 3. Session 与数据库交互 - 数据库表的增/删操作 - 成员变量新增 dialect 和 refTable 5. Engine 增加对dialect依赖 6. Clause 子句生成器 - 使用 iota 合理控制子句顺序 - clause 拼接各个独立子句 - %v 会根据变量的类型自动选择合适的格式进行输出 - 局部生成 sql,sqlvar -> 构建拼接生成更大的sql,sqlvar -> 底层数据库接口参数 7. Hook 机制 - 提前在可能增加功能的地方埋好(预设)一个钩子 - 通过反射(reflect)获取结构体绑定的钩子(hooks),并调用 - 钩子机制设计的好坏,取决于扩展点选择的是否合适,比如 curd 前后 - 适用场景 - 一个隐私字段 Password的值脱敏 - 原理 1. 检查实例反射值上的钩子方法是否存在实现 2. 优先使用实例钩子 ## 支持事务 - 事务的 ACID - 原子性 Atomicity 不可分割,全执行或反 - 一致性 Consistency 在事务执行前后必须满足预设的约束,规则 - 隔离性 Isolation 多个事务并发时,互不干扰 - 持久性 Durability 事务被提交,修改保存不会丢失,即使数据库出现故障 - SQLite 和 Go 标准库中的事务 - sqlite 1. BEGIN 开启事务 2. 执行操作 3. COMMIT / ROLLBACK 结束事务 - Go 事务 1. db.Begin() -> *sql.Tx 得到事务对象 2. tx.Exec() 执行操作 3. tx.Commit() / tx.Rollback() 提交/回滚 - 事务接口通常是兼容基础接口的,它会扩展基础接口的功能,同时保留基础接口的方法和属性 - 实现 - Session 新增成员 tx *sql.Tx - 使用数据库基础接口集作为通用返回db容器句柄,兼容非事务接口,有则用 ## 数据迁移 - SQL 语句 Migrate - 字段新增 - `ALTER TABLE table_name ADD COLUMN col_name, col_type;` - [删除字段](https://stackoverflow.com/questions/8442147/how-to-delete-or-add-column-in-sqlite) ``` CREATE TABLE new_table AS SELECT col1, col2, ... from old_table DROP TABLE old_table ALTER TABLE new_table RENAME TO old_table; ``` 1. 从 old_table 中挑选需要保留的字段到 new_table 中 2. 删除 old_table 3. 重命名 new_table 为 old_table - 实现 1. difference 用来计算前后两个字段切片的差集 2. 使用 ALTER 语句在旧表新增字段 3. 使用创建新表并重命名的方式删除字段 # 性能分析 - pprof - 编译到程序中的 `runtime/pprof` 包 数据采集 - 性能剖析工具 `go tool pprof` 数据分析 - CPU 性能分析 1. 生成 profile ``` func main() { f, _ := os.OpenFile("cpu.pprof", os.O_CREATE|os.O_RDWR, 0644) defer f.Close() pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() ... ``` - `go run main.go` 2. 分析数据 - `go tool pprof -http=:9999 cpu.pprof` 网页查看 - `go tool pprof cpu.pprof` 命令行交互模式查看 3. 安装绘图 [graphviz](https://graphviz.org/download/) - 内存性能分 - pkg/profile 采集性能数据,封装了runtime/pprof 的接口 - pprof 支持多种输出格式(图片、文本、Web等)图表化分析 ``` defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop() concat(100) ``` - benchmark 生成性能数据采集 profile - `go test -cpuprofile cpu.pprof -benchmem -run=^$ -bench ^BenchmarkFib$ days7.test/tests ` - 性能监控 - 采样方式 - 测试采集:go test -memprofile mem.out - 在线采集: - import _ "net/http/pprof" 向目标程序注入 - go tool pprof http://localhost:6060/debug/pprof/heap - 手工:runtime/pprof - 查看采样结果 - go tool pprof -top mem.out 命令行参数 - go tool pprof -http 0.0.0.0:8080 mem.out # Go 程序设计 ## 类型 - 引用类型特指 slice, map, channel 三种预定义类型 - new 按类型大小分配零值内存,返回指针 - 只关心类型长度 sizeof, 不涉及内部结构和逻辑 - 行为类似 malloc, 但优先在栈上分配 - make 转为类型构造函数或指令,完成内部初始化 - 编译器将 make 翻译成 makeSlice, makemap 之类的构造函数调用 - 类型转换 除常量,别名以及未命名类型外,强制要求显式类型转换 ## 函数 - 匿名函数 - 编译器为匿名函数自动生成名字 - 闭包是匿名函数和共引用的外部环境变量组合体 - 闭包会延长环境变量生命周期 - 闭包实质上是由匿名函数和一到多个环境变量指针构成的结构体 - 延迟调用 - 语句 defer 注册稍执行的函数调用 - 特性 1. 注册非 nil 函数,复制执行所需参数 2. 多个延迟调用按 FILO 次序执行 3. 运行时确保延迟调用总被执行,os.Exit 除外 - 错误处理 - 没有结构化异常,以返回值标记错误状态 ## 数据 - 字符串 - 不可变字节序列 - 遍历 - fori 遍历 byte - forrange 遍历 rune - 单引号字符字面量是 rune 类型,代表 Unicode 字符 - []byte, []rune 可变 - 数组指针 - 数组指针: &array, 指向整个数组的指针 - 元素指针: &array[0], 指向某个元素的指针 - 指针数组:[...]*int{&a,&b}, 元素是指针的数组 - 匿名/嵌入字段 - 隐式以类型名为字段名 - 嵌入类型与指针类型隐式字段名称相同 - 可以像直属字段那样访问可导出嵌入类型成员 - 除 **接口指针** 和 **多级指针** 以外的任何命名类型都可以作为匿名字段 - 指针 - 指针默认值 nil, 支持 ==, != 操作 - 不支持指针运算,但可借助 unsafe 变相实现 - 指针类型 - 普通指针: *T, 包含类型信息 - 通用指针:Pointer, 只有地址,没有类型 - 指针整数:uintptr, 足以存储地址的整数 ## 方法 - 方法有持续性状态,而函数通常没有 - 可为当前包内除接口和指针以外的任何类型定义方法 - 不支持静态方法或关联函数 - 不能以多级指针调用方法 - 方法本质是特殊函数,接收器是第一参数 - 确定接收参数 receiver 类型原则 - 修改实例状态,用 *T - 不修改状态的小对象或值,用 T - 大对象用 *T, 减少复制成本 - 引用类型,字符串,函数等指针包装对象,用 T - 含 Mutex 等同步字段,用 *T 避免因复制造成锁无效 - 无法确定用 *T - 匿名方法 - 同名遮蔽,实现类似 override 操作 - 匿名类型的方法只能访问自己的字段,对外层一无所知 - 方法集 method set - 根据接收参数 receiver 不同视角 - `T.set = T` - `*T.set = T + *T` - `T{E} = T + E` - `T{*E} = T + E + *E` - `*T{E|*E} = T + *T + E + *E` - 注意:不同的包的类型可以定义别名, 但不能定义方法 - 相同包的类型定义别名不受此限 - 方法值 - 方法表达式 将方法还原为普通函数,显式传递接收参数 `receiver` - 方法值 打包了接收参数 `receiver` 和方法 表现出 instance.method 的形式 ## 接口 - 只要目标类型方法集包含接口声明的全部方法,视为实现接口 - 接口内不能有字段 - 只能声明方法,不能实现 - 可嵌入其它接口 - 通常只有一个方法声明的接口,以 er 作为接口名称后缀 - 空接口 interface{}, any 没有任何方法声明 - 匿名接口可直接用于变量定义,或作为结构字段类型 - 接口会复制目标对象,通常以指针替代原始值 - 匿名嵌入其它接口 - 要求: 目标类型方法集中,必须包含嵌入接口在内的全部方法实现 - 相当于导入 include 声明,非继承 - 不能嵌入自身或循环嵌入 - 不允许声明重载 overload - 允许签名相同的声明,并集去重 - 鼓励小接口嵌入组合 - 接口由 itab 和 data 两个字段组成 - 内部使用 itab 结构存储运行期所需的相关类型信息 ## 泛型 基于类型的代码复用,使用实例化时才指定类型参数 - 特性 - 函数和类型(含接口)支持类型参数,方法暂不支持 - 支持推导,可省略类型实参 - 通常以单个大写字母命名类型参数 - 类型参数必须有约束 constraints - 支持使用普通接口类型约束 -> 类型集合 - 类型集合除类型集合外,还可以有方法声明 - 若类型约束不是接口,则无法调用其成员 - 泛型语法 - 使用[] 而非传统的 <> ,支持泛型函数或类型 - 类型集合 - 普通接口:方法集合 method sets 指示能做什么 - 类型约束:**类型集合** type sets 指示谁来做 - 普通接口 vs 类型约束 - 普通接口可以用作约束,但类型约束却不能当普通接口使用 - 竖线: 类型集,匹配其中任一类型即可 - 波浪线:底层类型 underlying type 是该类型的所有类型 ``` type Integer interface { Signed | Unsigned } type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } ``` - go 断言 - x.(type) 类型断言的特殊形式 - 只能用在switch语句中,且只能用于 interface{} 变量 - 判断该变量实际存储值的类型 - x.(int) 常规断言 - 判断接口变量x是否存储了int类型的值,并将其转换为 int ## 并发 - 概念 - 并发 concurrency 逻辑上具备同时处理多个任务的能力 - 并行 parallesim 物理上在同一时刻执行多个并发任务 - 任务 - goroutine 类似多线程和协程的综合体 - 按需扩张的极小初始栈,支持少量任务 - 高效无锁内存分配和复用,提升并发性能 - 调度器平衡任务队列,充分利用多处理器 - 线程自动休眠和唤醒,减少内核开销 - 基于信号实现抢占式任务调度 - 特性 1. 关键字 go 将目标函数和参数 **打包** (非执行)成并发任务单元,放入待运行队列 2. goroutine 参数立即计算并复制 3. 无法确定执行时间,执行次序,以及执行线程,由调度器负责处理 - 等待 - channel 信号通知 - WaitGroup 等待多个任务结束,实现群体性通知 - Context 上下文通知 - Mutex 锁阻塞 - 主动结束 - runtime.Goexit 终止任务 - os.Exit 结束进程 不等待其他任务,不执行延迟调用 - GOMAXPROCS 限制并发任务数 - 调度控制任务执行 - 挂起 安排执行次序 使用close(chan) - 发令 暂停一批任务,直到某个信号发出 如 wg.Wait() - 次序 多个任务按特定次序执行 如延时给调度器处理时间 - 存储 - 任务和线程并非绑定关系,不能使用TLS之类的本地存储 - 通常以参数方式传入外部容器,要避免和其他并发任务竞争 - 通道 - channel 行为类似消息队列,不限收发人数,不可重复消费 - 同步 - 没有数据缓冲区,须收发双方到场直接交换数据 - 阻塞 直到有另一方准备妥当或通道关闭 - 可通过 cap == 0 判断为无缓冲通道 - 异步 - 通道自带固定大小缓冲区,有数据或有空位时,不会阻塞 - 用cap, len 获取区大小和当前缓冲数 - 关闭 closed 或 nil 通道规则 - nil通道是指没有make的变量 - 无论收发 nil 通道都会阻塞 - 不能关闭 nil 通道 - 重复关闭通道,引发 panic! - 向已关闭的通道发送数据,引发panic! - 从已关闭通道接收数据,返回缓冲数据或零值 - 同步 - 通道解决高级别逻辑层次并发架构,锁则保护低级别局部代码安全 - 概念 - 竞态条件:多线程同时读写共享资源 - 临界区: 读写竞态资源的代码片段 - 互斥锁:同一时刻,只有一个线程能进入临界区 - 读写锁:写独占(其它读写均被阻塞),读共享 - 信号量:允许指定数量线程进入临界区 - 自旋锁:失败后,以循环积极尝试(无上下文切换,小粒度) - 悲观锁:操作前独占锁定 - 乐观锁:假定无竞争,后置检查 Lock Free CAS - 标准库锁 - Mutex 互斥锁 - must not be copied, 应避免复制导致锁机制失效 - 控制在最小范围内,及早释放 - 不支持递归 - RWMutex 读写锁 - 某些场合替代互斥锁,可提升性能 - WaitGroup 等待一组任务结束 - Cond 单播或广播唤醒其他任务 - 内部以计数器和队列作为单播和广播依据 - 引入外部锁作为竞态资源保护,可与其它资源同步 - sync.Cond cond.Signal cond.Wait - 消费者在等待生产者通知的过程中,**必须使用循环**来判断条件是否满足 - 因在多个goroutine之间的并发环境下,条件变量的通知可能会出现虚假唤醒(spurious wakeup)的情况 - Once 确保只调用一次 (函数) - 确保仅执行一次,无论后续是同一函数还是不同函数都不行 - Map 并发安全字典 少写多读,数据不重叠 - Pool 对象池 缓存对象可被回收 - 竞争检测 -race 编译 - 有较大性能损失,避免在基准测试或发布版本中使用 - 单元测试有效完整,定期执行竞争检查 ## 包 - 构成 - 包是成员作用域边界,包内成员可以相互访问 - 名称首字母大写为导出成员 exported , 可外部访问 - 特点 - 包名可以与目录名称不同,通常小写单数模式 - 同一目录下源文件必须使用相同的包名 - 为命令行提供包名参数 - 直接提供包名,如标准库 go list math - 以 . 或 .. 开始的相对路径 go build ./mylib - 用 ... 作为通配符,表示任意字符串 go build ./... - 特殊包名 - main 用户可执行文件入口包 - all 所有包, 包括标准库和依赖项 - std 标准库 - cmd 工具链 - 初始化 - 可以定义一到多个 init 初始化函数 - 初始化函数由编译器生成代码自动执行,仅执行一次,不能被其它代码调用 - 导入 - 导入完整的模块路径 module path,非包名 - 编译器依次搜索标准库,项目根目录,及缓存目录 - 引用包成员,用包名package,而非导入路径 - 版本标识 - v(major).(minor).(patch)-(pre|beta) 主要/次要/补丁/预发行或测试版 - go list -m -versions all 查看所有版本 - go list -m -versions github.com/mattn/go-sqlite3 查看指定模块的所有版本 - 工作空间 - go work init 初始化工作空间 - go work use ./test ./mylib 添加模块 - 离线编译 - go mod vendor 将依赖复制到 ./vendor 目录下 - GOWORk=off go build -mod vendor 禁用工作空间,以vendor方式编译