首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

InnoDB学习(五)之MVCC多版本并发控制

MVCC多版本并发控制,是一种数据库管理系统并发控制的方法。MVCC多版本并发控制下,数据库中的数据会有多个版本,分别对应不同的事务,从而达到事务之间并发数据的隔离。MVCC最大的优势是读不加锁,读写不冲突,在读多写少场景中,读写不冲突可以大幅提升数据库的并发性能。 MVCC多版本并发控制 在MYSQL中,MyISAM存储引擎使用的是表锁,InnoDB存储引擎使用的是行锁。而InnoDB的事务分为四个隔离级别,其中默认的隔离级别是可重复读,可重复读要求两个并行的事务之间数据的修改互不影响,通过添加行锁的方式虽然可以实现两个事务之间数据据的修改互不影响,但是者两个事务之间存在锁等待的情况,影响数据库效率。所以InnoDB的可重复读没有采用行锁,而是使用了更为强大的MVCC。 MVCC只有在可重复读和读已提交的隔离级别下生效,其它两个隔离级别和MVCC不兼容,因为读未提交总是读最新的数据行,和事务版本无关,串行化则是会对所有读取的行加锁。由于可重复读的情况比较复杂,并且是MySQL的默认隔离级别,所以本文会用可重复读来讲解MVCC的原理。 可重复读 数据库有四种隔离级别:读未提交/读已提交/可重复读/串行化,可重复度是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到一致的数据行。 数据行的一致性包含两部分: 情况1:已有数据的内容变更,在同一个事务中多次查询,查询结果应该相同,如果在当前事务中进行了修改,查询结果应该和当前事务中的修改结果相同; 情况2:数据行的增减,同一个事务只能查看到事务开启之前数据库中数据,或者由事务本身新增/删除的结果集,无法看到开启事务期间其它事务新增或删除的结果集; InnoDB默认的隔离级别是可重复读,可以解决以上两种情况的数据行一致性问题。其中解决情况1中的数据行一致性问题就是通过MVCC多版本并发控制实现的。 InnoDB用过Gap锁实现情况2中的数据行一致性问题,不过本文不会对Gap锁进行介绍。 MVCC的作用 MVCC可以确保同一个事务,在事务起始到结束读到的某一个数据是一致的,并且多个事务之间互不阻塞。我们以一张用户表为例,说明MVCC版本控制的作用。 首先我们需要创建用户表,并向其中插入一条用户数据,SQL语句如下: create table user_info ( age int , name varchar(255) ); insert into user_info(age,name) value (23,'张三'); 假设有A,B,C三个事务,这三个事务中在不同时刻对读取了插入用户的信息,并对用户信息进行了修改,时间线如下: T1时刻,事务A开始,事务A读取age=23的用户,该用户的name为张三; T2时刻,事务B开始,事务B读取age=23的用户,该用户的name为张三; T3时刻,事务A修改age=23的用户,把name修改为李四; T4时刻,事务A读取age=23的用户,该用户的name为李四,事务A提交事务; T5时刻,事务B读取age=23的用户,该用户的name为张三,事务B提交事务; T6时刻,事务C开始,事务C读取age=23的用户,该用户的name为李四,事务C提交事务; MVCC的作用可以在T5时刻体现出来,此时事务A已经提交,并且修改age=23的用户的name为李四,但是事务B看不到这次修改,事务B看到的age=23的用户的name为张三。这是因为在可重复度的隔离级别下,InnoDB事务读取到的数据是快照读,即事务B开始时为数据生成一个快照,事务B读到的数据始终都是这个快照,与快照读对应的是当前读: 当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁; 快照读:MVCC使用的就是快照读,在事务启动时为数据生成快照,快照读可以避免了加锁操作,提升数据库性能; MVCC原理 MVCC的目的就是多版本并发控制,在InnoDB中引入MVCC就是为了解决读写冲突,MVCC主要包含三部分内容:数据库中的3个隐藏字段、UndoLog日志 、ReadView读视图,这三部分在MVCC中的作用分别如下所示: 隐藏字段:为数据添加额外的版本信息,是MVCC版本控制的基石; UndoLog:存储了多个版本的数据,不同版本数据隐藏字段的内容不同; ReadView:判断当前事务应该读取哪个版本的数据; 隐藏字段 隐藏字段意味着我们通过SQL语句查找不到这些字段,但是这些字段在数据库中实际存在并占用了存储空间。为了实现MVCC版本控制,InnoDB为每一行数据添加了以下3个隐藏字段: DB_TRX_ID:6字节,最后修改本记录的事务ID; DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本(存储于Rollback Segment); DB_ROW_ID:6字节,隐藏主键,如果数据表没有显式主键,InnoDB用DB_ROW_ID构建聚簇索引; 我们使用以下SQL创建用户表,并向表中插入一条数据,新表会默认包含三个隐藏字段,表结构如下表所示。 create table user_info ( age int, name varchar(255) ); insert into user_info(age,name) value (23,'张三'); |age|name|DB_TRX_ID|DB_ROLL_PTR|DB_ROW_ID| |--|--|--|--|| |23|张三|1|0x222333|1| UndoLog日志 我在另外一篇文章中介绍过UndoLog日志,从名字也可以看出来,UndoLog日志主要用于回滚事务。但是InnoDB中的MVCC的快照读也使用了UndoLog。UndoLog可以分为两大类: Insert UndoLog:事务中的Insert语句对应的UndoLog,只在事务回滚时需要,所以事务提交后可以被立即丢弃; Update UndoLog:事务在进行Update或Delete时产生的UndoLog; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被Purge线程统一清除; Purge线程:InnoDB中,被删除的数据不会直接删除,而是先标记为删除,无用的Update UndoLog也不会立即删除。这些数据都是通过InnoDB中的后台任务Purge线程进行删除的。 下文中我们以上文中的用户表以及数据为例,解释Update UndoLog的工作流程,如下为起始时user_info表空间的数据状态: T1时刻,事务A开始,事务Id为2,事务A读取age=23的用户,该用户的name为张三;此时没有修改数据库数据,没有生成UndoLog,表空间无变化; T2时刻,事务B开始,事务Id为3,事务B读取age=23的用户,该用户的name为张三;此时没有修改数据库数据,没有生成UndoLog,表空间无变化; T3时刻,事务A修改age=23的用户,把name修改为李四;此时由于事务A尚未提交,所以会给事务A生成一条UndoLog,UndoLog中存储了事务A修改前的数据,表空间中最新数据中的回滚指针指向这条日志; T4时刻,事务A读取age=23的用户,由于表数据中的记录的事务ID和事务A的事务ID一致,所以事务A会读取到表数据中的记录,读取到用户的name为李四,事务A提交事务; T5时刻,事务B读取age=23的用户,由于表空间中数据不满足可见性条件(下一节具体介绍),所以事务B会查找表数据的UndoLog,UndoLog中的数据满足可见性条件,所以查询到UndoLog中的用户,用户的name为张三,事务B提交事务; T6时刻,事务C开始,事务ID为3,事务C读取age=23的用户,由于事务C开始时事务A已经提交,所以事务C可以查询到已提交的数据,事务C读取到用户的name为李四; T7时刻,事务C开始,事务ID为3,事务C修改age=23的用户,把name修改为王五;此时由于事务C尚未提交,所以会给事务C生成一条UndoLog,UndoLog中存储了事务C修改前的数据; 从上面的例子可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的UndoLog成为一条记录版本线性链表,UndoLog的链首就是最新的旧记录,链尾就是最早的旧记录(UndoLog的节点可能会被Purge线程清除掉) UndoLog是为回滚而用,具体内容就是复制事务前的数据库记录行到UndoBuffer,在适合的时间把UndoBuffer中的内容刷新到磁盘。UndoBuffer与RedoBuffer一样,也是环形缓冲,但当缓冲满的时候,UndoBuffer中的内容会也会被刷新到磁盘;与RedoLog不同的是,磁盘上不存在单独的UndoLog文件,所有的UndoLog均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。 ReadView读视图 ReadView就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大) 所以我们知道ReadView主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个ReadView读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的UndoLog里面的某个版本的数据。 ReadView遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由ReadView维护),如果DB_TRX_ID跟ReadView的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出UndoLog中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。 ReadView判断可见性的原理如下,在InnoDB中,创建一个新事务之后,当新事务读取数据时,数据库为该事务生成一个ReadView读视图,InnoDB会将当前系统中的活跃事务列表创建一个副本保存到ReadView。当用户在这个事务中要读取某行记录的时候,InnoDB会将该行当前的版本号与该ReadView进行比较。具体的算法如下: 设该行的当前事务ID为cur_trx_id,ReadView中最早的事务ID为min_trx_id, 最迟的事务ID为max_trx_id; 如果cur_trx_id < min_trx_id,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。跳到步骤6. 如果cur_trx_id > max_trx_id,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见.跳到步骤5; 如果min_trx_id<= cur_trx_id <= max_trx_id, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从min_trx_id到max_trx_id进行遍历,如果cur_trx_id等于他们之中的某个事务id的话,那么不可见。跳到步骤5; 从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的UndoLog的版本号,将它赋值该cur_trx_id,然后跳到步骤2; 将该可见行的值返回; 总结一下:MVCC版本控制中,以事务第一次快照读为分界线,事务后续只能查找到第一次快照读及之前提交的数据版本,之后提交的数据版本不可见。 读已提交和可重复度 读已提交和可重复度隔离级别下的InnoDB快照读有什么不同?答案是:ReadView生成时机的不同,从而造成读已提交和可重复度级别下快照读的结果的不同: 可重复读隔离级别下,事务第一次快照读会生成ReadView时,ReadView会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于ReadView创建的事务所做的修改均是可见; 读已提交隔离级别下的,事务每次快照读都会新生成一个快照和ReadView, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因; 总之在读已提交隔离级别下,是每个快照读都会生成并获取最新的ReadView;而在可重复读隔离级别下,则是同一个事务中的第一个快照读才会创建ReadView, 之后的快照读获取的都是同一个ReadView。 MVCC与幻读 幻读是指,同一个事务里面连续执行两次同样的SQL语句,可能导致不同结果的问题,第二次SQL语句可能会返回之前不存在的行。举例说明:T1时刻事务A和事务B同时开启,分别进行了快照读,然后事务A向数据库中插入一条新的记录,如果事务B可以读到这条记录,就出现了"幻读",因为B第一次快照读没有读到这条数据。 MVCC是否可以解决幻读问题呢?答案是有的情况下可以解决,有的情况下不可以解决。如果事务B中的读是快照读,那么MVCC版本控制可以解决幻读问题;如果事务B中使用的是当前读,那么MVCC无法解决幻读问题。 快照读是基于MVCC和UndoLog来实现的,适用于简单Select语句; 当前读是基于Gap锁来实现的,适用于Insert,Update,Delete,Select ... For Update, Select ... Lock In Share Mode语句,以及加锁了的Select语句; 事实上,MVCC对于所有的当前读都无效,比如事务A修改数据之后,事务B去Update对应的数据,Update语句筛选条件针对的是数据库中当前的数据,而不是快照数据。 我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd 参考文档 MySQL之MVCC与幻读<br/> 正确的理解MySQL的MVCC及实现原理<br/> MySQL数据库事务各隔离级别加锁情况--read committed && MVCC 本文最先发布至微信公众号,版权所有,禁止转载!

