方法

尽管没有被大众所接受的明确的OOP的定义, 从我们的理解来讲, 一个对象其实也就是一个简单的值或者一个变量, 这个对象中会包含一些方法, 而一个方法则是一个一个和特殊类型关联的函数. 一个面向对象的程序会使用方法来表达其属性和对应的操作, 这样使用这个对象的用户就不需要直接去操作对象, 而是借助方法来做这些事情.

方法声明

在函数声明时, 在其名字之前放上一个变量, 即是一个方法. 这个附加的参数会将该函数附加到这种类型上, 即相当于为这种类型定义了一个独占的方法.

函数名字前的变量称为方法的接受器, 早期的面向对象语言留下的遗产将调用一个方法称为”向一个对像发送消息”.

Go语言中, 我们并不会像其它语言那样用this或者self作为接收器; 我们可以任意选择接收器的名字. 由于接受器的名字会经常被使用到, 所以保持其在方法间传递时的一致性或简短性是不错的注意. 这里的建议是可以使用其类型的第一个字母的小写.

方法可以被声明到任意类型, 只要不是一个指针类型或者interface.

基于指针对象的方法

当接收器变量本身比较大或我们希望更新接受器对象, 我们可以用其指针而不是对象来声明方法.

在声明方法时, 如果一个类型名本身是一个指针的话, 是不允许其出现在接收器中的.

不管你的methodreceiver是指针类型还是非指针类型, 都是可以通过指针/非指针类型进行调用的, 编译器会帮你做类型转换.

在声明一个methodreceiver该是指针还是非指针类型时, 你需要考虑两方面的内部, 第一方面是这个对象本身是不是特别大, 如果声明为非指针变量时, 调用会产生一次拷贝; 第二方面是如果你用指针类型作为receiver, 那么你一定要注意, 这种指针类型指向的始终是一个块内存地址.

Nil也是一个合法接收器类型

就像一些函数允许nil指针作为参数一样, 方法理论上也可以用nil作为其接收器, 尤其当nil对于对象来说是合法的零值, 比如map或者slice.

当你定义一个允许nil作为接收器值的方法的类型时, 在类型前面的注释中指出nil变量代表的意义是很有必要的.

通过嵌入结构体来扩展类型

嵌套类型, 不是is a关系, 而是has a关系, 内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法.

在类型中内嵌的匿名字段也可能是一个命名类型的指针, 这种情况下字段和方法会被间接地引入到当前的类型(访问需要通过该指针指向的对象去取). 添加这一层间接关系让我们可以共享通用的结构并动态地改变对象间的关系.

方法只能在命名类型或指向类型的指针上定义, 但是多亏了内嵌, 有些时候我们给匿名struct类型来定义方法也有了手段.

方法值和方法表达式

我们经常选择一个方法, 并且在同一个表达式里执行, 比如常见的p.Distance()形式, 实际上将其分成两步来执行也是可能的. p.Distance叫作”选择器”, 选择器会返回一个方法”值”, 一个将方法Point.Distance绑定到特定接收器变量的函数. 这个函数可以不通过指定其接收器即可被调用; 即调用时不需要指定接收器, 只要传入函数的参数即可.

在一个包的API需要一个函数值, 且调用方希望操作的是某一个绑定了对象的方法的化, 方法”值”会非常实用.

T是一个类型时, 方法表达式可能会写作T.f或者(*T).f, 会返回一个函数”值”, 这种函数会将其第一个参数用作接收器, 所以可以用不写选择器的方式来进行调用.

实例: BIT数组

Go语言里的集合一般会用map[T]bool这种形式来表示, T代表元素类型.

一个bit数组通常会用一个无符号数或者称之为”字”的slice来表示, 每个元素的每一位都表示集合里的一个值. 当集合的第i位被设置时, 我们才说这个集合包含元素i.

封装

一个对象的变量或者方法如果对调用方是不可见的话, 一般就定义为”封装”. 封装有时候也叫作信息隐藏, 同时也是面向对象编程最关键的一个方面.

Go语言只有一种控制可见性的手段: 大写手写字母的标识符会从定义它们的包中被导出, 小写字母的则不会. 这种限制包内成员的方式同样适用于struct或者一个类型的方法. 因而如果我们想要封装一个对象, 我们必须将其定义为一个struct.

这种基于名字的手段使得语言中最小的封装单元是package, 而不是像其它语言一样的类型. 一个struct类型的字段对同一个包的所有代码都有可见性, 无论你的代码是写在一个函数还是一个方法里.

函数

函数声明

每个函数声明都包含一个名字, 一个形参列表, 要给可选的返回列表以及函数体:

1
2
3
func name(parameter-list) (result-list) {
body
}

返回值也可以像形参一样命名. 这个时候, 每个命名的返回值会声明为一个局部变量, 并根据变量类型初始化为相应的0值.

函数的类型称作函数签名. 当两个函数拥有相同的形参列表和返回列表时, 认为这两个函数的类型或签名是相同的.

每一次调用函数都需要提供实参来对应函数的每一个形参. Go语言没有默认参数值的概念, 也没有任何方法可以通过参数名指定形参.

实参是按值传递的, 所以函数接收到的是每个实参的副本; 修改函数的形参变量并不会影响到抵用者提供的实参. 然后就, 如果提供的实参包含引用类型, 比如指针, slice, map, 函数或者通道, 那么当函数使用形参变量时就有可能会间接修改实参变量.

如果函数声明没有函数体, 那说明这个函数使用了除Go以外的语言实现.

递归

许多编程语言使用固定长度的函数调用栈; 大小在64KB2MB之间. 递归的深度会受限于固定长度的栈大小, 所以但进行深度递归调用时必须谨慎防栈溢出. 固定长度的栈甚至会造成一定的安全隐患. 相比固定长的栈, Go语言的实现是使用了可变长度的栈, 栈的大小会随着使用而增长, 可达到1GB左右的上限.

多返回值

Go语言的垃圾回收机制将回收未使用的内存, 但不能指望它会释放未使用的操作系统资源, 比如打开的文件以及网络连接. 必须显式地关闭它们.

返回一个多值结果可以是调用另一个多字返回的函数.

一个多值调用可以作为单独的实参传递给拥有多个形参的函数中.

良好的名称可以使得返回值更加有意义. 尤其在一个函数返回多个结果且类型相同时.

一个函数如果有命名的返回值, 可以省略return语句的操作数, 这称为裸返回. 因为能直观看出返回值, 应该保守使用裸返回.

错误

如果当函数函数调用发生错误时返回一个附加的结果作为错误值, 习惯上将错误值作为最后一个结果返回. 如果错误只有一种情况, 结果通常设置为布尔类型. 更多时候, 尤其对于I/O操作, 错误的原因可能多种多样, 而调用者则需要一些详细的信息. 在这种情况下, 错误的结果类型往往是error.

error是内置的接口类型. 非空的错误类型有一个错误消息字符串, 可以通过调用它的Error方法或者通过调用fmt.Println(err)fmt.Print("%v", err)直接输出错误消息.

Go中, 函数运行失败时会返回错误信息, 这些错误信息被认为时一种预期的值而非异常, 这使得Go有区别于那些将函数运行失败看作异常的语言. 虽然Go有各种异常机制, 但这些机制仅被使用在处理那些未被预料到的错误, 即bug, 而不是那些键壮程序中应该被避免的程序错误.

错误处理策略

当一个函数调用返回一个错误时, 调用者应负责检查错误并采取合适的处理应对.

首先也还是最常见的情形是将错误传递下去, 使得子例程中发生的错误变为主调例程的错误. 我们为原始错误消息不断添加额外的上下文信息来建立一个可读的错误描述. 当错误最终被程序的main函数处理时, 它应该能够提供一个根本问题到总体故障的清晰因果链.

