Golang 学习笔记
本文记述的一些问题较为凌乱,主要是方便自己后续复查
1. 匿名函数作为参数使用
这次发现的问题是,业务代码中的一个函数需要调用main函数中的一些变量或者函数,但是总所周知,一般情况下,只有main函数才会去调用别的package 或者 func,解决办法目前知道两种:
- 使用接口的方式:使用接口将需要调用main函数中的函数作为接口中的成员,然后在main中实现,再将这个接口当作参数传给需要调用者,但是看起来并不雅观,我为了调用还需要定义一个接口,不推荐。
net/wire/addr.go
type aaa interface{
Add(i int) (string,error)
}
func (a xxx) AddAddress(a aaa){
dosomething()
}
main.go
func Add(i int) (string,error){
dosomething()
}
- 另一种方式是传匿名函数,将匿名函数作为参数传入,在main中调用的时候,传入需要调用的函数。
net/wire/addr.go
func (a *AddrManager) NewAddress(filterOut func(gKey string) bool, astn func(string) (net.Addr, error)) (net.Addr, error) {
dosomething
}
main.go
amgr.NewAddress(func(groupKey string) bool {
return s.OutboundGroupCount(groupKey) != 0
},addrStringToNetAddr)
匿名函数并不是golang的首创,在cpp和Java中有类似处理。
2.处理package之间的循环引用问题
我目前能想到较为不错的处理方式:
- 放同一个包里
- 抽取公共层
- 依赖倒置
依赖倒置:比如 a->call->b,这时候 a 需要依赖 b,但这时候你可以把 b 可以完成的行为定义一些 interface,把 interface 定义在 a 里,让 b 实现这些 interface,然后初始化的时候把 b 赋值给 a 中的某个 interface 变量(golang的interface实现是隐私的)。
抽取公共层:屏蔽a,b之间的直接引用,抽离出一个公共包实现他们的调用路由,这是第二种办法。 借鉴了设计模式里的中介者模式。
3.open与openfile
最近在看cpp的代码,在c和cpp的代码中,有 rb+ 、wb+之类的操作,翻译为golang,当时候以为就是一个string类型的前缀,没有意识到是有特殊的含义,之后查询man fopen
,如下:
The argument mode points to a string beginning with one of the following
letters:
``r'' Open for reading. The stream is positioned at the beginning of
the file. Fail if the file does not exist.
``w'' Open for writing. The stream is positioned at the beginning of
the file. Create the file if it does not exist.
``a'' Open for writing. The stream is positioned at the end of the
file. Subsequent writes to the file will always end up at the
then current end of file, irrespective of any intervening
fseek(3) or similar. Create the file if it does not exist.
An optional ``+'' following ``r'', ``w'', or ``a'' opens the file for
both reading and writing. An optional ``x'' following ``w'' or ``w+''
causes the fopen() call to fail if the file already exists. An optional
``e'' following the above causes the fopen() call to set the FD_CLOEXEC
flag on the underlying file descriptor.
golang中提供了 OpenFile函数,可以对文件的权限进行操作,具体如下:
OpenFile is the generalized open call; most users will use Open or Create instead. It opens the named file with specified flag (O_RDONLY etc.) and perm (before umask), if applicable. If successful, methods on the returned File can be used for I/O. If there is an error, it will be of type *PathError.
官方例子:
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
带append操作的:
// If the file doesn't exist, create it, or append to the file
f, err := os.OpenFile("access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
if _, err := f.Write([]byte("appended some data\n")); err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
4.interface
interface结构主要是在runtime包下实现的,主要有两个struct:
iface结构:
type iface struct { // 16 bytes on a 64bit arch
tab *itab
data unsafe.Pointer
}
它的内部维护了两个指针:
- tab 持有 itab 对象的地址,该对象内嵌了描述 interface 类型和其指向的数据类型的数据结构。
- data 是一个 raw (i.e. unsafe) pointer,指向 interface 持有的具体的值。
itab 结构:
type itab struct { // 40 bytes on a 64bit arch
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
_type
是golang中所有类型的超集。
itab 是 interface 的核心。
首先,itab 内嵌了 _type,_type 这个类型是 runtime 对任意 Go 语言类型的内部表示。 _type 类型描述了一个“类型”的每一个方面: 类型名字,特性(e.g. 大小,对齐方式...),某种程度上类型的行为(e.g. 比较,哈希...) 也包含在内了。 在这个例子中,_type 字段描述了 interface 所持有的值的类型,data 指针所指向的值的类型。
其次,我们找到了一个指向 interfacetype 的指针,这只是一个包装了 _type 和额外的与 interface 相关的信息的字段。 像你所期望的一样,inter 字段描述了 interface 本身的类型。
最后,func 数组持有组成该 interface 虚(virtual/dispatch)函数表的的函数的指针。
空接口:
空接口的的数据结构和直觉推测差不多: 一个不带 itab 的 eface 结构。 这样做有两个原因:
- 空接口中没有任何方法,和动态分发相关的东西都可以从数据结构中移除。
- 干掉虚表之后,接口本身的类型,这里注意不要和接口中数据的类型混了,始终都是相同的(i.e. 我们说的是 这个 空接口而不是 一个 空接口)
type eface struct { // 16 bytes on a 64bit arch
_type *_type
data unsafe.Pointer
}
其中 _type 持有 data 指针指向的数据类型的信息。 itab 被完全干掉了。
尽管空接口理论上可以重用 iface 数据结构(因为 iface 可以算是 eface 的一个超集),runtime 还是选择对这两种 interface 进行区分,主要有两个理由: 为了节省空间,以及代码清晰。
5 new 和make的区别
new和make主要用于golang中引用类型(slice、map、channel)的初始化。
- 内置函数new会去计算类型的大小,为其分配零值内存,并返回指针
- make会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,并返回对象而不是指针
6 slice
slice的底层构造如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice内部有三个元素,array指向底层的数组,len表示slice的长度,cap表示slice的容量。
slice的扩容
slice的扩容一般遵循如下原则:如果当前slice的大小是原先大小的2倍以上,那我就把大小直接扩容为当前slice的大小。如果不够两倍,针对大小的不同有不同的扩容方案:<1> 如果大小不超过1024,那就直接按照2倍来增长,反正也没有多大,如果超过的话,按照当前大小的1/4来扩容。
slice的扩容需要通过append操作来触发,当超过slice的cap值时,会进行扩容,并重新分配底层的数组。
7 string
字符串是不可变值类型,内部 指针指向 UTF-8 字节数组。
- 默认值是空字符串 ""。
- 索引号访问某字节,如 s[i]。
- 不能 序号获取字节元素指针,&s[i] 法。 • 不可变类型, 法修改字节数组。
- 字节数组尾部不包含 NULL。
runtime.h
struct string {
byte* str;
intgo len;
}
string内部有两个元素,一个是指向固定地址的str指针,一个是表示当前string类型的len值。
通常string常量是编译器分配到只读段的(.rodata),对应的数据地址不可写入。