优秀的个人博客,低调大师

Rasa Open Source 3.0 发布,开源机器学习框架

Rasa Open Source 提供了创建虚拟助手的基石。使用 Rasa 可以在网站和社交媒体等平台,实现人机交互的自动化。Rasa Open Source 提供了三个主要功能,自然语言理解、对话管理和集成。 Rasa Open Source 3.0 发布,更新内容如下: 弃用与移除 #6487: 删除与 Rasa Open Source 1.x、Rasa Enterprise 0.35 的向下兼容代码,以及rasa.cli.x、rasa.core.utils、rasa.model_testing、rasa.model_training和rasa.shared.core.events中过时的向后兼容代码。 #8569: 移除了对 Python 3.6 的支持,因为它将在 2021 年 12 月结束支持. #8869: 从DialogueStateTracker中删除了弃用的change_form_to和set_form_validation方法。 #8870: 移除对 Markdown 训练数据格式的支持。 #8872: 从TrainingData中删除已弃用的sorted_intent_examples方法。 #8871: Removed automatic renaming of deprecated actionaction_deactivate_formtoaction_deactivate_loop.action_deactivate_formwill just be treated like other non-existing actions from now on. #8873: 当使用class_from_module_path加载类以外的类型时,会引发RasaException而不是弃用警告。 …… 特性 #10150: 训练数据版本从 2.0 升级到 3.0,因为 Rasa 3.0 中的格式发生了重大变化。 #10170: 增加了一个新的实验性功能,称为Markers。Markers允许你将对话中的兴趣点定义为一组需要满足的条件。一个新的命令rasa evaluate markers允许你将这些条件应用于你现有的跟踪器存储,并输出条件满足的点。 #9803: Rasa Open Source 现在使用模型配置来构建 directed acyclic graph 改进 #10189:更新了/status端点响应有效载荷和相关文档,以返回/反映更新的 3.0 键/值。 #7619: 将 TensorFlow 版本升级至 2.6。 #8057: 为连接到外部 RabbitMQ 服务器增加了认证支持。 #8469: 增加了i命令行选项,使 RASA 监听一个特定的 IP 地址,而不是任何网络接口。 #8760:rasa data validate现在会检查 active_loop 指令中引用的表单是否在域中定义。 #8914: 每个对话事件现在都在其元数据中包括创建时加载的模型的 ID。 #8924: 通过事件代理向 Rasa X 发送 UserUttered 事件以及用户消息令牌的索引。 #9068: 将 spaCy 的依赖性从 3.0 版本升级到 3.1 版本。 …… 错误修复 #10079: 修正了验证行为和围绕未使用的意图和语料的日志输出。 #8614:rasa test nlu --cross-validation在没有定义管道时使用自动配置,而不是失败。 #9852: 修复 CVE-2021-41127 …… 改进文档 #10095: 为 Markers 添加了新的文档。 #10230: 在安装 rasa 的同一命令中更新 pip,并在文档中澄清支持的版本。 …… 更多详情可查看:https://rasa.com/docs/rasa/changelog/