因为错误消息频繁地串联起来, 所以消息字符串首字母不应该大写, 而且应该避免换行.

一般而言, 被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者, 调用者需要添加一些错误信息中不包含的信息.

第二种错误处理策略. 对于不固定或者不可预测的错误, 在短暂的间隔后对操作进行重试是合乎情理的, 超出一定的重试次数和限定的时间后在报错退出.

第三, 如果依旧不能顺利进行下去, 调用者能够输出错误然后优雅地停止程序, 但一般这样的处理应该留给主程序部分. 通常库函数应当将错误传递给调用者, 除非这个错误表示一个内部一致性错误, 这意味着库内部存在bug.

一个更方便的方法是通过调用log.Fatalf实现相同的效果. 就和所有日志函数一样, 它默认会将时间和日期作为前缀添加到错误消息前.

第四, 在一些错误情况下, 只记录下错误信息然后程序继续运行. 可以使用log.Printf或者直接输出到标准错误流. log包中所有函数会为没有换行符的字符串增加换行符.

第五, 在某些罕见的情况下我们可以直接安全地忽略掉整个错误. 要习惯考虑到每个函数调用可能发生的出错情况, 当你有意地忽略一个错误的时候, 清晰地注释一下你的意图.

如果某个错误会导致函数返回, 那么成功的逻辑代码不应该放在else语句中, 而应直接放在函数体中.

文件结尾错误(EOF)

Io包保证任何由文件结束引起的读取失败都返回同一个错误(io.EOF).

函数值

Go中, 函数被看作第一类值: 函数像其他值一样, 拥有类型, 可以被赋值给其他变量, 传递给其他变量, 传递给函数, 从函数返回. 对函数值的调用类似函数调用.

函数类型的零值是nil, 调用一个空的函数变量将导致panic错误. 函数变量可以和nil比较, 但它们本省不可比较, 所以不可以相互进行比较或者作为键值出现在map中.

匿名函数

命名函数只能在包级别的作用域进行声明, 但我们能够使用函数字面量在任何表达式内指定函数变量. 函数字面量就像函数声明, 但在func关键字后面没有函数名称. 它是一个表达式, 它的值称作匿名函数.

函数字面量在我们需要的时候才定义.

更重要的是, 以这种方式定义的函数能够获取到整个词法环境, 因此里层的函数可以使用外层函数中的变量.

函数值不仅仅是一串代码, 还记录了状态. 存在变量引用. 这就是函数值属于引用类型和函数值不可比较的原因. Go使用闭包技术实现函数值, Go程序员也把函数值叫做闭包.

当一个匿名函数需要进行递归, 必须先声明变量然后将匿名函数赋值给这个变量.

警告: 捕获迭代变量

为什么在循环体内将循环变量赋给一个新的局部变量, 而不是直接使用循环变量.

问题的原因在于循环变量的作用域. for循环引入了新的语法块, 循环变量在这个词法块中被声明. 在该循环中生成的所有函数值都共享相同的函数变量. 需要注意, 函数值中记录的是循环变量的内存地址, 而不是循环变量某一时刻的值.

这样的隐患不仅仅存在于使用rangefor循环中. 在普通for循环中也存在.

go语句和defer语句中会经常遇到此类问题. 者不是godefer本身导致的, 而是因为它们都会等待循环结束后, 在执行函数值.

变长函数

变长函数被调用的时候可以有可变的参数个数. 最令人熟知的例子就是fmt.Printf与其变种. Prinf需要在开头提供一个固定的参数, 后续便可以就收任意数目的参数.

在参数列表最后的类型名称之前使用省略号...表示声明一个变长函数, 调用这个函数的时候可以传递该类型任意数目的参数.

调用者隐式地创建一个数组, 并将原始参数复制到数组中, 再把数组的一个切片作为参数传递给被调用函数. 如果原始参数已经是切片类型, 我们如何传值? 只需要在slice后加....

虽然在可变参数函数内部, 尽管可变参数的行为很像切片类型, 但实际上, 可变参数函数和以切片作为参数的函数是不同的.

可变参数函数经常被用于格式化字符串. interface{}表示函数的最后一个参数可以接收任意类型.

延迟函数调用

语法上, 一个defer语句就是一个普通的函数或方法调用, 在调用之前加上关键字defer. 函数和参数表达式会在语句执行时求值, 但是无论是正常情况下, 执行reutrn或函数执行完毕, 还是不正常的情况下, 比如发生宕机, 实际的调用推迟到包含defer语句的函数结束后才执行. defer语句没有限制使用次数; 执行的时候以调用defer语句的顺序倒序进行.

defer语句经常被用于处理成对的操作, 如打开, 关闭, 连接, 断开连接, 加锁, 释放锁. 通过defer机制, 无论函数逻辑多复杂, 都能保证在任何执行路径下, 资源被释放. 释放资源的defer应该直接跟在请求资源的语句后.

调试复杂程序时, defer机制也常被用于记录何时进入和退出函数. defer后面的函数返回一个函数. 返回函数的函数会在入口执行, 返回的函数会在出口执行.

我们知道, defer语句中的函数会在return语句更新返回值变量后在执行, 又因为在函数定义的匿名函数可以访问函数包括返回变量在内的所有变量, 所以, 对匿名函数采用defer机制, 可以使其观察函数的返回值. 被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值.

在循环体中的defer语句需要特别注意, 因为只有在函数执行完毕之后, 这些函数被延迟的函数才会被执行, 有些需要在一次循环结束后关闭, 这时可以把循环体变成一个函数, 在这个函数中使用defer.

如果试图使用延迟调用去关闭一个本地文件就会有些问题. 在许多文件系统中, 尤其是NFS, 写错误往往不是立即返回而是推迟到文件关闭的时候. 如果没有检查文件关闭时的反馈信息, 可能会导致数据丢失, 而我们还认为写入操作成功.

Panic异常

Go的类型系统会在编译时捕获很多错误, 但有些错误只能在运行时检查, 如数组访问越界, 空指针引用等. 这些运行时错误会引起panic异常.

一般而言, 当panic异常发生时, 程序会中断运行, 并立即执行在该goroutine中被延迟的函数(defer机制). 随后, 程序崩溃并输出日志信息. 日志信息包括panic value和函数调用的堆栈很跟踪信息. panic value通常是某种错误信息. 对于每个goroutine, 日志信息中都会有与之相对的, 发生panic时的函数调用堆栈跟踪信息.

不是所有的panic异常都来自运行时, 直接调用内置的panic函数也会引发panic异常; panic函数接受任何值作为参数. 当某些不应该发生的场景发生时, 我们就应该调用panic.

断言函数必须满足的前置条件时明智的做法, 但者很容易被滥用. 除非你能提供更多的错误信息, 或者能更快的发现错误, 否则不需要使用断言, 编译器在运行时会帮你检查代码.

虽然Gopanic机制类似于其他语言的异常, 但panic的适用场景有些不同. 由于panic会引起程序的崩溃, 因此panic一般用于严重的错误, 如程序内部的逻辑不一致. 勤奋的程序员认为任何崩溃都表明程序中存在漏洞, 所以对于大部分漏洞, 我们应该使用Go提供的错误机制, 而不是panic, 尽量避免程序崩溃. 在健壮的程序中, 任何可以预料到的错误, 如不正确的输入, 错误的配置或是失败的I/O操作都应该被优雅地处理, 最好的处理方式, 就是使用Go的错误机制.

