Golang基础语法细节
一、类型
-
简短模式(
x := 100
)定义限制:定义变量,同时显示初始化。
不能提供数据类型。
只能用在函数内部。
-
简短模式有可能会出现退化的赋值操作。例如:
退化赋值前提条件:最少有一个新变量被定义,且必须是同一作用域
-
编译器将未使用的局部变量当做错误,全局变量没问题,局部常量
const y = 123
也没问题,可指定常量类型或由编译器推断,const y int = 123
-
符号名字首字母大小写决定了其作用域。首字母大写为导出成员,可被包外引用。
-
Go并没有明确意义上的枚举(enum)定义,不过可以借助itoa标识符实现一组自增常量值来实现枚举类型。
-
自定义数据类型(type)中,
type data int
不能理解为取别名,这只能表明它们有相同底层数据结构,两者间不存在任何关系,属于完全不同的两种类型,不能隐式转换,也不能直接用于比较表达式。
二、表达式
-
自增、自减不再是运算符。只能作为独立语句,不能用于表达式。
-
局部变量的有效范围包含整个if/else块,
-
编程细节:尽可能减少代码块嵌套,让正常逻辑处于相同层次。将流程和局部细节分离是常见做法,不同的变化因素被分隔在各自独立单元(函数或模块)内,可避免修改时造成关联错误,减少患”肥胖症“的函数数量。
该示例中,if块显然承担了两种逻辑:错误处理和后续正常操作。
如此,if块仅完成条件检查和错误处理,相关正常逻辑保持在同一层次。
-
switch语句中,无须显示执行break语句,case执行完毕后自动中断。如须贯通后续case,须执行fallthrough,但不再匹配后续条件表达式。注意,fallthrough必须放在case块末尾,可使用break语句阻止。
-
无论普通的for循环,还是range迭代,其定义的局部变量都会重复使用。
注意:range会复制目标数据。
相关数据类型中,复制成本都很小,无须专门优化。
三、函数
-
不管是指针、应用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象还是拷贝指针而已。
-
如果函数参数过多,建议将其重构为一个符合结构类型,也算变相实现可选参数和命名实参功能。
闭包
闭包是函数和引用环境的组合体。关键要分析出返回函数引用到哪些变量。
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓"延迟求值"的特性。
由于for循环复用局部变量i,则每次添加匿名函数引用的自然是同一变量。
错误
Go的错误处理方式存在不足,即大量函数和方法返回error,使得调用代码变得很难看,一堆堆的检查语句充斥在代码行间。解决思路有:
四、数据
-
要修改字符串,须将其转换为可变类型([]byte或[]rune),待完成后再转换回来。但不管如何转换,都须重新分配内存,并复制数据。
-
数组中,元素类型相同,长度不同的数组不属于同一类型。
-
与C不同,Go数组是值类型,赋值和传参操作都会复制整个数组数据,可以改用指针或切片,避免数据复制。
切片
- 可基于数组来创建切片,注意cap的计算方式
cap = max - low
,结合下图理解:
属性cap表示切片所引用数组片段的真实长度,len用于限定可读的写元素数量。
使用append追加元素时,数据被追加到原底层数组。如果超出cap限制,则为新切片对象重新分配数组。注意:1.是超出切片cap限制,而非底层数组长度限制,因为cap可小于数组长度。2.新分配数组长度是原cap的2倍,而非原数组的2倍,并且非总是2倍,较大的切片会尝试扩容1/4,以节约内存。
注意:切片初始化使用make函数时,若不指定cap,则cap和len一样。
- 利用上述特性,可以容易实现一个栈式数据结构,很巧妙,可以学习使用
编程技巧:1.正是因为存在重新分配底层数组的缘故,某些场合建议多预留足够的空间,避免中途内存分配和数据复制开销。2.如果切片长时间引用大数组中很小的片段,建议新建独立切片,复制出所需数据,以便原数组内存可被及时回收。
字典
- 因为内存访问安全和哈希算法的缘故,字典被设计成"not addresssable",故不能直接修改value成员(结构或数组)。例如
m[1].age += 1
正确做法是返回整个value,待修改后再设置字典键值,或直接用指针类型。 - 运行时会对字典并发操作做出检测,可使用sync.RWMutex实现同步,避免读写操作同时进行。
- 字典对象本身就是指针包装,传参时无须再次取地址;在创建时预先准备足够空间有助于提升性能,减少扩张时的内存分配和重新哈希操作。
结构
- 空结构可作为通道元素类型,用于事件通知。
- 实际上,这类**“长度"为零的对象通常都指向runtime.zerobase变量**。
五、方法
- 方法内部不引用实例,则可省略参数名,仅保留类型。
- 类型T和类型*T的方法集不相同,具体关系如下:
- 可以像访问匿名字段成员那样调用其方法
很显然,匿名字段就是为方法准备的。Go语言更加倾向于"组合优于继承"的思想将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入的方式组合到一起。
六、接口
- Go接口实现机制很简洁,只要目标类型方法集内包含接口声明的全部方法,就被视为实现了接口,无须作显示声明。当然,目标类型可以实现多个接口。但是接口会有一些语法限制。
接口通常以er作为名称后缀,方法名是声明组成部分,但参数名可不同或省略。
-
超集接口变量可隐式转换为子集,反过来不行。
-
将对象赋值给接口变量时,会复制该对象。**甚至无法修改接口存储的复制品,**即便将其复制出来,用本地变量修改后,依然无法对iface.data赋值。解决方法是将对象指针赋值给接口。
-
只有当接口变量内部的两个指针(itab,data)都为nil时,接口才等于nil。itab中还存放了接口类型的字段。
七、并发
- 并发和并行的区别,简单地说,并行是并发设计的理想执行模式。
并发:逻辑上具备同时处理多个任务的能力。
并行:物理上在同一时刻执行多个并发任务。
- 与defer一样,goroutine也会因“延迟执行”而立即计算并复制执行参数。进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号。
**如果等待多个任务结束,推荐使用sync.WaitGroup。**通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。
注意:尽管WaitGroup.Add实现了原子操作,但还是建议在goroutine外累加计数器,避免Add尚未执行,Wait已经退出。
- 通常使用工厂方法将goroutine和通道绑定
- 通道可能会引发资源泄露(goroutine leak),确切的说,是指goroutine处于发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致它们会在等待队列里长久休眠,形成资源泄露。
八、包结构
- 编译器等相关工具按GOPATH设置的路径搜索目标,即在导入目标库时,排在列表前面的路径比当前工作空间优先级更高。
注意:不同操作系统,GOPATH列表分隔符不同。UNIX-like使用冒号,Windows使用分号。
- 同一目录下所有源码文件必须使用相同包名称,不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。不同路径的同名包允许存在,可以利用后面给包取别名来区分。
- 程序编译时,会使用声明main包的代码所在的目录的目录名作为二进制可执行文件的文件名(如果编译时不给出编译后的可执行文件名)。
- 编译器首先确保完成所有全局变量初始化,然后才开始执行初始化函数。直到这些全部结束后,运行时才正式进入main.main入口函数。
GOPATH模式
通过输入go env
命令,可以看到GOPATH变量的结果,GOPATH目录下一共包含了三个子目录,分别是:
- bin:存储所编译生成的二进制文件。
- pkg:存储预编译的目录文件,以加快程序的后续编译速度。
- src:存储所有
.go
文件或源代码。
因此在使用GOPATH模式下,我们需要把应用代码存放在固定的$GOPATH/src
目录下,并且如果执行go get
来拉取外部依赖会自动下载并安装到$GOPATH
目录下。
弃用GOPATH模式
- GOPATH模式下没有版本控制的概念,在执行
go get
的时候,你无法传达任何版本信息,也就是说你无法知道自己当前跟新的是哪一个版本,也无法通过指定具体版本。 - 运行Go应用程序时,无法保证其他人与你所期望依赖的第三方库是相同的版本。
Go Modules模式
提供了如下命令进行操作:
提供的环境变量
-
GO111MODULE
这个环境变量作为Go modules的开关,允许设置一下参数:
- auto:只要项目包含go.mod文件的话启动Go modules。
- on:启动Go modules。
- off:禁用Go modules。
-
GOPROXY
这个环境变量主要是用于设置Go模块代理,作用是用于使Go在后续拉取模块版本时直接通过镜像站点快速拉取。默认是是
https://proxy.golang.org,direct
,但是国内无法访问,可以修改为https://goproxy.cn,direct
。*GOPROXT的值以一个英文逗号
,
分隔Go的模块代理列表,允许设置多个模块代理。
direct是什么?
实际上是一个特殊指示符,用于指示Go回源到模块版本的源地址去抓取(比如GitHub等),过程如下:当值列表中上一个Go模块代理返回404或410错误时,Go自动尝试列表中下一个,遇到"direct"时回源抓取。
-
GOSUMDB
用于在拉取模块时保证拉取到的模块版本数据未经过篡改。默认值是:
sum.golang.org
,但是GOSUMMDB可以被Go模块代理所代理,所以不必过度关心这个变量。 -
GONOPROXY|GONOSUMDN|GOPROVATE
暂时用不到,不用了解,后续用到可以学习。
使用Go Modules
- 使用
go mod init 模块名
来指定模块导入路径,后续可以在项目根目录执行go get 第三方包路径
导入第三方依赖。
在第一次拉取模块依赖后,还会多出一个go.sum文件。
- 模块拉取的结果缓存在
$GOPATH/pkg/mod
和$GOPATH/pkg/sumdb
目录下。如果希望清理已缓存的模块版本数据,可执行go clean -modcache
命令。
Go Modules下的go get行为
拉取项目依赖时,过程分为三步,分别是finding(发现)、downloading(下载)以及extracting(提取)。注意 不指定版本信息,则由Go modules自行按照内部规则进行选择。
最小版本选择(MVS)
最小版本选择可以理解为”最新非最大“版本选择。举例如下
上图显示了moduleA,B和C分别独立地需要module D和各自需要D的不同版本。我们可以要求go向我们提供所有已存在(打 tag)的版本列表。
可以看到module D 最新最大版本是1.4.2。
如果只有module A,则选择v1.0.6版本即可,如果引入module B,则module D版本会升级到v1.2.0;再次引入module C,则Go将从当前所需版本集合中(v1.0.6 v1.2.0 v1.3.2)选择最新版本v1.3.2, 最后如果删除module C ,Go会将项目锁定到module D 的版本v1.3.2上。降级到版本v1.2.0将是一个更大的更改,而Go知道版本v1.3.2可以正常并稳定运行,因此版本v1.3.2仍然是module D的"最新但非最大"版本。
参考博客:go modules 讲解、最小版本选择、go modules讲解2