优秀的个人博客,低调大师

Go语言学习11-数据的使用

4. Go语言数据的使用 前面的博文,我们介绍了Go语言的各种数据类型,包括基本数据类型、数组类型、切片类型、字典类型、函数类型、接口类型、结构体类型和指针类型;从本篇开始我们一起来了解Go语言数据的使用。 4.1 赋值语句 如果值 x 可以被赋给类型为 T 的变量,那么它们至少需要满足以下条件中的一个赋值规则: 如果值 x 的类型是 T , 那么 x 可以被赋给 T 类型的变量。 如果值 x 的类型是 V,那么 V 和 T 应该具有相同的潜在类型,并且它们之中至少有一个是未命名的类型。未命名的类型是指未被署名的数据类型。例如,字面量: struct { a int b string }{0,"string"} 所代表的值的类型是 struct { a int b string } 而这个类型就是一个未命名的类型。它的潜在类型与结构体类型 type Anonym struct { a int b string } 相同。因此,上面的字面量可以被赋给类型为 Anonym 的变量。 类型 T 是一个接口类型,且值 x 的类型实现了 T。因此,x 就可以被赋给类型为 T 的变量。 如果值 x 是一个双向通道类型的值,而 T 也是一个通道类型。那么 x 的类型 V 和 T 应该具有相同的元素类型,并且它们之中至少有一个是未命名的类型。(在之后讲述通道类型的时候详细说明) 如果值 x 预定义标识符 nil,那么它可以被赋给切片类型、字典类型、函数类型、接口类型、指针类型和通道类型的变量。只要变量不是值类型,它就可以被赋予空值 nil。 如果值 x 是一个由某个数据类型的值代表的无类型的常量(可以理解为字面量),那么他就可以被赋给该数据类型的变量。例如,字符串字面量“ABC”可以被赋给 string 类型的变量,以及整数字面量 123 可以被赋给 int 类型的变量。 所有值都可以被赋给 空标识符"_"。空标识符有时也被称为占位标识符。它只起到占位的作用,不会与任何值建立绑定关系。 赋值语句一般由左右分立的两个表达式列表和处于中间的一个赋值操作符组成。例如: var ints = []int{1, 2, 3} 表达式列表中的多个表达式之间需要有逗号作为分隔符。在大多数情况下,左右两边的表达式的数量必须是相同的。当左边表达式的数量大于1时,就形成了多个赋值操作同时进行的情况,这种情况常常被称为平行赋值。 在赋值操作符左边的那个表达式列表中的每个表达式的结果值都必须是可寻址的。如果不需要对复制操作符右边的某个表达式的结果值进行绑定,可以在赋值操作符左边的相应位置上应用 空标识符"_"。例如: ints[1], _ = (ints[1] + 1), (ints[2] + 1) 对于普通赋值语句(以=为赋值操作符的赋值语句)来说,在赋值操作符两边的表达式的数量可以不相等。当在赋值操作符的右边只有一个表达式且该表达式是一个多值表达式(与单值表达式相对应)的时候,在赋值操作符的左边的表达式可以有多个。这时右边唯一的表达式有4种情况: 此表达式是一个调用会返回多个结果的函数或者方法的表达式。这时,在赋值操作符的左边的表达式的数量应该等于该函数或方法的结果的数量。 此表达式是一个应用于字典值之上的索引表达式。这时,在赋值操作符的左边可以有一个或两个表达式。例如: v, ok := map["k1"] 此表达式是一个类型断言表达式。这时,在赋值操作符的左边可以有一个或两个表达式。例如: v, ok := x.(string) 此表达式是一个由接收操作符和通道类型值组成的表达式。这时,在赋值操作符的左边可以有一个或两个表达式。例如: v, ok := <-ch 赋值语句的执行分为两个阶段: 第一个阶段,在赋值操作符左边的索引表达式和取址表达式的操作数以及在赋值操作符右边的表达式,都会按照通常的顺序被求值。 第二个阶段,赋值会以从左到右的顺序进行。 在Go语言中可以使用平行赋值来交换两个变量的值: a, b = b, a 由于平行赋值永远是从左向右进行的,所以即使靠右的赋值引发了运行时恐慌,它左边的赋值也依然生效。 在赋值语句中,每个右边的表达式的结果值必须是可以被赋给与其相对应的左边的表达式的类型的,即使这些值由无类型的常量代表。 4.2 常量与变量 常量一旦被声明它的值就不能被改变,而对于变量却没有这样的限制。 4.2.1 常量 在Go语言中,常量总会在编译期间被创建,即使它们作为局部变量被定义在了函数内部。常量只能由字面量常量或常量表达式来赋值。常量表达式是能够且会在编译期间被求值的。而其他的表达式只能在程序运行期间被求值,所以它们并不能被赋给常量。 Go语言的常量:布尔常量、rune常量(也成为字符常量)、整数字面量、浮点数字面量、复数字面量或字符串字面量表示。由相应的字面量表示的基本数据类型的值都可以被称为常量值。由字面量表示的常量值也简称为字面常量。例如,布尔字面量true是一个字面常量。 常量可以是有类型的也可以是没有类型的。有字面量表示的常量,如true、false、“A”和 iota 以及由仅以无类型的常量作为其操作数的常量表达式的结果值都属于无类型的常量。 从语言规范上来说,数组型常量可以有任意的精度,但编译器却只会使用一个有限精度的内部表示方法来实现它们。对于每一个实现,都必须满足一下条件: 整数字面量至少要用256个比特位来表示。 浮点数字面量的小数部分至少要用256个比特位来表示,而其指数部分至少要用32个比特位来表示。对于复数常量的实部和虚部中的相应部分也是如此。 若不能精确地表示一个整数常量,则要给出一个错误。 若由于溢出而不能表示一个浮点数常量或复数常量,则要给出一个错误。 若由于精度限制而不能表示一个浮点数常量或复数常量,则这个值会被四舍五入为一个可表示的最相近的常量。 4.2.1.1 常量表达式 常量表达式就是仅以常量作为操作数的表达式。无类型的布尔常量、数值常量和字符串常量都可以作为常量表达式的操作数。如果一个二元操作的操作数是两个不同种类的无类型的数组型常量,那么对于非布尔操作(不包含比较操作符的操作)来说其操作结果的种类遵循着这样的优先级顺序(从高到低):复数、浮点数、rune 和整数。例如: 2 + 3.0 // 结果是一个无类型的浮点数常量5.0 15 / 4.0 // 结果是一个无类型的浮点数常量3.75 'w' + 1 // 结果是一个无类型的rune常量'x' 操作数为无类型常量的移位操作的结果总会是一个无类型的整数常量。例如: 1 << 3.0 // 结果是一个无类型的整数常量8 1.0 << 3 // 结果是一个无类型的整数常量8 比较操作结果总会是一个无类型的布尔常量。例如: "A" > "C" // 结果是一个无类型的布尔常量false 常量表达式总会被正确地求值。中间值和作为表达式结果的常量值自身都会有足够的精度。这个精度可以比Go语句中预定义的那些类型所支持的精度更高。例如: 1 << 100 // 结果是一个无类型的整数常量1267650600228229401496703205376 这个常量值实际上已经超出了Go语言中任何一个整数类型(即使是 uint64 类型)所能表示的范围了。 对于有类型的常量来说,它的值必须永远能够被精确地表示为其类型的值。 4.2.1.2 常量的声明 常量声明会将字面量或常量表达式与标识符绑定在一起。与变量不同的是,对常量的赋值必须与其声明同时进行。并且,只能对常量赋值一次。 一条常量声明语句总会以关键字 const 开始。例如: const untypedConstant = 10.0 // 无类型的常量的声明 const typedContstant int64 = 1024 // 有类型的常量的声明 在常量声明语句中还可以包含平行赋值。如下: const tc1, tc2, tc3 int64 = 1024, -10, 88 在包含平行赋值的常量声明语句中,如果类型被给定,那么所有的常量的类型都应该与这个被给定的类型一致。相应的,赋值操作符右边的字面常量或常量表达式的结果值也都可以被赋给这个类型。 如果在包含平行赋值的常量声明语句中为给定类型,那么赋值操作符右边的多个字面量或常量表达式的结果值的种类都会是彼此独立的,即它们的种类都可以是任意的。例如: // 标识符 utc1, utc2, utc3分别表示了一个浮点数常量,一个布尔常量和一个字符串常量。 const utc1, utc2, utc3 = 6.3, false, "C" 将上面的常量声明语句进行拆分,如下: const ( utc1 = 6.3 utc2 = false utc3 = "C" ) 在这个圆括号中的每一行都可以被称为一个常量声明。在圆括号中的常量声明内,同样可以进行平行赋值: const ( utc1, utc2 = 6.3, false utc3 = "C" ) 在带圆括号的常量声明语句中,有时候并不需要显式地对所有的常量进行赋值。被省略了赋值的常量实际上还是有值的,只不过这个值是被隐含地赋予了。它们的值及其类型都会与上面的,最近的且被显式赋值的那个常量相同。因此对第一个被声明的常量的赋值是永远不能够被省略的。例如: const ( utc1, utc2 = 6.3, false utc3 = "C" utc4 utc5 ) // 常量utc4和utc5的值及其类型都会与在它们上面被声明的常量utc3相同。 尽管可以被省略,但是还是有两个与此有关的约束: 如果有一个未被显示赋值的常量,那么与它同一行的常量(如果有的话)的赋值也都必须被省略。 在未包含显示赋值的那一行常量中的常量标识符的数量必须与在它上面的、最近的且包含显式赋值的那一行常量声明中的常量标识符的数量相等。例如: const ( utc1, utc2, utc3 = 6.3, false, "C" utc4, utc5 ) // 不合法,不符合约束2,会造成一个编译错误。 可以这样解决: const ( utc1 = 6.3 utc2, utc3 = false, "C" utc4, utc5 ) 在Go语言中不但可以为隐含地多个常量赋予同一个值,而且还可以更加方便地对多个常量分别赋予一系列连续的值。例如: const { Sunday = itoa Monday Tuesday Wednesday Thursday Friday Saturday } 在常量声明语句中,iota 代表了连续的、无类型的整数常量。它第一次出现在一个以 const 开始的常量声明语句中的时候总会表示整数常量 0。 在同一条常量声明语句中,iota 在第二个包含它的常量声明中会表示为整数常量 1,在第三个包含它的常量声明中会表示为 2,以此类推。随着在同一条常量声明语句中包含 iota 的常量声明的数量的增加,iota 所表示的整数值也会递增。 const x = iota // 常量x的值是整数常量0 const y = iota // 常量y的值是整数常量0 利用 iota 进行更加灵活的常量隐式赋值。例如: const { u = 1 << iota v w } // 常量u、v和w的值分别是1, 2, 4。 在同一条常量声明语句中,iota 代表的整数常量的值是否递增取决于是否又有一个常量声明包含了它,而不是它是否又在常量声明中出现了一次。例如: const { e, f = iota, 1 << iota g, h i, j } // e,f的值为0,1 g,h的值为1,2 i,j的值为2,4 Go语言中可以利用 空标识符"_" 来跳过 iota 表示的递增序列中的某个或某些值。例如: const { e, f = iota, 1 << iota _, _ g, h i, j } // e,f的值为0,1 g,h的值为2,4 i,j的值为3,8 总结:对常量进行声明的时候必须同时对它进行赋值。对一个常量的赋值只能进行一次,且只有字面常量和常量表达式可以作为它的值。可以利用 隐式赋值、平行赋值和 itoa 对常量进行非常灵活和复杂的赋值操作。 4.2.2 变量 变量 与 常量 的最主要的区别是它在被声明之后可以被赋值任意次。对于变量来说,它的值是可以在程序运行期间才被计算出来的。 4.2.2.1 变量声明 一个变量声明可以将一个标志符与一个变量值绑定在一起。前提条件是这个变量值与该变量的类型之间必须要满足赋值规则。 变量声明语句总是会以关键字 var 开始: var v int64 = 0 我们也可以省略变量的类型: var v = 0 如果变量的类型未被显式地指定,那么它将会由变量值推导得出。如果在省略类型的同时,赋值操作符右边的表达式的求值结果是一个字面常量,那么该变量的类型将会根据这个字面常量的种类被推导出来。 字面常量与变量类型的对应关系 字面常量的种类 变量的类型 布尔常量 bool 字符常量 rune 浮点数常量 float64 复数常量 complex128 字符串常量 string 以上的对应情况下,Go语言的运行时程序会根据字面常量的种类将其转换为对应的数据类型,然后在赋给相应的变量。 在Go语言中同样可以对多个变量进行平行赋值: var v1, v2 = 0, -1 把多个变量的声明拆分成多行: var ( v1 = 0 v2 = -1 ) 注意: 隐式赋值 在变量声明中是不可用的。 在Go语言中,可以不对一个新声明的变量的值进行初始化。如果初始化的显示赋值被省略,那么变量的值将会是与该变量的类型相对应的零值。这时,变量的类型不可以被省略。如果是平行赋值的话,要么省略其中所有变量的初始赋值,要么就必须对所有变量进行初始赋值。例如: var v3, v4, v5 float64 或者必须这样: var v3, v4, v5 float64 = 3, 4, 5 4.2.2.2 局部变量 与常量相同,变量声明可以作为源码文件中的顶级元素,也可以成为函数体内容的一部分。前者可以称为全局变量,后者可以被称为某个函数的局部变量。局部变量有时也被称为本地变量。 在函数体内部,局部变量会遮蔽与它同名的全局变量。例如: packge main import "fmt" var v6 bool //没有初始化,默认为false func main() { var v6 bool = true fmt.Printf("v6: %v\n", v6) // 打印字符串 v6: true } 在函数内部声明变量的时候可以采用一种简单方式,前面已经涉及过,如下: v6 := true // 短变量声明,根据它的值推出变量的类型 短变量声明也不需要以 var 开始,这种特殊标记 := 只会被用于对变量的声明和初始化的语句中,所以并不会产生歧义。短变量声明与普通变量声明一样,也支持平行赋值。例如: v7, v8 := "Go", 1.2 重声明仅会出现在短变量声明中,可以理解为对当前的已存在变量的又一次赋值。例如: v8, v9 := 2.0, false 短变量声明的约束条件: 短变量声明仅能够在函数体内部声明变量的时候使用。 在短变量声明中的:=的左边的标识符至少要有一个代表在当前上下文环境中的新变量。 注意 :空标识符"_" 代表的并不是新的变量,即使它在当前上下文环境中并没有出现过。 短变量声明可以出现在 if、for 和 switch 等语句的初始化器中,并被用来声明仅存在于这些语句块中的本地临时变量。这些知识点在之后的博文讲述的Go语言流程控制方法中介绍。 如果我们在当前上下文环境中声明了某个局部变量但没有使用它,那么就会造成一个编译错误。Go语言认为这种情况是对计算机资源的浪费,甚至预示着更加严重的问题的出现。 注意:对变量的赋值并不算是对它的使用。 4.3 可比性与有序性 4.3.1 类型的恒等 别名类型和它的源类型是两个完全不同的类型。一个命名类型和一个匿名类型总是不恒等的。如果两个匿名类型的类型字面量是相同的,就可以说它们是恒等的。 各个数据类型的恒等判断方法的规则: 对于两个数组类型,如果它们的长度一致且元素的数据类型是恒等的,那么它们就是恒等的。 [4]string [4]string//这两个数组类型就是恒等的 [4]string [3]string//这两个数组类型就是不恒等的。 数组类型的长度是类型声明的一部分,也是类型的一部分 对于两个切片类型,如果它们的元素的数据类型恒等,那么它们就是恒等的。 []string []string//这两个切片类型是恒等的。 切片类型的长度并不会存在于类型声明中,并且两个同一切片类型的值的长度也不一定会相同。 对于两个结构体类型来说,如果它们之中的字段声明的数量是相同的,并且在对应位置上的字段具有相同的字段名称(如果有的话)和恒等的数据类型,那么这两个结构体数据类型就是恒等的。 var a1 struct { f1 sort.Interface f2 int64 } var a2 struct { f1 sort.Interface f2 int64 } 从字面看,变量 a1 和变量 a2 的类型显然是恒等的。如果其中一个结构体类型声明中有匿名函数,那么在另一个结构体类型的声明中的对应位置上的字段声明也必须不包含名称,否则,它们就是不相等的。如果结构体类型的某个字段声明是有标签的,那么这个标签也应该作为恒等判断的一个依据。 对两个指针类型,如果它们的基本类型(也就是它们指向的那个类型)恒等,那么它们就是恒等的。 对于两个函数类型,如果它们包含了相同数量的参数声明和结果声明,并且在对应位置上的参数或结果的类型都是恒等的,那么它们就是恒等的。 func(name string, dept string, isManager bool) (id int, done bool) func(appName string, targetOs string, authRequired bool) (id int, doen bool) 函数类型的恒等判断并不会以参数和结果的名称为依据,而只关心它的数量、顺序和类型。如果其中一个函数类型是可变参函数,那么另一个函数类型也应该是这样,否则它们就是不恒等的。 对于两个接口类型,如果它们拥有相同的方法集合,那么它们就是恒等的。两个接口类型所包含的方法声明的数量必须相同,并且对于一个接口类型中包含的每一个方法声明都能够在另一个接口类型中找到与它完全相等的方法声明。两个接口类型中的方法声明的顺序是无关紧要的。如下两个是恒等的: type Ia interface { Name() string Age() int } type Ib interface { Age() int Name() string } 对于两个字典类型,如果它们具有恒等的键类型和元素类型,那么它们就是恒等的。 对于两个通道类型,如果它们具有恒等的元素类型,并且方向相同,那么它们就是恒等的。(之后的博文中会详细的介绍) 注意: 如果两个数据类型在不同的代码包中,即使它们满足了上述相关规则也是不相等的。 4.3.2 数据的可比性与有序性 上面的类型的恒等阐述了Go语言的数据类型之间的可比性,下面关注的是数据类型的值之间的可比性和有序性。可比性 是可以判断相等与否,有序性 是可以比较大小的含义。 各个数据类型的值的相关特性: 布尔类型值具有可比性。布尔值只有 true 和 false 两种可能。两个布尔值可以判断是否相等,却无法比较两个布尔值的大小。 整数类型值具有可比性,也具有有序性。 浮点数类型值具有可比性,也具有有序性。这被定义在IEEE-754标准中(一个针对二进制浮点数的算术标准)。 复数类型值具有可比性。判断两个复数类型值是否相等的结果是通过分别对它们的实部和虚部上的值进行比较而得出的。 字符串类型值具有可比性,也具有有序性。两个字符串类型值判断相等或比较大小的方法就是对它们中的每个对应位置上的字节进行判断或比较。这就相当于对多对整数类型值依次进行判断或比较,直到可以得出结果为止。 指针类型具有可比性。如果两个指针类型值指向了同一个变量,或者它们都为空值 nil,那么就可以判定它们是相等的。例如: numArray := [3]int{1, 23, 456} p1 := &numArray p2 := &numArray fmt.Printf("%v\n", p1 == p2) // 打印true 通道类型值具有可比性。如果两个通道类型值的元素类型和缓冲区大小都一致,那么就可以被判定为相等。如果两个通道类型的变量的值都是 nil ,那么它们也是相等的。 接口类型值具有可比性。如果两个接口类型值拥有相等的动态类型和相同的动态值,那么就可以判定它们是相等的。如果有一个接口类型的变量,那么在这个变量中就只能存储实现了该接口类型的类型的值。把存储在该变量中的那个值的类型叫作该变量的动态类型,而把这个值叫作该变量的动态值。如果两个接口类型的变量的值都是空值,那么它们也是相等的。例如: type Ic interface { Code() string } type Sc struct { code string } func (self Sc) Code() string { return self.code } 结构体类型 Sc 是接口类型 Ic 的一个实现类型。如果有两个 Ic 类型的变量: var ic1 Ic = Sc{code: "A"} var ic2 Ic = Sc{code: "A"} fmt.Printf("%v\n", ic1 == ic2) // 打印true 非接口类型 X 的值 x 可以与接口类型 T 的值 t 判断相等,当且仅当接口类型 T 具有可比性且类型 X 是接口类型 T 的实现类型。新增一个变量声明: var sc1 Sc = Sc{code: "A"} fmt.Printf("%v\n", ic1 == sc1) // 打印true fmt.Printf("%v\n", ic2 == sc1) // 打印true 如果一个结构体类型中的所有字段都具有可比性,那么这个结构体类型的值就具有可比性。如果两个结构体值中的对应的字段值是相等的,那么这两个结构体类型值就是相等的。例如: type Sd struct { ints []int } sd1 := Sd{ints: []int{0, 1}} sd2 := Sd{ints: []int{0, 1}} fmt.Printf("%v\n", sd1 == sd2)//被编译的时候就会造成一个编译错误 结构体类型 Sc 中包含了一个切片类型的字段,而切片类型的值是不具有可比性的。 数组类型值具有可比性,当前仅当其元素类型的值具有可比性。如果两个数组类型值在对应位置上的值都是相等的,那么这两个数组类型值就是相等的。例如: slices1 := [3][]int{[]int{0, 1}} slices2 := [3][]int{[]int{0, 1}} fmt.Printf("%v\n", slices1 == slices2)//被编译的时候就会造成一个编译错误 变量 slice1 和 slice2 都代表了元素类型为 [ ]int 的数组类型的值。这两个值的类型的元素类型都是不具有可比性的,从而这两个数组类型的值也不具有可比性。 在判断两个具有相同接口类型的值是否相等的时候,如果它们的动态类型不具有可比性就会引发一个运行恐慌。比如,两个接口类型的变量的动态类型是 切片类型 或 字典类型 的 别名类型。同样适用于如下情况,以接口类型为元素类型的数组类型的值,以及以接口类型为其中某个字段的类型的结构体类型的值。 type Se []int func (self Se) Code() string { return "" } // 这个实现了接口类型Ic的类型的值就可以被赋给Ic类型的变量 var ic3 Ic = Se{1, 2} var ic4 Ic = Se{1, 2} fmt.Printf("%v\n", ic3 == ic4) // 这里会引发一个运行时恐慌,切片类型Se不具有可比性 func (self Sd) Code() string { return "" } var ic5 Ic = Sd{ints: []int{0, 1}} var ic6 Ic = Sd{ints: []int{0, 1}} fmt.Printf("%v\n", ic5 == ic6) // 这里会引发一个运行时恐慌,结构体类型Sd不具有可比性 注意: 切片类型、字典类型 和 函数类型 的值是不具有可比性的。但这些值可以与空值 nil 进行判等的。 下篇继续讲解Go语言数据的使用,主要包括 类型转换 和 内建函数 的介绍。 最后附上知名的Go语言开源框架: NSQ: 一个实时的分布式消息平台。它拥有很高的可伸缩性,并能够每天处理数以十亿计的消息。它的官方网址是:http://nsq.io

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册