当调用者明确的知道正确的输入不会引起函数错误是, 要求调用者检查这个错误时不必要和累赘的. 我们应该假设函数的输入一直合法, 就如前面的断言一样: 当调用者输入了不该出现的输入时, 触发panic异常. 函数名中的Must前缀是一种针对此类函数的命名约定.

Recover捕获异常

通常来说, 不应该对panic异常做任何处理, 但有时, 也许我们可以从异常中恢复, 至少可以在程序崩溃前, 做一些操作.

如果在deferred函数中调用了内置函数recover, 并且定义该defer语句的函数发生了panic会使程序从panic中恢复, 并返回panic value. 导致panic异常的函数不会继续运行, 但能正常返回. 在未发生panic时调用recover, recover会返回nil.

不加区分的恢复所有的panic异常, 不是可取的做法; 因为在panic之后, 无法保证包级变量的状态仍然和我们预期一致.

作为被广泛遵守的规范, 你不应该试图去恢复其他包引起的panic. 公有的API应该将函数的运行失败作为error返回, 而不是panic. 同样的, 你不应该恢复一个由他人开发的函数引起的panic, 比如说调用者传入的回调函数, 因为你无法确保这样做是安全的.

只恢复应该恢复的panic异常, 此外, 这些异常所占的比例应该尽可能的低. 为了标识某个panic是否应该被恢复, 我们可以将panic value设置成特殊类型. 在recover时对panic value进行检查, 如果发现panic value是特殊类型, 就将这个panic作为error, 如果不是, 则按照正常的panic进行处理.

常用函数

1
2
3
4
5
6
7
fmt.Printf("%*s", 数量, "字符")

# 字符串排序
sort.String()

# runtime包允许程序员输出堆栈信息
runtime.Stack()

复合数据类型

复合数据类型是由基本数据类型以各种方式组合而构成的. 数组和结构体都是聚合类型, 它们的值由内存中的一组变量构成. 数组的元素具有相同的类型, 而结构体中元素类型则可以不同. 数组和结构体的长度都是固定的, 反之, slicemap都是动态数据结构, 它们的长度都是元素添加到结构体中时可以动态增长.

数组

数组是具有固定长度且拥有零个或多个相同数据类型元素的序列. 由于数组的长度固定, 所以在Go里面直接很少使用. slice的长度可以增长和缩短, 在很多场合下使用得更多.

Go内置的函数len可以返回数组中元素元素的个数.

默认情况下, 一个新数组中的元素初始值为元素类型的零值, 对于数字来说, 就是0. 也可以使用数组字面量根据一组值来初始化一个数组. 在数组字面中, 如果使用...代替数组长度, 那么数组的长度由初始化数组的个数决定.

数组长度是数组类型的一部分, 所以[3]int[4]int是两种不同的数组类型. 数组的长度必须是常量表达式, 也就是说, 这个表达式的值在程序编译时就可以确定.

数组可以通过索引赋值, 没有指定值的索引位置的元素默认被赋予数组元素类型的零值.

如果一个数组的元素类型是可比较的, 那么这个数组也是可比较的, 这样我们就可以直接使用==操作符来比较两个数组, 比较的结果是两边元素的值是否完全相同. 只有长度相同的数组可以比较, 如果长度不同, 会认为是两个不同类型的数组.

Go语言中, 把数组和其他的类型都看成值传递, 对参数的任何修改都发生在复制的数组上. 而在其语言中, 数组时隐式地使用了引用或指针传递.

slice

slice表示一个拥有相同类型元素的可变长度的序列. slice通常写成[]T, 其中元素的类型都是T; 它看上去像没有长度的数组类型.

一个slice是一个轻量级的数据结构, 提供了访问数组子序列元素的功能, 而且slice的底层确实引用了一个数组对象. 一个slice有三个属性: 指针, 长度和容量. 指针指向数组的第一个可以从slice中访问的元素, 这个元素并不一定是数组的第一个元素. 长度是指slice中元素的个数, 它不能超过slice的容量. 容量的大小通常是从slice的起始元素到底层数组的最后一个元素间元素的个数. Go的内置函数lencap用来返回slice的长度和容量.

一个底层数组可以对应多个slice, 这些slice可以引用数组的任何位置, 彼此之间的元素还可以重叠.

slice操作符s[i:j](其中$0 \le i \le j \le cap(s)$) 创建了一个新的slice, 这个新的slice引用了序列s中ij-1索引位置的所有元素, 这里的s既可以是数组或是指向数组的指针, 也可以是slice.

如果切片操作超出了cap(s)的上限将导致一个panic异常, 但是超出了len(s)则是意味着扩展了slice, 因为新的slice的长度会变大.

因为slice包含了指向数组元素的指针, 所以将一个slice传递给函数的时候, 可以在函数内部修改底层数组的元素.

slice字面量看上去和数组字面量很像, 都是用逗号分隔并用花括号扩起来的一个元素序列, 但是slice没有指定长度. 这种隐式区别的结果分别是创建有固定长度的数组和创建指向数组的slice. 和数组一样, slice也按照顺序指定元素, 也可以通过索引了指定元素, 或者两种结合.

和数组不同的是, slice无法做比较, 因此不能用==来测试两个slice是否拥有相同的元素. 标准库里面提供了高度优化的函数bytes.Equal来比较两个字节slice. 但是对于其他类型的slice, 我们必须自己写函数来比较.

slice唯一允许的比较操作是和nil做比较. slice类型的零值是nil. 值是nilslice没有对应的底层数组. 值为nikslice长度和容量都是零, 但是也有非nilslice长度和容量都是零, 例如[]int{}make([]int,3)[3:]. 对于任何类型, 如果它们的值可以nil, 那么这个类型的值可以使用一种转换表达式, 例如[]int(nil).

所以, 如果想检查一个slice是否为空, 那么使用len(s) == 0, 而不是s == nil, 因为s != nil情况下, slice也可能是空.

内置函数make可以创建一个具有指定元素类型, 长度和容量的slice. 其中容量参数可以省略, 在这种情况下, slice的长度和容量相等.

1
2
make([]T, len)
make([]T, len, cap) // 和 make([]T, cap)[:len]功能相同

深度研究一下, 其实make创建了一个无名数组并返回了它的一个slice; 这个数组仅可以通过slice来访问. 在上面的第一行代码中, 所返回的slice引用了整个数组. 在第二行代码中, slice只引用了数组的前len个元素, 但是它的容量和数组的长度, 这为未来的slice元素留出空间.

append函数

内置函数append用来将元素追加到slice的后面.

更新slice变量不仅对调用append函数是必要的, 实际上对应任何可能导致长度, 容量或底层数组变化的操作都是必要的.

函数参数声明中的省略号...表示该函数可以接受可变参数列表, 实参后面的省略号表示将一个slice转换为参数列表.

slice内存技巧

输入的slice和输出的slice共享一个底层数组. 这可以避免分配另一个数组, 不过原来的数据可能会被覆盖.

map

Go语言中, map是散列表的引用, map的类型是map[K]V, 其中KV是字典的键和值对应的数据类型. 键的类型K, 必须可以通过操作符==来进行比较的数据类型, 所以map可以检测某一个键是否已经存在. 虽然浮点型是可以比较的, 但是比较浮点型的相等性不是一个好主意.

内置函数make可以用来创建一个map:

1
args := make(map[string]int) // 创建一个从string到int的map

也可以使用map的字面量来创建一个带初始键值对元素的字典:

1
2
3
4
5
6
7
8
9
args := map[string]int{
"alice": 31,
"charlie": 34,
}

等价于
args := make(map[string]int) // 等价于map[string]int{}
args["alice"] = 31
args["charlie"] = 34

