摘录
- Go语言是Google公司开发的一种静态型、编译型并自带垃圾回收和并发的编程语言。
- Go语言不使用虚拟机,只有运行时(runtime)提供垃圾回收和goroutine调度等。
- Go语言使用自己的链接器,不依赖任何系统提供的编译器、链接器。因此编译出的可执行文件可以直接运行在几乎所有的操作系统和环境中。
- 从Go 1.5版本之后,Go语言实现自举,实现了使用Go语言编写Go语言编译器及所有工具链的功能。
- Go语言可以利用自己的特性实现并发编译,并发编译的最小元素是包。从Go 1.9版本开始,最小并发编译元素缩小到函数,整体编译速度提高了20%。
- Go语言的并发是基于goroutine,goroutine类似于线程,但并非线程。可以将goroutine理解为一种虚拟线程。Go语言运行时会参与调度goroutine,并将goroutine合理地分配到每个CPU中,最大限度地使用CPU性能。
- 在Go语言中,自增操作符不再是一个操作符,而是一个语句。因此,在Go语言中自增只有一种写法:
i++
如果写成前置自增“++i”,或者赋值后自增“a=i++”都将导致编译错误。 - 在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错。
- 布尔型无法参与数值运算,也无法与其他类型进行转换。
- 切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
- 变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址。
- 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
- “*”操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值;当操作在左值时,就是将值设置给指向的变量。
- 堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
- 编译器觉得变量应该分配在堆和栈上的原则是:
- 变量是否被取地址。
- 变量是否发生逃逸。
- ASCII字符串长度使用len()函数。
- Unicode字符串长度使用utf8.RuneCountInString()函数。
- ASCII字符串遍历直接使用下标。
- Unicode字符串遍历用for range。
- Go语言的字符串是不可变的。
- 修改字符串时,可以将字符串转换为[]byte进行修改。
- []byte和string可以通过强制类型转换互转。
- 一般情况下,这个过程可以交给编译器,让编译器在编译时,根据元素个数确定数组大小。
var team = [...]string{"hammer", "soldier", "mum"}
“...”表示让编译器确定数组大小。上面例子中,编译器会自动为这个数组设置元素个数为3。 - 从数组或切片生成新的切片拥有如下特性。
- 取出的元素数量为:结束位置-开始位置。
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用slice[len(slice)]获取。
- 当缺省开始位置时,表示从连续区域开头到结束位置。
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾。
- 两者同时缺省时,与切片本身等效。
- 两者同时为0时,等效于空切片,一般用于切片复位。
- 根据索引位置取切片slice元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误。生成切片时,结束位置可以填写len(slice)但不会报错。
- 切片有点像C语言里的指针。指针可以做运算,但代价是内存操作越界。切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。
- 切片是动态结构,只能与nil判定相等,不能互相判等时。
- 切片已经被分配到了内存,但没有元素,因此和nil比较时是false。
- 使用make()函数生成的切片一定发生了内存分配操作。但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。切片不一定必须经过make()函数才能使用。生成切片、声明后使用append()函数均可以正常使用切片。
- append()函数除了添加一个元素外,也可以一次性添加很多元素。
- Go语言中切片删除元素的本质是:以被删除元素为分界点,将前后两个部分的内存重新连接起来。
- 大多数语言中映射关系容器使用两种算法:散列表和平衡树。
- 散列表可以简单描述为一个数组(俗称“桶”),数组的每个元素是一个列表。
- 根据散列函数获得每个元素的特征值,将特征值作为映射的键。如果特征值重复,表示元素发生碰撞。碰撞的元素将被放在同一个特征值的列表中进行保存。散列表查找复杂度为O(1),和数组一致。最坏的情况为O(n),n为元素总数。散列需要尽量避免元素碰撞以提高查找效率,这样就需要对“桶”进行扩容,每次扩容,元素需要重新放入桶中,较为耗时。
- 平衡树类似于有父子关系的一棵数据树,每个元素在放入树时,都要与一些节点进行比较。平衡树的查找复杂度始终为O(log n)。
- sort.Strings的作用是对传入的字符串切片进行字符串字符的升序排列。
- Go语言中并没有为map提供任何清空所有元素的函数、方法。清空map的唯一办法就是重新make一个新的map。不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数高效多了。
- Go语言中的map在并发情况下,只读是线程安全的,同时读写线程不安全。
- 也就是说使用了两个并发函数不断地对map进行读和写而发生了竞态问题。map内部会对这种并发操作进行检查并提前发现。
- sync.Map有以下特性:
- 无须初始化,直接声明即可。
- sync.Map不能使用map的方式进行取值和设置等操作,而是使用sync.Map的方法进行调用。Store表示存储,Load表示获取,Delete表示删除。
- 使用Range配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值。Range参数中的回调函数的返回值功能是:需要继续迭代遍历时,返回true;终止迭代遍历时,返回false。
- sync.Map没有提供获取map数量的方法,替代方法是获取时遍历自行计算数量。sync.Map为了保证并发安全有一些性能损失,因此在非并发情况下,使用map相比使用sync.Map会有更好的性能。
- 列表是一种非连续存储的容器,由多个节点组成,节点通过一些变量记录彼此之间的关系。列表有多种实现方法,如单链表、双链表等。
- 在Go语言中,将列表使用container/list包来实现,内部的实现原理是双链表。列表能够高效地进行任意位置的元素插入和删除操作。
- list的初始化有两种方法:New和声明。两种方法的初始化效果都是一致的。
- 列表与切片和map不同的是,列表并没有具体元素类型的限制。因此,列表的元素可以是任意类型。这既带来遍历,也会引来一些问题。给一个列表放入了非期望类型的值,在取出值后,将interface{}转换为期望类型时将会发生宕机。
- Go语言规定与if匹配的左括号“{”必须与if和表达式放在同一行,如果尝试将“{”放在其他位置,将会触发编译错误。与else匹配的“{”也必须与else在同一行,else也必须与上一个if或else if的右边的大括号在一行。
- 通过for range遍历的返回值有一定的规律:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
- 对map遍历时,遍历输出的键值是无序的,如果需要有序的键值对输出,需要对结果进行排序。
- 在Go语言中的switch,不仅可以基于常量进行判断,还可以基于表达式进行判断。
- 函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率。
- Go语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。
- Go语言的函数属于“一等公民”(first-class),也就是说:
- 函数本身可以作为值进行传递。
- 支持匿名函数和闭包(closure)。
- 函数可以满足接口。
- golang中,函数可以为单返回值或多返回值,当函数为多返回值时,需要使用括号对返回值列表进行约束;函数的返回值可以为不同类型,并且可以在定义时指定返回值的变量名,并且在函数体中进行显式赋值,在函数内return时,对于已经显式赋值的返回值,在return时可以不带参数,也可以将部分返回值放在return语句中,此时return语句需要写出完整的返回值列表。
- 同一种类型返回值和命名返回值两种形式只能二选一,混用时将会发生编译错误。
- 函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
- Go语言中传入和返回参数在调用和返回时都使用值传递,这里需要注意的是指针、切片和map等引用型对象指向的内容在参数传递中不会发生复制,而是将指针进行复制,类似于创建一次引用。
5.6 闭包(Closure)——引用了外部变量的匿名函数
- 闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。
- 一个函数类型就像结构体一样,可以被实例化。函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”。函数是编译期静态的概念,而闭包是运行期动态的概念。
- 闭包(Closure)在某些编程语言中也被称为Lambda表达式。闭包对环境中变量的引用过程,也可以被称为“捕获”,在C++ 11标准中,捕获有两种类型:引用和复制,可以改变引用的原值叫做“引用捕获”,捕获的过程值被复制到闭包中使用叫做“复制捕获”。在Lua语言中,将被捕获的变量起了一个名字叫做Upvalue,因为捕获过程总是对闭包上方定义过的自由变量进行引用。闭包在各种语言中的实现也是不尽相同的。在Lua语言中,无论闭包还是函数都属于Prototype概念,被捕获的变量以Upvalue的形式引用到闭包中。C++与C#中为闭包创建了一个类,而被捕获的变量在编译时放到类中的成员中,闭包在访问被捕获的变量时,实际上访问的是闭包隐藏类的成员。
- 被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。
5.7 可变参数——参数数量不固定的函数形式
- 可变参数一般被放置在函数列表的末尾,前面是固定参数列表,当没有固定参数时,所有变量就将是可变参数。
- v为可变参数变量,类型为[]T,也就是拥有多个T元素的T类型切片,v和T之间由“...”即3个点组成。
- T为可变参数的类型,当T为interface{}时,传入的可以是任意类型。
5.8 延迟执行语句(defer)
- Go语言的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
- 延迟调用是在defer所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。
5.8.2 使用延迟执行语句在函数退出时释放资源
- 处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。
- defer语句正好是在函数退出时执行的语句,所以使用defer能非常方便地处理资源释放问题。
5.10 宕机(panic)——程序终止运行
- 宕机不是一件很好的事情,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到ATM机蓝屏一样。但是,如果在损失发生时,程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命。因此,宕机有时是一种合理的止损方法。
- Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时地发现错误,同时减少可能的损失。Go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。
- 当panic()触发的宕机发生时,panic()后面的代码将不会被运行,但是在panic()函数前面已经运行过的defer语句依然会在宕机发生时发生作用。
5.11 宕机恢复(recover)——防止程序崩溃
- 无论是代码运行错误由Runtime层抛出的panic崩溃,还是主动触发的panic崩溃,都可以配合defer和recover实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。
- panic和recover的关系,panic和defer的组合有如下几个特性。
- 有panic没recover,程序宕机。
- 有panic也有recover捕获,程序不会宕机。执行完对应的defer后,从宕机点退出当前函数后继续执行。
- 在panic触发的defer函数内,可以继续调用panic,进一步将错误外抛直到程序整体崩溃。如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。
第6章 结构体(struct)
- Go语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。
- Go语言中的类型可以被实例化,使用new或“&”构造的类型实例的类型是类型的指针。
- 结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:
- 字段拥有自己的类型和值。
- 字段名必须唯一。
- 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
6.2 实例化结构体——为结构体分配内存并初始化
- 结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。因此必须在定义结构体并实例化后才能使用结构体的字段。
- 实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。
- 创建指针类型的结构体
- Go语言中,还可以使用new关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。
- 在C/C++语言中,使用new实例化类型后,访问其成员变量时必须使用“->”操作符。在Go语言中,访问结构体指针的成员变量时可以继续使用“.”。这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将ins.Name形式转换为(*ins).Name。
- 在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。
第7章 接口(interface)
- Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer,有关闭功能的接口叫Closer等。
7.2.1 接口被实现的条件一:接口的方法与实现接口的类型方法格式一致
- 当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
第9章 并发
- 并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。本章讲解的并发含义属于多线程编程。
- Go语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go语言的并发通过goroutine特性完成。goroutine类似于线程,但是可以根据需要创建多个goroutine并发工作。goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度完成。
- Go程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个goroutine,一个goroutine必定对应一个函数。
- 所有goroutine在main()函数结束时会一同结束。
- goroutine虽然类似于线程概念,但是从调度性能上没有线程细致,而细致程度取决于Go程序的goroutine调度器的实现和运行环境。
- 并发和并行之间的区别。
- 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
- 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
- 两个概念的区别是:任务是否同时执行。
- 同步——保证并发环境下数据访问的正确性
- Go程序可以使用通道进行多个goroutine间的数据交换,但这仅仅是数据同步中的一种方法。通道内部的实现依然使用了各种锁,因此优雅代码的代价是性能。在某些轻量级的场合,原子访问(atomic包)、互斥锁(sync.Mutex)以及等待组(sync.Wait Group)能最大程度满足需求。
第10章 反射
- 反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
- Go程序在运行期使用reflect包访问程序的反射信息。
10.1 反射的类型对象(reflect.Type)
- 在Go程序中,使用reflect.Type Of()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
有疑问加站长微信联系(非本文作者)