Go语言的100个错误使用场景(11-20)|项目组织和数据类型

前言

大家好,这里是白泽。 《Go语言的100个错误以及如何避免》 是最近朋友推荐我阅读的书籍,我初步浏览之后,大为惊喜。就像这书中第一章的标题说到的:“Go: Simple to learn but hard to master”,整本书通过分析100个错误使用 Go 语言的场景,带你深入理解 Go 语言。

我的愿景是以这套文章,在保持权威性的基础上,脱离对原文的依赖,对这100个场景进行篇幅合适的中文讲解。所涉内容较多,总计约 8w 字,这是该系列的第二篇文章,对应书中第11-20个错误场景。

🌟 当然,如果您是一位 Go 学习的新手,您可以在我开源的学习仓库中,找到针对 《Go 程序设计语言》 英文书籍的配套笔记,期待您的 star。

公众号【白泽talk】,聊天交流群:622383022,原书电子版可以加群获取。

前文链接:

《Go语言的100个错误使用场景(1-10)|代码和项目组织》

2. Code and project organization

🌟 章节概述:

  • 主流的代码组织方式
  • 高效的抽象:接口和范型
  • 构建项目的最佳实践

2.11 没有使用函数式选项模式(#11)

当设计一个 API 的时候,如何处理可选的配置项输入项是一个问题。下面先展示一些不好的例子。假设场景是创建一个 HTTP Server 服务,需要输入 IP 地址,端口等信息,但同时需要提供默认值,当不传入的时候也可以工作。

反例1(直接包含所有,配置参数):

func NewServer(addr string, port int) (*http.Server, error) {// ...
}

直接在参数列表罗列各种参数,然后在内部依次处理,但是使用时必须传入所有参数。

反例2(Config 存放可选参数):

package httplib
​
type Config struct {// 使用引用类型则未传入int则是nil,否则会和0值混淆Port *int
}func NewServer(addr string, cfg Confg) {
}
--------------------------------------------------
func main() {port := 0config := httplib.Config{Port: &port,}httplib.NewServer("localhost", config)
}

这种方式允许用户将可选的配置参数通过 Config 存放,然后在 NewServer 方法的内部读取 Config 结构的字段去初始化,但是有两个问题:

  1. 随着可选参数的增多,NewServer 内的初始化逻辑将无限扩大。
  2. 如果用户 Config 整个都选择默认参数,则必须传入一个空的 Config{},使得用户需要对 Config 的用法提高理解成本。
httplib.NewServer("localhost", httplib.Config{})

反例3(建造者模式):

package httplib
​
type Config struct {Port int
}type ConfigBuilder sruct {port *int
}func (b *ConfigBuilder) Port(port int) *ConfigBuilder {b.port = &portreturn b
}func (b *ConfigBuilder) Build() (Config, error) {cfg := Config{}if b.port == nil {cfg.Port = defaultHTTPPort} else {if *b.port == 0 {cfg.Port = randomPort()} else if *b.port < 0 {return Config{}, errors.New("port should be positive")} else {cfg.Port = *b.port}}reutrn cfg, nil
}func NewServer(addr string, config Config) (http.Server, error) {// ...
}
----------------------------------------------
// 用法
func main() {builder := httplib.ConfigBuilder{}builder.Port(8080)cfg, err := builder.Build()if err != nil {// ...}server, err := httplib.NewServer("localhost", cfg)if err != nil {// ...}
}

这种写法下仍然有两个问题:

  1. 当可选参数希望使用默认值的时候,需要传递 nil(虽然已经比示例2进步了)。
server, err := httplib.NewServer("localhost", nil)
  1. 使用建造者模式链式创建过程中,只允许返回一个参数。一旦过程中发生错误,为了确保链式调用继续,即使需要错误处理也只能内聚的具体一个个方法内部,并不能将 err 传递出来。从而只能在 Builder 方法内验证可能发生的错误,使得 err 的处理成本大大提高。
cfg, err := builder.Foo("foo").Bar("bar").Build()

🌟 推荐方法(函数式配置选项模式),核心思想如下:

  1. 用一个不对外倒出的结构存放配置:options
  2. 每一个 option 是一个函数返回同一个结构:Option

代码展示:

package httplib
​
type options struct {port *int
}type Option func(options *options) errorfunc WithPort(port int) Option {return func(options *options) error {if port < 0 {return errors.New("port should be positive")}options.port = &portreturn nil}
}

WithPort 接受一个 port 参数代表端口号,返回一个给 options 设置端口号的函数。这种形式的函数本质是一个匿名的闭包,持有外部的 options 配置集合。

func NewServer(addr string, opts ...Option) (*http.Server, error) {// 初始化配置集合 optionsvar options optionsfor _, opt := range opts {err := opt(&options)if err != nil {return nil, err}}// 针对配置字段内容,添加验证需要的逻辑var port intif options.port == nil {port = defaultHTTPPort} else {if *options.port == 0 {port = randomPort()} else {port = *options.port}}
}
---------------------------------------------
// 用法
int main() {server, err := httplib.NewServer("localhost", httplib.WithPort(8080), httplib.WithTimeout(time.second))
}

在 NewServer 内通过循环将 Option 的配置通过函数调用应用到 options 集合中,然后在编写针对 options 配置字段的验证逻辑,因为所有可选的 Option 都是外部传入的,NewServer 内需要为其进行二次校验。

🌟 这种方式也是 Go 的地道用法,在很多开源项目如 gRPC 中都大量使用。

2.12 项目缺乏组织(#12)

Go 语言是一个自由的语言,并不强制要求你选择某一种组织项目的模板,但是你需要为此行动。一个常见的模板展示:

/cmd # 主要的源代码位置,foo应用的入口文件位于 /cmd/foo/main.go
/internal # 内部使用的代码,不希望被导出使用
/pkg # 公共的代码,希望被导出
/test # 额外的外部测试代码和测试数据,Go的单测应该和源代码在同一个package内,但是集成测试等代码需要在这个目录
/configs # 配置文件
/docs # 设计文档和用户手册
/examples # 项目的使用示例代码
/api # API 文件,例如 Swagger,PB等
/web # Web应用拥有的资源文件,如静态文件等
/build # 打包和持续集成文件
/scripts # 各种脚本
/vendor # 当前项目的依赖文件

没有 src/ 目录因为它太泛用了,从而将其细分成了上述的各个目录。但这只是一个参考。

package 的组织方式:

/net/httpclient.go.../smtpauth.go...addrselect.go...

这是 Go 标准库中 net 包的组织结构,虽然 /http 位于 /net 之后,但是 net/http 这个包只能访问 net 包中被导出的内容(大写开头),使用子目录这种组织结构是为了使相关功能的包聚集在一起管理。

🌟 选择根据上下文进行组织项目还是分层组织项目都可以,只要你可以确保项目清晰:

  • 按上下文:将代码分成 customer,constract 等等模块。
  • 六边形架构(DDD):按功能进行分层。

🌟 最佳实践:

  1. 避免为时过早的 package 创建,允许演化,而不是一直遵守一开始的强制规划。
  2. 避免产生大量细粒度的 package,只包含个别文件。当然过大也是一个问题。
  3. 包的命名需要根据它提供的功能出发,用一个小写单词表示。
  4. 最小化包需要导出的内容,减少耦合,不确定就先不导出。
  5. 代码库的编码风格一致性。

2.13 创建公共设施包(#13)

一种常见的不好的实践:创建共享的包如 utils,common & base。

代码展示:

package util
​
func NewStringSet(...string) map[string]struct{} {// ...
}func SortStringSet(map[string]struct{}) []string {// ...
}
-----------------------------------------
// 用法
func main() {set := util.NewStringSet("a", "b", "c")fmt.Println(util.SortStringSet(set))
}

工具包内的两个函数实现了创建 string 集合和针对 key 进行排序输出的函数,但是此处包命名为 util 则没有任何意义,完全可以替换成 common,shared…

代替方案:

package stringset
​
type Set map[string]struct{}
func New(...string) Set {...}
func (s Set) Sort() []string {...}
-----------------------------------------
// 用法
set := stringset.New("a", "b", "c")
fmt.Println(set.Sort())

用 stringset 代替 util 这个包的名称,使其更具表达性。同时将方法的前缀去除,用一个结构 Set 去接收 Sort 方法,将所有逻辑内聚在一个用途明确的 stringset 包中。调用侧使用也收到了明确约束,更加方便。

2.14 忽略包名的冲突(#14)

示例代码:

package redis
​
type Client struct {...}func NewClient() *Client {...}func (c *Client) Get(key string) (string, error) {...}
----------------------------------------------------
// 冲突的场景
func main() {redis := redis.NewClient()v, err := redis.Get("Foo")
}

在这种场景下,虽然 redis 变量现在是可以工作的,但它本质是变量,会被修改。但是 redis 包将无法再在代码中访问。

  • 直观的解决方案:
func main() {redisClient := redis.NewClient()v, err := redisClient.Get("Foo")
}

修改变量名称,避免冲突。

  • 更推荐的解决方案:
import redisapi "mylib/redis"func main() {redis := redis.NewClient()v, err := redis.Get("Foo")
}

通过给 import 的 redis 包起别名的方式,避免与变量名的冲突,这是一种更推荐的做法。

📒 Tips:变量名的创建要避免与内置关键字或者函数同名

2.15 代码文档缺失(#15)

文档对于项目的开发者和使用者都十分重要,这里给出几个法则:

  1. 为每一个导出的对象都配备文档(通过注释)
// Customer is a customer representation
type Customer struct// ID returns the customer identifier
func (c Customer) ID() string {...}
  1. 注释需要是一个完整的句子,以.结尾。并且针对于描述对象的功能,而不是如何实现。确保提供足够的描述信息,使得用户无需阅读代码即可使用。
  2. 针对废弃的 API 使用注释:
// ComputePath returns the fastset path between two points
// Deprecated: This function uses a deprecated way to compute
// the fastest Path. Use ComputeFastestPath instead. func ComputePath () {}
func ComputePath() {}
  1. 为 package 添加文档:
// Package math provides basic constants and mathmatical functions
//
// This package ...
package math

第一行需要简洁,因为在文档中会展现:

image-20240130150051245

2.16 不使用 code-linter(#16)

Linter 是一个自动化代码分析工具,可以帮助我们分析代码,找到潜在的错误。所以 Code Linter 也是持续集成中必不可少的一环。

go vet: Go 内置的静态代码检查工具。

go vet ./...

Linters: 可以是外部工具,如 Golint、GolangCI-Lint、Staticcheck 等,它们通过外部安装,并提供更多的规则和功能。

通常情况下,建议同时使用 go vet 和 linters 以确保代码的质量和一致性。

示例代码:

func main() {unusedVariable := 42 // 未使用的变量
}

运行 go vet ./...

baize@baizedeMacBook-Air mistakes % go vet ./...
# mistake
vet: ./main.go:4:2: unusedVariable declared and not used

3. Data types

🌟 章节概述:

  • 基本类型涉及的常见错误
  • 掌握 slice 和 map 的基本概念,避免使用时产生 bug
  • 值的比较

3.1 八进制产生的混乱局面(#17)

在 Go 当中,以0字面量开头的数值表示8进制,因此:

sum := 100 + 010 // 结果为108

但是8进制也有其发挥作用的场景,如赋予文件对应 Linux 系统的权限:

file, err := os.OpenFile("foo", os.O_RDONLY, 0644)
// 0644可以替换成0o644或者0O644

Go 语言中的不同进制表示:

  • 二进制:使用 0b 或者 0B 为前缀
  • 十六进制:使用0x 或者 0X 为前缀
  • 虚数:使用 i 为后缀

Go 语言中支持用下划线作为数值的分隔符,提高可读性:

1_000_000_000 // 一百万
0b00_00_01 // 二进制也是可以的

3.2 忽略整型溢出(#18)

常见的整型类型:

image-20240131112800057

整型溢出场景:

var counter int32 = math.MaxInt32
counter++
fmt.Printf("counter=%d\n", counter)

整个程序编译和运行不会报错,但是:

counter = -2147483648 // counter++ 导致int32溢出

计算机存储有符号整数用二进制表示:

image-20240131114037048

针对有符号整数,第一位为符号位,0表示正数,1表示负数,全0表示0(约定)。

比如上述 int32 有符号整型最大值+1(左侧图片),得到右侧图片(是一个负数)。

计算时使用补码:正数的补码等于原码,负数的补码等于原码除去符号位外,所有位数取反,最后整体+1。

举例 int8 计算 8 + (-8) = 0 用补码的表示:

00001000 + 10001000 // 原码
00001000 + 11111000 = 100000000 // 补码,左侧0溢出,因为只有8位,得到0

如果希望手动检测整型溢出,这是一些模板代码:

func Inc32(counter int32) int32 {if counter == math.MaxInt32 {panic("int32 overflow")}return counter+1
}func addInt(a, b int) int {if a > math.MaxInt-b {panic("int overflow")}return a + b
}func MultiplInt(a, b int) int {if a == 0 || b == 0 {return 0}result := a * bif a == 1 || b == 1 {return result}if a == math.MinInt || b == math.MinInt {panic("integer overflow")}if result/b != a {panic("integer overflow")}return result
}

3.3 不理解浮点数(#19)

浮点数在计算机中的存储通常遵循 IEEE 754 标准,该标准定义了单精度和双精度浮点数的表示方式。

  1. 单精度浮点数(32位):

    • 符号位:1位
    • 指数位:8位
    • 尾数位:23位

    单精度浮点数的存储结构如下:

    SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
    
    • S:符号位,表示正负。
    • E:指数位,以偏移值(127)存储,范围为-126到+127。
    • M:尾数位,23位精度。

    具体数值表示为:(-1)^S * 1.M * 2^(E-127)

  2. 双精度浮点数(64位):

    • 符号位:1位
    • 指数位:11位
    • 尾数位:52位

    双精度浮点数的存储结构如下:

    SEEEEEEE EEEEMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM
    
    • S:符号位,表示正负。
    • E:指数位,以偏移值(1023)存储,范围为-1022到+1023。
    • M:尾数位,52位精度。

    具体数值表示为:(-1)^S * 1.M * 2^(E-1023)

需要注意的是,由于二进制浮点数的特性,某些十进制小数可能无法精确表示,会有舍入误差。在编程中,特别是涉及金融等需要高精度的领域,需要小心处理浮点数的精度问题。

🌟 举例 1.0001 在单精度和双精度下的存储:

  1. 单精度(32位):

    0 01111111 00010010000111101011100
    
    • 符号位:0(正数)
    • 指数位:01111111(127的二进制表示,表示偏移值为0)
    • 尾数位:00010010000111101011100(23位精度)

    具体数值表示为:(-1)^0 * 1.00010010000111101011100 * 2^(0-127) ≈ 1.0001

  2. 双精度(64位):

    0 01111111111 0001001000011110101110000101000111101011100010100011111
    
    • 符号位:0(正数)
    • 指数位:01111111111(1023的二进制表示,表示偏移值为0)
    • 尾数位:0001001000011110101110000101000111101011100010100011111(52位精度)

    具体数值表示为:(-1)^0 * 1.0001001000011110101110000101000111101011100010100011111 * 2^(0-1023) ≈ 1.0001

🌟 浮点数的使用准则:

  1. 比较大小需要在合理精度范围内
  2. 运算时注意相似精度优先运算,减少精度波动

3.4 不理解切片长度和容量(#20)

Go 的切片本质上是一个结构体,包含一个指向数组的指针,以及两个变量记录长度和容量。

🌟 切片创建于扩容场景分析:

s := make([]int, 3, 6) // 创建长度为3容量为6的int类型的切片
s[1] = 1 // 赋值s[1]=1

image-20240131123853071

访问切片大于长度范围的位置将触发 panic:

panic: runtime error: index out of range [4] with length 3

在 len 小于 cap 的时候,可以直接向切片添加元素:

s = append(s, 2)

image-20240131124253144

此时切片的 len 自动增加到4,因为容量足够,不会触发切片的扩容,但如果添加的元素数量超过 cap 的限制,则会触发切片的扩容:

s = append(s, 3, 4, 5)
fmt.Println(s) // 得到:[0 1 0 2 3 4 5]

image-20240131124546956

当添加5的时候,原底层数组的容量达到上限6,触发2倍扩容(创建新数组),拷贝原切片内容到新数组,并追加5。

大致的 Go 切片扩容规则:容量1024以下双倍扩容,以上扩容25%

原底层数组因为丢失了引用,如果在堆内存内,会被后续的 Go GC 回收(GC 相关场景将在全书后期讲解)。

🌟 切片截取场景分析:

s1 := make([]int, 3, 6) // 长度为3,容量为6
s2 := s1[1:3] // 从s1的索引1-3截取,左闭右开,此时s2的长度为2,容量是5

image-20240131125818202

此时如果更新 s1[1] 或者 s2[0] 为1,则由于共享底层数组的原因,导致另一方可以读取到变更内容。

如果追加内容,则底层共享的数组追加2。此时 s2 的 len 变为3,但是 s1 的 len 依旧为3。

image-20240131130436982

并且此时打印两个切片得到的内容如下:

s1 = [0 1 0], s2 = [1 0 2]

如果继续向 s2 追加元素直到 s2 的长度超过容量,则会触发扩容,创建新底层数组(二倍):

s2 = append(s2, 3)
s2 = append(s2, 4)
s2 = append(s2, 5)

image-20240131130649349

小结

已完成全书学习进度20/100,再接再厉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/661238.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DevSecOps 参考模型介绍

目录 一、参考模型概述 1.1 概述 二、参考模型分类 2.1 DevOps 组织型模型 2.1.1 DevOps 关键特性 2.1.1.1 模型特性图 2.1.1.2 特性讲解 2.1.1.2.1 自动化 2.1.1.2.2 多边协作 2.1.1.2.3 持续集成 2.1.1.2.4 配置管理 2.1.2 DevOps 生命周期 2.1.2.1 研发过程划分…

leetcode刷题(剑指offer)54.螺旋矩阵

54.螺旋矩阵 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例 2&#xff1a; 输入&#xff1a;ma…

Java基础-集合框架

集合框架&#xff1a; 内存层面可考虑的数据存储容器&#xff1a;数组&#xff0c;集合 数组的特点&#xff1a;长度&#xff0c;存储元素类型确定&#xff0c;既可以放基本数据类型&#xff0c;也可以放引用数据类型 缺点&#xff1a;长度不可变&#xff0c;存储元素特点单…

从零开始 Linux(一):基础介绍与常用指令总结

从零开始 Linux 01. 概念理解 1.1 什么是 Linux&#xff1f; Linux 是一个开源免费的 操作系统&#xff0c;具有很好的稳定性、安全性&#xff0c;且有很强的处理高并发的能力 Linux 的应用场景&#xff1a; 可以在 Linux 下开发项目&#xff0c;比如 JavaEE、大数据、Python…

3D词云图

工具库 tagcanvas.min.js vue3&#xff08;框架其实无所谓&#xff0c;都可以&#xff09; 实现 <script setup> import { onMounted, ref } from vue; import ./tagcanvas.min.js;const updateFlag ref(false);// 词云图初始化 const initWordCloud () > {let …

RabbitMQ快速实战

目录 什么是消息队列&#xff1f; 消息队列的优势 应用解耦 异步提速 削峰填谷 总结 主流MQ产品特点比较 Rabbitmq快速上手 创建用户admin Exchange和Queue Connection和Channel RabbitMQ中的核心概念总结 什么是消息队列&#xff1f; MQ全称Message Queue&#xf…

Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入)

Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入) 反转(转移)控制(IOC Inverse of Control) 控制&#xff1a;对于成员变量赋值的控制权 反转控制&#xff1a;把对于成员变量赋值的控制权&#xff0c;从代码中反转(转移)到Spring⼯⼚和配置⽂件中完成好处&#xff1a;…

七、并发工具(上)

一、自定义线程池 1&#xff09;背景&#xff1a; 在 QPS 量比较高的情况下&#xff0c;我们不可能说所有的访问都创建一个线程执行&#xff0c;这会导致内存占用过高&#xff0c;甚至有可能出现 out of memory另外也要考虑 cpu 核数&#xff0c;如果请求超过了cpu核数&#…

【bitonicSort学习】

bitonicSort学习 什么是Bitonic Sort核心 什么是Bitonic Sort https://zhuanlan.zhihu.com/p/53963918 这个是用来并行排序的一个操作 之前学过一些CPU排序&#xff0c;快排 冒泡 归并啥的&#xff0c;有一些能转成并行&#xff0c;有一些不适合 像快排这种二分策略就可以考虑…

Vue3的自定义指令怎么迁移到nuxt3

一、找到Vue3中指令的源码 const DISTANCE 100; // 距离 const ANIMATIONTIME 500; // 500毫秒 let distance: number | null null,animationtime: number | null null; const map new WeakMap(); const ob new IntersectionObserver((entries) > {for (const entrie…

草图导入3d后模型贴材质的步骤?---模大狮模型网

3D模型在导入草图大师后出现混乱可能有多种原因&#xff0c;以下是一些可能的原因和解决方法&#xff1a; 模型尺寸问题&#xff1a;如果3D模型的尺寸在导入草图大师时与画布尺寸不匹配&#xff0c;可能导致模型混乱。解决方法是在3D建模软件中调整模型的尺寸&#xff0c;使其适…

FreeRTOS使用计数信号量进行任务同步与资源管理

FreeRTOS使用计数信号量进行任务同步与资源管理 介绍 在多任务系统中&#xff0c;任务之间的同步和对共享资源的管理是非常重要的。FreeRTOS 提供了丰富的同步机制&#xff0c;其中计数信号量是一种强大的工具&#xff0c;用于实现任务之间的同步和对资源的访问控制。 什么是…

figure方法详解之清除图形内容

figure方法详解之清除图形内容 一 clf():二 clear():三 clear()方法和clf()方法的区别&#xff1a; 前言 Hello 大家好&#xff01;我是甜美的江。 在数据可视化中&#xff0c;Matplotlib 是一个功能强大且广泛使用的库&#xff0c;它提供了各种方法来创建高质量的图形。在 Mat…

unity 拖入文件 窗口大小

目录 unity 拖入文件插件 设置窗口大小 unity 拖入文件插件 GitHub - Bunny83/UnityWindowsFileDrag-Drop: Adds file drag and drop support for Unity standalong builds on windows. 设置窗口大小 file build

Iceberg从入门到精通系列之二十一:Spark集成Iceberg

Iceberg从入门到精通系列之二十一&#xff1a;Spark集成Iceberg 一、在 Spark 3 中使用 Iceberg二、添加目录三、创建表四、写五、读六、Catalogs七、目录配置八、使用目录九、替换会话目录十、使用目录特定的 Hadoop 配置值十一、加载自定义目录十二、SQL 扩展十三、运行时配置…

电子电器架构——车载网关转发buffer心得汇总

电子电器架构——车载网关转发buffer心得汇总 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力…

vue2父组件向子组件传值时,子组件同时接收多个数据类型,控制台报警的问题

最近项目遇到一个问题,就是我的父组件向子组件(公共组件)传值时,子组件同时接收多个数据类型,控制台报警的问题,如下图,子组件明明写了可同时接收字符串,整型和布尔值,但控制台依旧报警: 仔细检查父组件,发现父组件是这样写的: <common-tabletooltip :content=…

2024 springboot Mybatis-flex 打包出错

Mybatis-flex官网&#xff1a;快速开始 - MyBatis-Flex 官方网站 从 Mybatis-flex官网获取模板后&#xff0c;加入自己的项目内容想打包确保错&#xff0c;先试试一下方法 这里改成skip的默认是true改成false&#xff0c;再次打包就可以了

Git系列---标签管理

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.理解标签2.创建标签…

MySQL原理(一)架构组成之物理文件组成

目录 一、日志文件 1、错误日志 Error Log 1.1、作用&#xff1a; 1.2、开启关闭&#xff1a; 1.3、使用 2、二进制日志 Binary Log & Binary Log Index 2.1、作用&#xff1a; 2.2、开启关闭&#xff1a; 2.3、Binlog还有一些附加选项参数 &#xff08;1&#x…