可以使用内置函数delete来从字典中根据键移除一个元素:

1
delete(args, "alice")

即使键不在map中, 上面的操作也都是安全的. map使用给定的键来查找元素, 如果对应的元素不存在, 就返回值类型的零值.

快捷赋值方式(如x+=y和x++)对map中元素同样适用.

我们无法获得map元素的地址的一个原因是map的增长可能会导致已有元素元素被重新散列到新的存储位置, 这样就可能使得获得的地址无效.

可以使用for循环(结合range关键字)来便历map中所有的键和对应的值.

如果需要按照某种顺序来遍历map中的元素, 我们必须显式地来给键排序, 然后再取值. 如果一开始就知道了元素的个数, 直接指定一个slice的长度会更加高效.

map类型的零值是nil, 大多数的map操作都可以安全地在map的零值nil上执行, 包括查找元素, 删除元素, 获取map元素的个数(len), 执行range循环, 应为这和空map的行为一致. 但是向零值map中设置元素会导致错误.

判断一个元素知否在map中, 还是值是零值.

1
if age, ok := args["bob"]; !ok { /* ... */}

slice一样, map不可比较, 唯一合法的比较就是和nil做比较. 为了判断两个map是否拥有相同的键和值, 必须写一个循环.

Go没有提供集合类型, 但既然map的键都是唯一的, 就可以用map来实现这个功能.

map的值类型本身可以是复合数据类型, 例如mapslice.

结构体

结构体是将零个或多个任意类型的命名变量组合在一起的聚合数据类型. 每个变量都叫做结构体的成员.

结构体可以通过点号的方式来访问成员. 点号同样可以用在结构体指针上.

成员变量的顺序对于结构体同一性很重要. 顺序不一样, 结构体就不一样.

命名结构体类型S不可以定义一个拥有相同结构体类型S的成员变量, 也就是一个聚合类型不可以包含它自己(同样的限制对数组也是适用). 但是S中可以定义一个S的指针类型, 即*S, 这样我们就可以创建一些递归数据结构, 比如链表和树.

结构体的零值由结构体成员的零值组成. 通常情况下, 我们希望零值是一个默认自然的, 合理的值.

任何没有成员变量的结构体称为空结构体, 写作struct{}. 它没有长度, 也不携带任何信息, 但是有时候会很有用.

结构体字面量

结构体类型的值可以通过结构体字面量来设置, 即通过设计结构体的成员变量来设置.

1
2
type Point struce{ X, Y int}
p := Point{1, 2}

有两种格式的结构体字面量. 第一中格式如上, 它要求按照正确的顺序, 为每个成员变量指定一个值, 这种格式一般只在定义结构体的包内部使用, 或者是在较小的结构体中使用, 这些结构体的成员排列比较规则. 第二种形式, 通过指定部分或全部成员变量的名称和值来初始化结构体变量. 如果在这种初始化方式中某个成员变量没有指定, 那么它的值就是该成员变量类型的零值. 因为了指定了成员变量的名字, 所以它们的顺序是无所谓的.

这两种初始化方式不可以混合使用, 另外也无法使用第一种初始化方式来绕过不可导出的变量无法在其他包中使用的规则.

如果考虑效率的haunted, 较大的结构体通常会使用指针的方式传入和返回.

如果要在函数内部修改结构体成员的话, 用指针传入是必须的; 因为在Go语言中, 所有的函数参数都是值拷贝传入的, 函数参数将不再是函数调用时的原始变量.

结构体比较

如果结构体的所有成员变量都是可以比较的, 那么这个结构体就是可比较的. 两个结构体的比较可以使用==或者!=. 其中==操作按照顺序比较两个结构体变量的成员变量.

结构体嵌套和匿名成员

Go允许我们定义不带名称的结构体成员, 只需要指定类型即可; 这种结构体成员称做匿名成员. 这个结构体成员的类型必须是一个命名类型或指向命名类型的指针. 正因为有了这种结构体嵌套的功能, 我们才能直接访问到我们需要的变量而不是指定一大串中间变量.

匿名成员的名称其实是对应类型的名字, 只是这些名字在点号访问时时可选的. 结构体字面值并没有简短表示匿名成员的语法.

因为”匿名成员”拥有隐式的名字, 所以你不能在一个结构体里面定义两个相同类型的匿名成员, 否则会引起冲突. 由于匿名成员的名字是由它们的类型决定的, 因此它们的可导出性也是由它们的类型决定的.

以快捷方式访问匿名成员的内部变量同样适用于访问匿名成员的内部方法. 因此, 外围的结构体类型获取的不仅是匿名成员的内部变量, 还有相关的方法.

JSON

javaScript对象表示法(JSON)是一种发送和接收格式化信息的标准. JSONJavaScript值的Unicode编码, 这些值包括字符串, 数字, 布尔值, 数组和对象. JSON里面的\Uhhhh转义数字来表示一个UTF-16编码(译注: UTF-16UTF-8一样是一种变长的编码, 但是有大端和小端之分).

文本和HTML模版

通常情况下, Printf函数就够用了, 但是有的情况下格式化会很复杂, 并且要求格式和代码彻底分离. 这个可以通过text/template包和/html/template包里面的方法来实现, 这两个包提供了一种机制, 可以将程序变量的值植入到文本或者是HTML模版中.

常用函数

1
2
3
4
5
6
7
8
9
10
11
// 解码UTF-8编码, 并返回三个值, 解码的字符, UTF-8编码中字节的长度和错误值. 这里唯一可能出现的错误是文件结束(EOF), 如果输入的不是合法的UTF-8字符, 那么返回的字符是unicode.ReplacementChar并且长度是1
in.ReadRune()

// 副词#使得%v用和`Go`语言类似的语法打印. 对于结构体类型来说, 将包含每个成员的名字
fmt.Printf("%#v", w)

# marshal编组, 将go对象编组为JSON对象, 这个是紧凑形式, 格式化方法, json.MarshlIndent(movies, "", " "), 只有可以导出的成员可以转换为JSON字段
json.marshal(movies)

# unmarshal解码, 将JSON转换为go对象. 通过合理地定义go的数据结构, 我们可以选择将哪部分JSON数据解码到结构体中, 哪些数据可以丢弃, 解码忽略大小写
json.Unmarsh

技巧

将一个slice左移n个元素的简单方法是连续调用reverse函数三次. 第一次反转前n个元素, 第二次反转剩下的元素, 最后一次对整个slice再做一次反转(如果将元素右移n个元素, 那么先做第三次调用)

基本数据

毫无疑问, 计算机底层全是位, 而实际操作则是基于大小固定的单元中的数值, 称为字, 这些值可解释为整数, 浮点数, 位集或内存地址等, 进而构成更大的聚合体, 以表示数据包, 像素, 文件, 诗集, 以及其他种种.

Go的数据类型分为四大类: 基础类型, 聚合类型, 引用类型和接口类型. 基础类型, 包括数字, 字符串和布尔型. 聚合类型包括, 数组和结构体. 引用类型, 包括指针, slice, map, 函数, 以及通道, 它们的共同点都是全部间接指向程序变量或状态, 这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝.

整型

Go语言的数值类型包括几种不同大小的整数, 浮点数和复数. 各种数值类型分别有自己的大小, 对正负号支持也各异.

Go同时具备有符号整数和无符号整数. 有符号整数分为四种大小: 8位, 16位, 32位, 64位, 用int8, int16, int32, int64表示, 对应的无符号整数是uint8, uint16, uint32, uint64.

此外还有两种类型intuint. 在特定平台上, 其大小和原生的有符号数\无符号整数相同, 或等于该平台上运算效率最高的值. int是目前使用最广泛的数值类型. 者两种类型大小相等, 都是32位或64位, 版不能认为它们就一定是32位, 或一个就是64位; 即使在同样的硬件平台上, 不同的编译器也可能选用不同的大小.

rune类型是int32类型的同义词, 常常用于指明一个值是Unicode码点. 这两个名称可互换使用. 同样, byte类型是uint8类型的同义词, 强调一个值是原始数据, 而非量值.

最后, 还有一种无符号整数uintptr, 其大小并不明确, 但足以完整存放指针. uintptr类型仅仅用于底层编码, 例如在Go程序与C程序库或操作系统接口相互交互的地方.

int, uintuintptr都是不同类型的兄弟类型, 也必须进行显式地类型操作转换.

有符号数以补码表示.

算术运算符+, -, *, /可应用于整数, 浮点数和复数, 而取模运算符%仅能用于整数. 取模运算符%的行为因编程语言而异. 就Go而言, 取模余数的正负号总是与被除数一致, 于是-5%3-5%-3都得-2. 除法运算(/)的行为取决于操作数是否都为整形, 整数相除, 商会舍弃掉小数部分.

不论是有符号数还是无符号数, 若表达算术运算结果所需的位超出该类型的范围, 就称为溢出. 溢出的高位部分会无提示地丢弃.

实际上, 全部基本类型的值(布尔值, 数值, 字符串)都可以比较, 这意味着两个相同类型的值可用==!=运算符比较. 整数, 浮点数和字符串还能根据比较运算符排序.

如果作为二元运算符, 运算符^表示按位”异或”(XOR); 若作为一元前缀运算符, 则它表示按位取反或按位取补, 运算结果就是操作数逐位取反. 运算符&^是按位清除(AND NOT): 表达式z=x&^y中, 若y的某位是1, 则z的对应位等于0; 否则, 他就等于x的对应位.

在位移运算符x<<nx>>n中, 操作数你决定位移量, 而且n必须为无符号型; 操作数x可以是有符号型也可以是无符号型. 算术上, 左移运算x<<n等价于x乘以$2^n$; 而右移运算x>>n等价于x除以2^n, 向下取整.

左移以0填补右边空位, 无符号数右移同样以0填补左边空位, 但是有符号数的右移操作是按符号位的值填补空位. 因此, 当进行位运算的时候, 尽量使用无符号数.

因此, 无符号整数往往只用于位运算符和特定算术运算符, 如实现位集时, 解析二进制格式的文件, 或散列和加密. 一般而言, 无符号整数极少用于表示非负值.

通常, 将某种类型的值转换成另一种, 需要显式转换. 对于算术和逻辑(不含移位)的二元运算符, 其操作数的类型必须相同. 虽然这有时会导致表示式相对冗长, 但是一整类错误得以避免, 程序也更容易理解.

浮点型转成整形, 会舍弃小数部分, 趋零截尾(正值先下取整, 负值先上取整). 如果有些转换的操作数的值超出了目标类型的取值范围, 就应当避免这种转换, 因为其行为依赖具体实现.

任何大小的整数字面值都能写成十进制数, 也能写成八进制数(以0开头), 或十六进制(以0x或0x)开头.当前, 八进制数似乎仅有一种用途, 表示POSIX文件系统的权限, 而十六进制数广泛用于强调其位模式, 而非数值大小.

字符面值通过一对单引号直接包含字符. 用%c输出文字符号, 如果希望输出带有单引号则用%q.

浮点数

Go具有两种大小的浮点数float32float64. 这两个类型的值可从超细微到超宏大. math包给出了浮点值的极限.

十进制下, float32的有效数字大约是6位, float64的有效数字大约15位. 绝大多数情况下, 应优先选用float64, 因为除非格外小心, 否则float32的运算会迅速累积误差. 另外, float32能精确表示的正整数范围有限.

小数点前的数字可以省略(.707), 后面的也可以省去(1.). 非常小或非常大的数字最好使用科学计数法表示, 此方法在数量级指数前写字母eE.

使用Printf函数的%g输出, 该谓词会自动保持足够的精度, 并选择最简介的表示方式, 但是对于数据表, %e(有指数)或%f(无指数)的形式可能更合适.

除了大量常见的数学函数之外, math包还有函数用于创建和判断IEEE 754标准定义的特殊值: 正无穷大和负无穷大, 它表示超出最大许可值的数和除零的结果; 以及NAN, 它表示数学意义上无意义的运算结果.

math.isNaN函数判断其参数是否是非数值, math.NaN函数则返回非数值. 在数字运算中, 我们倾向于将NaN当作信号值, 不拿它来进行比较操作, 因为NaN和任何数的比较操作都为false(除了!=true). 在浮点数中, NaN, 正无穷大和负无穷大都不是唯一的, 每个都有非常多种的bit模式表示.

如果一个函数返回的浮点数结果可能失败, 最好的做法是用单独的标志报告失败.

复数

Go具有两种大小的复数complex64complex128, 二者分别由float32float64构成. 内置的complex函数根据给定的实部和虚部创建复数, 内置的real函数和imag函数则分别提取复数的实部和虚部.

如果一个浮点数面值或一个十进制整数面值后面跟着一个i, 例如3.14i2i, 它将构成一个复数的虚部, 复数的实部是0.

可以用==!=判断复数是否相等. 若两个复数的实部和虚部都相等, 则它们相等.

布尔值

bool型的值或布尔值只有两种可能: 真true和假false. 布尔值可以和&&||操作符结合, 并且会发生短路行为.

布尔值无法隐式转换成数值(如0或1), 反之也不行.

字符串

字符串是不可变的字节序列, 它可以包含任意数据, 包括0值字节. 习惯上, 文本字符串被解读成按UTF-8编码的Unicode码点(文字符号)序列.

内置的len函数返回字符串的字节数(并非文字符号的数目), 下标访问操作s[i]则取的第i个字符, 其中$0 \le i \le len(s)$. 试图访问许可范围以外的字节会触发panic异常.

子串生成操作s[i:j]产生一个新字符串, 内容取自原字符串的字节, 下标从i(含边界值)开始, 直到j(不含边界值). 结果的大小是j-i个字节, 若下标越界, 或者j的值小于i, 将触发panic异常.

操作数ij的默认值分别是0(字符串起始位置)和len(s)(字符串终止位置), 若省略ij, 或两者, 则取默认值.

加号(+)运算符连接两个字符串而生成一个新字符串.

字符串可以通过比较运算符做比较, 如==<; 比较运算按字节进行, 结果服从本身字典排序.

字符串值无法改变, 不可变意味着两个字符串能安全地共用同一段底层内存, 使得复制任何长度字符串的开销都很低廉.

字符串字面量

字符串的值可以直接写成字符串字面量, 形式上就是带双引号的字节序列. 源码中的字符串也可以包含十六进制或八进制的任意字节.

原生的字符串字面量的书写形式使用反引号而不是双引号. 原生的字符串字面量内, 转义序列不起作用; 实质内容与字面写法严格一致, 包括反斜杠和换行符, 因此, 在程序源码中, 原生的字符串字面量可以展开多行. 唯一的特殊处理是回车符会被删除(换行符会保留), 使得同一字符串在所有平台上的值都是相同的.

正则表达式往往含有大量反斜杠, 可以方便地写成原生的字符串字面量. 原生的字面量也适用于HTML模板, JSON字面量, 命令行提示信息, 以及需要多行文本表达的场景.

Unicode

Unicode是一种字符集, 它攘括了世界上所有文书体系的全部字符, 对它们各自赋予一个叫Unicode码点的标准数字. 在Go的术语中, 这些字符记号称为文字符号.

我们可以将文字符号的序列表示成int32值序列, 这种表示方式称作UTF-32UCS-4, 每个Unicode码点的编码长度相同, 都是32位.

UTF-8

UTF-8以字节为单位对Unicode码点做变长编码. 每个文字字符用1~4个字节表示, ASCII字符的编码仅占1个字节, 而其他常用的文字字符的编码只是2或3个字节.

变长编码的字符串无法按下标直接访问第n个字符. 同时UTF-8编码的顺序和Unicode码点的顺序一致, 因此可以直接排序UTF-8编码.

Go语言字符串字面值中的的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符. 有两种形式, \uhhhh表示16位码点值, \uhhhhhhhh表示32位码点值, 其中每个h代表要一个十六进制数值. 这两种形式都以UTF-8编码表示给定的码点.

1
2
3
4
5
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
'世' '\u4e16' '\U00004e16'

码点值小于256的文字符号可以写成单个十六进制转义的形式, 如A写成\x41; 而更高的码点值则必须使用\u\U转义. 这就导致'\xe4\xb8\x96\xe7\x95\x8c'不是合法的文字符号, 虽然这三个字节构成了某个有效的UTF-8编码的码点.

所幸, Gorange循环也适用于字符串, 按UTF-8隐式解码. 注意, 对于非ASCII文字符号, 下标增量大于1.

每次UTF-8解码器读入一个不合理的字节, 无论是显式调用utf8.DecodeRuneInString, 还是在range循环内隐式读取, 都会产生一个专门的Unicode字符\uFFFD替换它, 其输出通常是个黑色六角形或类似钻石的形状, 里面有个白色问号. 如果程序碰到这个文字符号值, 通常意味着, 生成字符串数据的系统上游部分在处理文本编码方面存在瑕疵.

UTF-8是一种分外便捷的交互格式, 而在程序内部使用文字字符类型可能更加方便, 因为它们大小一致, 便于在数组和slice中用下标访问.

[]rune转换作用于UTF-8编码字符串时, 返回该字符串的Unicode码点序列.

如果是将一个[]rune类型的Unicode字符slice或数组转为string, 则对它们进行UTF8编码.

将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串.

如果对应码点的字符是无效的, 则用\uFFFD无效字符作为替换.

字符串和字节slice

4个标准包对字符串操作特别重要: bytes, strings, strconvunicode.

strings包提供了许多函数, 用于搜索, 替换, 比较, 截断, 拆分和连接字符串.

bytes包也有类似的函数, 用于操作字节slice. 由于字符串不可变, 因此按增量方式构建字符串会导致多次内存分配和复制. 在这种情况下, 使用bytes.Buffer类型会更高效.

strconv包具备的函数, 主要用于转换布尔值, 整数, 浮点数为与之对应的字符串形式, 或者把字符串转换为布尔值, 整数, 浮点数, 另外还提供了双引号转义相关的转换.

unicode包具备判别文字符号特征的函数, 如IsDisgit, IsLetter, IsUpperIsLower. 每个函数一以单个文字符号值作为参数, 并返回布尔值. 若文字符号是英文字母, 转换函数(如ToUpperToLower)将其转换成指定的大小写. 上面所有函数都遵循Unicode标准对字母数字等的分类原则. strings包也有类似的函数, 函数名也是ToUpperToLower, 它们对原字符的每个字符做指定变换, 生成并返回一个新字符串.

path包和path/filepath包提供提取basename的函数, 用来操作文件路径等具有层次结构的名字. path包处理以斜杠/分段的路径字符串, 不分平台. 它不适用于处理文件名, 却适合其他领域, 像URL地址的路径部分. 相反地, path/filepath包根据宿主平台的规则处理文件名. 例如POSIX系统使用/, 而Microsoft Windows系统使用\.

一个字符串是包含的只读字节数组, 一旦创建, 是不可变的. 相比之下, 一个字节slice的元素则可以自由地修改.

从概念上讲, 一个[]byte(s)转换是分配一个新的字节数组用于保存字符串数据的拷贝, 然后引用这个底层的字节数组. 编译器的优化可以避免在一些场景下分配和复制字符串数据, 但是总的来说需要确保slice变量被修改的情况下, 原始的字符串也不会改变. 将一个字节slice转到字符串string(b)从操作则是构造一个字符串拷贝, 以确保字符串是只读的.

bytes包还提供了Buffer类型用于字节slice的缓存. 一个Buufer开始是空的, 但是随着string, byte[]byte等数据类型的写入可以动态增长, 一个bytes.Buffer变量并不需要处理化, 因为零值也是有效的.

当向bytes.Buffer添加任意字符的UTF0-8编码时, 最好使用bytes.BufferWriteRune方法, 但是WriteByte方法对写入类似[]ASCII字符则会更加有效.

字符串和数字的相互转换

要将整数转换成字符串, 一种选择是使用fmt.Sprintf, 另一种做法是用函数strconv.Itoa().

FomatIntFomatUint可以按不同进制位格式化数字.

如要要将要给字符串解析位整数, 可以使用strconv包的AtoiParseInt函数, 还有用于解析无符号整数的ParseUint函数.

有时候也会使用fmt.Scanf来解析输入的字符串和数字, 特别是当字符串和数字混在一行的时候, 它可以灵活处理不完整或不规则的输入.

常量

常量是一种表达式, 其可以保证在编译阶段就可以计算出表达式的值, 并不需要等到运行时, 从而使编译器以知晓其值. 所有常量本质上都是属于基本类型: 布尔型, 字符串或数字.

对于常量操作数, 所有数学运算, 逻辑运算和比较运算的结果依然是常量, 对常量的类型转换操作或以函数调用都是返回常量结果: len, cap, real, imag, complexunsafe.Sizeof.

因为它们的值是在编译期就确定的, 因此常量可以是构成类型的一部分, 例如用于指定数组类型的长度.

常量声明可以同时指定类型和值, 如果没有显式指定类型, 则类型根据右边的表达式推断. 若同时声明一组变常量, 除了第一项之外, 其他项在等号右侧的表达式都可以省略, 这意味着会复用前面一项的表达式以及类型.

常量生成器iota

常量的声明可以使用常量生成器iota, 它创建一系列相关值, 而不是逐个值显式写出. 常量声明中, iota0开始取值, 逐项加1.

无类型常量

常量可以任何基本数据类型, 也包括具名的基本类型, 但是许多常量并没有一个明确的基本类型. 编译器为这些没有明确的基础恶劣性的数字常量提供了比基础类型更高精度的算术运算; 你可以认为至少有256bit的运算精度. 这里有六种未明确类型的常量类型, 分别是无类型的布尔型, 无类型的整数, 无类型的字符, 无类型的浮点数, 无类型的复数, 无类型的字符串.

通过延迟明确常量的具体类型, 无类型的常量不仅可以提供更高的运算精度, 而且可以直接用于更多的表达式而不需要显式地类型转换.

只有常量可以是无类型的. 当一个无类型的常量被赋值给一个变量的时候, 无类型的常量将会被隐式转换为对应的类型, 如果转换合法的话.

对于一个没有显式类型的变量声明语法(包括短变量声明语法), 无类型的常量会被隐式转为默认的变量类型.

常用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 打印8位的表示
fmt.Printf("%08b\n", x)

# 将一个参数打印多次, #用来告知是打印前缀
fmt.Printf("%d %[1]o %#[1]o %[1]x %#[1]x %#[1]X")

# 将浮点数指定输出宽度和数值精度, g, f, e都可以这样
fmt.Printf("%8.3f")

# 给出文字字符的数量
utf8.RuneCountInString(s)

# 解码出文字字符和文字字符占的字节数
utf8.DecodeRuneInString()

# 十六进制数, 字节以空格分隔
fmt.Printf(% x, s)

程序结构

命名

Go语言中的函数名, 变量名, 常量名, 类型名, 语句标号和包名等所有的命名, 都遵循一个简单的命名规则: 一个名字必须以一个字母(Unicode字母)或下划线开头, 后面可以跟任意数量的字母, 数字或下滑线. 大写字母和小写字母是不同的: heapSortHeapSort是两个不同的名字. 名字的长度没有逻辑限制. 在习惯上, Go 语言程序员推荐使用驼峰式命名, 同时缩略词的中每个字母大小写保持一致.

Go语言中关键字有25个:

1
2
3
4
5
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

Go大约有30多个预定义的名字:

1
2
3
4
5
6
7
8
9
内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool type rune string error

内建函数: make len cap new append copy close delete
complex real imag panic recover

如果一个名字是在函数内部定义, 那么它就只能在函数内部有效. 如果是在函数外部定义, 那么将在当前包的所有文件中都可以访问. 名字的开头字母的大小写决定了名字在包外的可见性. 如果一个名字是大写字母开头的, 那么它将是导出的, 也就是说可以被外部的包访问.

声明

Go语言主要有四种类型的声明语句: var, const, typefunc, 分别对应变量, 常量, 类型和函数实体对象的声明. 程序中包一级的声明一般按照类型, 变量, 常量, 函数的声明语句.

变量

var声明语句可以创建要给特定类型的变量. 一般声明语法如下:

1
var 变量名称 类型 = 表达式

如果省略的是类型信息, 那么将根据初始化表达式来推导变量的类型信息. 如果初始化表达式被省略, 那么将用零值初始化该变量. 数值类型变量对应的零值是0, 布尔类型变量对应的零值是false, 字符串类型对应的零值是空字符串, 接口或引用类型(包括slice, map, chan和函数)变量对应的零值是nil. 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值.

在包级别声明的变量会在main入口函数执行前完成初始化, 局部变量将在声明语句被执行到的时候完成初始化.

短变量声明

在函数内部, 有一种称为短变量声明语句的形式可用于声明和初始化局部变量. 它以名字:=表达式形式声明变量, 变量的类型根据表达式来自动推导.

因为简单和灵活的特点, 短变量声明被广泛用于局部变量的声明和初始化. var形式的声明语句往往是用于需要显式指定变量类型的地方, 或者因为变量稍后会被重新赋值而初始值无关紧要的地方.

如果有一些已经在相同的词法域声明过了, 那么短变量声明语句对这些已经声明过的变量就只有赋值行为. 短变量声明语句中至少要声明一个新的变量.

指针

一个指针的值是是一个变量的地址. 并不是每一个值都会有一个内存地址, 但是对于每个变量必然有对应的内存地址. 通过指针, 我们可以直接读或更新对应变量的值, 而不需要知道变量的名字.

如果指针对应的数据类型是*int, 指针被称之为”指向int类型的指针”. 如果指针名字为p, 那么可以说”p指针指向变量x“ , 或者说”p指针保存了x变量的内存地址”.

任何类型的指针的零值都是nil. 如果p != nil测试为真, 那么p是指向某个有效变量. 指针之间也是可以进行相等测试的, 只有当它们指向同一个变量或全部都是nil时才相等.

Go语言中, 返回函数中局部变量的地址也是安全的.

new函数

另一个创建变量的方式是调用内建的new函数. 表达式new(T)将创建要给T类型的匿名变量, 初始为T类型的零值, 然后返回变量地址, 返回的指针类型为*T.

new创建变量和普通变量声明语句方式创建变量没有什么区别, 除了不需要声明一个临时变量.

请谨慎使用大小为0的类型, 因为如果类型的大小为0的话, 可能导致Go语言的自动垃圾回收器有不同的行为.

变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔. 对于在包一级声明的变量来说, 它们的生命周期和整个程序的运行周期是一致的. 而相比之下, 在局部变量的生命周期则是动态的: 从每次创建一个新变量的声明语句开始, 直到该变量不再被引用为止, 然后变量的存储空间可能被回收. 函数的参数变量和返回值都是局部变量. 它们在函数每次被调用的时候创建.

函数的有右小括弧也可以另起一行缩进, 同时为了防止编译器在行尾自动插入分号而导致的编译错误, 可以在末尾的参数变量后面显式插入逗号.

Go语言的自动垃圾收集器是如何知道一个变量时何时可以被回收的呢? 基本的实现思路是, 从每个包级的变量和每个当前运行函数的每一个局部变量开始, 通过指针或引用的访问路径遍历, 是否可以找到该变量. 如果不存在这样的访问路径, 那么说明该变量时不可达的.

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间, 这个选择并不是由用var还是new声明变量的方式决定的.

1
2
3
4
5
6
7
8
9
10
11
12
var global *int

func f() {
var x int
x = 1
global = &x
}

func g() {
y := new(int)
*y = 1
}

f函数里的x变量必须在堆上分配, 因为它在函数退出后依然可以通过包一级的global变量找到, 虽然它是在函数内部定义的; 用Go语言的术语来说, 这个x局部变量从函数f中逃逸了. 相反, 当g函数返回时, 变量*y将是不可达的, 也就是说可以马上被回收. 因此, *y并没有从函数g中逃逸, 编译器可以选择在栈上分配*y的存储空间(译注: 也可以选择在堆上分配, 然后由Go语言的GC回收这个变量的内存空间), 虽然这里用的是new方式. 其实在任何时候, 你并不需要为了编写正确的代码而要考虑变量的逃逸行为, 要记住的是, 逃逸的变量需要额外分配内存, 同时对性能的优化可能产生细微的影响.

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助, 但也不是说你完全不用考虑内存了. 你虽然不需要显式地分配和释放内存, 但是要编写高效的程序你依然需要了解变量的生命周期. 例如, 如果将指向短生命周期对象保存到具有长生命周期的对象中, 特别是保存到全局变量时, 会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能).

赋值

使用赋值语句可以更新一个变量的值.

元组赋值

元组赋值是另一种形式的赋值语句, 它允许同时更新多个变量的值. 在赋值之前, 赋值语句右边的所有表达式将会先进行求值, 然后再统一更新左边对应变量的值. 这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助, 例如我们可以这样交换两个变量的值:

1
2
x, y = y, x
a[i], a[j] = a[j], a[i]

有些表达式会产生多个值, 比如调用一个有多个返回值的函数. 当这样一个函数调用出现在元组赋值右边的表达式中时(右边不能再有其它表达式), 左边变量的数目必须和右边一致.

通常, 这类函数会用额外的返回值来表达某种错误类型, 例如os.Open是用额外的返回值返回一个error类型的错误, 还有一些是用来返回布尔值, 通常被称为ok.

和变量声明一样, 我们可以用下划线空白标识符_来丢弃不需要的值.

可赋值性

赋值语句是显式的赋值语句, 但是程序中还有很多地方会发生隐式的赋值行为: 函数调用会隐式地将调用参数的值赋值给函数的参数变量, 一个返回语句将隐式地将返回操作的值赋值给结果变量, 一个复合类型的字面字面量也会产生赋值行为.

不管是隐式还是显式地赋值, 在赋值语句左边的变量和右边最终求得的值必须有相同的数据类型. 更直白地说, 只有右边的值对于左边地变量时可赋值的, 赋值语句才是允许的.

对于两个值是否可以用==!=进行相等比较的能力也和可赋值能力有关系: 对于任何类型的值的相等比较, 第二个值必须是对第一个变量值类型对应的变量是可赋值的, 反之亦然.

类型

变量或表达式的类型定义了对应存储值的属性特征, 例如数值在内存中的存储大小(或者是元素的bit个数), 它们在内部是如何表达的, 是否支持一些操作符, 以及它们自己关联的方法集等.

一个类型声明语句创建了一个新的类型名称, 和现有类型就有相同的底层结构. 新命名提供了一种方式, 用来分隔不同概念的类型, 这样即使它们底层类型相同也是不兼容的.

1
type 类型名称 底层类型

由于对于中文汉字, Unicode标志都是作为小写字母处理, 因此中文的命名默认不能导出.

对于每一个类型T, 都有一个对应的类型转换操作T(x), 用于将x转为T类型(如果T是指针类型, 可能会需要用小括弧包括T, 比如(*int)(0)). 只有当两个类型的底层基础类型相同时, 才能允许这种转型操作, 或者是两者都是指向相同底层结构的指针类型, 这些转换只改变类型而不会影响值本身.

底层数据类型决定了内部结构和表达方式, 也决定是否可以像底层类型一样对内置运算符的支持.

通过==<之类的比较操作符, 命名类型的值可以与其相同类型的值或者底层类型相同的未命名类型的值相比较.

包和文件

Go语言中的包和其他语言的库或模块的概念类似, 目的都是为了支持模块化, 封装, 单独编译和代码重用. 每个包都对应一个独立的命名空间.

在每个源文件的包声明前紧跟着的注释是包注释. 通常, 包注释的第一句应该是包的功能概要说明. 一个包通常只有一个源文件有包注释.

导入包

Go程序语言中, 每个包都是有一个全局唯一的导入路径. 导入语句中类似gopl.io/ch2/tempconv的字符串对应包的导入路径. Go语言的规范并没有定义这些字符串的具体含义或包来自哪里, 它们是由构建工具来解释的. 当使用Go语句自带的go工具箱时, 一个导入路径代表一个目录中的一个或多个Go源文件.

除了包的导入路径, 每个包还有一个包名, 包名一般是短小的名字(并不要求包名是唯一的), 包名在包的声明处指定. 按照惯例, 一个包的名字和包的导入路径的最后一个字段相同.

导入语句将导入的包绑定到一个短小的名字, 然后通过该短小的名字就可以引用包中导出的内容. 在默认情况下, 导入的包绑定到包声明语句指定的名字, 但是我们也可以绑定到另一个名字, 以避免名字冲突.

如果导入了一个包, 但是又没有使用该包将被当作一个编译错误处理.

包的初始化

包的初始化首先是解决包级变量的依赖顺序, 然后按照包级变量声明出现的顺序依次初始化.

如果包中包含多个.go源文件, 它们将按照发给编译的顺序进行初始化, Go语言的构建工具首先会将.go文件根据文件名排序, 然后依次调用编译器编译.

对于在包级别声明的变量, 如果有初始化表达式则用表达式初始化, 还有一些没有初始化表达式的. 在这种情况下, 我们可以用一个特殊的init初始化函数来简化初始化工作. 每个文件都可以包含多个init初始化函数

1
2
3
func init() {

}

这样的init初始化函数除了不能被调用或引用之外, 其他行为和普通函数类似. 在每个文件中的init初始化函数, 在程序开始执行时按照它们声明的顺序被自动调用.

每个包在解决依赖的前提下, 以导入声明的顺序初始化, 每个包只会被初始化一次. 初始化工作是自下而上进行的, main包最后被初始化. 以这种形式, 可以确保在main函数执行之前, 所有依赖的包都已经完成初始化工作了.

1
2
3
4
5
6
7
8
9
//  循环的一种写法
for i := range pc {

}

// 循环的另外一种写法
for i, _ := range pc {

}

作用域

一个声明语句将程序中的实体和一个名字关联, 比如一个函数或一个变量. 声明语句的作用域是指源代码中可以有效使用这个名字的范围.

不要将作用域和生命周期混为一谈. 声明语句的作用域对应的是一个源代码的文本区域; 它是一个编译时的属性. 一个变量的生命周期是指程序运行时变量存在的有效时间段, 在此时区域内它可以被程序的其他部分引用; 是一个运行时的概念.

声明语句对应的词法域决定了作用域范围的大小. 对于内置的类型, 函数和常量, 比如int, lentrue等是在全局作用域的, 因此可以在整个程序中直接使用. 任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问. 对于导入的包, 则是对应源文件级的作用域, 因此只能在当前的文件中访问导入的包, 当前包的其他源文件无法访问在当前源文件导入的包. 还有许多声明语句, 例如函数中的变量, 则是局部作用域, 它只能在函数内部(甚至只能是局部的某些部分)访问.

当编译器遇到一个名字引用时, 将从最内层的词法域向全局的作用域查找其声明. 如果查找失败, 则报告undeclared name错误; 如果在内层和外层块都存在这个声明, 内层的将先被找到. 在这种情况下, 内层声明将覆盖外部声明, 使它不可访问.

在包级别, 声明的顺序和它们的作用域没有关系, 所以一个声明可以它自己或者跟在它后面的其他声明, 使我们可以声明递归或相互递归的类型和函数. 如果常量或变量声明引用它自己, 则编译器会报错.

常用的函数

1
2
3
4
5
6
7
8
9
# 读入命令行参数
flag.Bool()
flag.String()

# 用于更新每个标志参数对应变量的值(之前是默认值)
flag.parse()

# 获得当前的工作目录
os.Getwd()

初始化

树莓派连接wifi和使用SSH连接树莓派

  1. SD卡的根目录新建wpa_supplicant.conf文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
ssid="wifi-A"
psk="12345678"
key_mgmt=WPA-PSK
priority=3
}

network={
ssid="wifi-B"
psk="12345678"
key_mgmt=WPA-PSK
priority=2
}

network={
ssid="wifi-C"
psk="12345678"
key_mgmt=WPA-PSK
priority=1
}

树莓派使用5Gwifi可能出现问题, 建议使用2.4Gwifi.

ssidpsk出现特殊字符和中文字符有可能连不上wifi.

priority的值越大, 优先级越高.

树莓派启动时会自动把这个文件

  1. SD卡的根目录新建ssh文件, 这个会让树莓派开启ssh

SD不要在在用的时候进行插拔, 很容易损坏

树莓派设置用户和密码

新的系统不再支持pi'raspberry, 需要自己往SD中写入东西. 新建userconf, 里面的内容username:encrypted-password.加密的密码用下面的内容生成.

1
echo 'mypassword' | openssl passwd -6 -stdin

初始化root密码

1
sudo passwd root

Linux命令别名和命令颜色显示

修改/etc/bash.bashrc中文件可以对所有的用户生效

1
2
3
4
5
alias ls='ls --color=auto'
alias ll='ls -a --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'

设置VIM

修改/etc/vim/vimrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
" 设置行号
set nu
" 始终显示状态栏
set laststatus=2
" 高亮当前行
set cul
" 设置tab的大小为4个空格
set tabstop=4
" 回退删除4个空格
set softtabstop=4
" 自动缩进
set autoindent
" 搜索高亮
set hlsearch
" 设置字体和大小
set guifont=Courier\ New:h12
" 设置Vim主题
colorscheme elflord
" 取消回撤文件
set noundofile
" 取消备份文件
set nobackup
" 取消交换文件
set noswapfile

设置DDNS

  1. 在阿里云上添加一条AAAA的记录, 下面的ipv6地址随便填, 之后会由DDNS自动刷新
  2. 下载github上的DDNS, 填入阿里云的accesskey
  3. 运行定时任务的脚本
0%