go之struct、iota和string
学习一下go语言基础所做笔记,主要参考《Go专家编程》和《Go程序设计语言》这两本书。
一 struct
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。
type Point struct {
X, Y int
}
我们可以直接对每个成员赋值:
var p Point
p.X = 1
后面的都是比较日常的使用就不介绍了,感觉Tag比较有意思,遂记录。
Tag
Go的struct声明允许字段附带 Tag 来对字段做一些标记。
该 Tag 不仅仅是一个字符串那么简单,因为其主要用于反射场景, reflect 包中提供了操作 Tag 的方法,所以 Tag 写法也要遵循一定的规则。ps:其实就是反射的时候获取字段的tag值,用于操作。
规则
Tag 本身是一个字符串, 但字符串中却是: 以空格分隔的 key:value 对 。注意: 冒号前后不能有空格如下代码所示, 如此写没有实际意义, 仅用于说明 Tag 规则:
type Server struct {
ServerName string `json:"server_name" xorm:"ser_name"`
ServerIP string `json:"server_ip"`
}
前面说过, Tag 只有在反射场景中才有用,而反射包中提供了操作 Tag 的方法。在说方法前,有必要先了解一下 Go是如何管理struct字段的。以下是 reflect 包中的类型声明,省略了部分与本文无关的字段。
可见, 描述一个结构体成员的结构中包含了 StructTag , 而其本身是一个 string 。
用法
StructTag 提供了 Get(key string) string 方法来获取 Tag
func main() {
s := Server{}
st := reflect.TypeOf(s)
field := st.Field(0)
fmt.Println(field.Tag.Get("json"), field.Tag.Get("xorm"))
}
使用反射可以动态的给结构体成员赋值, 正是因为有tag, 在赋值前可以使用tag来决定赋值的动作。 比如, 官方的 encoding/json 包, 可以将一个JSON数据 Unmarshal 进一个结构体, 此过程中就使用了Tag. 该包定义一些规则, 只要参考该规则设置tag就可以将不同的JSON数据转换成结构体 。
二 iota
常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每
行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置
为0,然后在每一个有常量声明的行加一。
下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常
量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
周日将对应0,周一为1,如此等等。
规则
很多书上或博客描述的规则是这样的:
- iota在const关键字出现时被重置为0
- const声明块中每新增一行iota值自增1
我曾经也这么理解,看过编译器代码后发现,其实规则只有一条:
iota代表了const声明块的行索引(下标从0开始)
这样理解更贴近编译器实现逻辑,也更准确。除此之外,const声明还有个特点,即第一个常量必须指定一个表达
式,后续的常量如果没有表达式,则继承上面的表达式。
举例:
const (
A = iota // A = 0
_ // 空白标识符,跳过这一行
C // C = 2
)
const (
A = 1 // A = 1
B // B = 1
C = iota // C = 2
D // D = 3
)
编译原理
const块中每一行在GO中使用spec数据结构描述,spec声明如下:
这里我们只关注ValueSpec.Names, 这个切片中保存了一行中定义的常量,如果一行定义N个常量,那么
ValueSpec.Names切片长度即为N。
const块实际上是spec类型的切片,用于表示const中的多行。
所以编译期间构造常量时的伪算法如下:
for iota, spec := range ValueSpecs {
for i, name := range spec.Names {
obj := NewConst(name, iota...) //此处将iota传入,用于构造常量
...
}
}
从上面可以更清晰的看出iota实际上是遍历const块的索引,每行中即便多次使用iota,其值也不会递增。
这就是书上的原文,可能我比较笨,当时没太理解,所以我举了个例子,假如:
const (
A, B = iota, iota << 1 // A=0, B=0
C, D // C=1, D=2
E // E=2
)
换句话说,ValueSpec
代表的是const
块中的每一行声明。例如,在给定的const
块中有3个ValueSpec
实例,每个实例对应一行声明。而Names
字段则表示该行声明中定义的所有常量名称。比如,在第一行声明中如果有两个常量(如A
和B
),那么这一行对应的ValueSpec.Names
的长度就是2。这样在回头看编译期的伪代码是不是就理解很多了?
三 stirng
builtin 给出了所有内置类型的定义。源代码位于 src/builtin/builtin.go ,其中关于string的描述如
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string strin
所以string是8比特字节的集合,通常但并不一定是UTF-8编码的文本。
另外,还提到了两点,非常重要:
数据机构
源码包 src/runtime/string.go:stringStruct 定义了string的数据结构:
string数据结构跟切片有些类似,只不过切片还有一个表示容量(cap)的成员,事实上string和切片,准确的说是byte切 片经常发生转换。
字符串构建过程是先跟据字符串构建stringStruct,再转换成string。转换的源码如下:
func gostringnocopy(str *byte) string { // 跟据字符串地址构建string
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} // 先构造stringStruct
s := *(*string)(unsafe.Pointer(&ss)) // 再将stringStruct转换成string
return s
}
string在runtime包中就是stringStruct,对外呈现叫做string。
与[]byte的转换
byte切片可以很方便的转换成string,如下所示:
func GetStringBySlice(s []byte) string {
return string(s)
}
需要注意的是这种转换需要一次内存拷贝。
转换过程如下:
- 跟据切片的长度申请内存空间,假设内存地址为p,切片长度为len(b);
- 构建string(string.str = p;string.len = len;)
- 拷贝数据(切片中数据拷贝到新申请的内存空间)
其实就是深拷贝,string转byte切片也是同理,我就不介绍了,注意换成byte切片之后,切片的cap=len。
字符串拼接
字符串可以很方便的拼接,像下面这样:
str := "Str1" + "Str2" + "Str3
即便有非常多的字符串需要拼接,性能上也有比较好的保证,因为新字符串的内存空间是一次分配完成的,所以性能消耗主要在拷贝数据上。
一个拼接语句的字符串编译时都会被存放到一个切片中,拼接过程需要遍历两次切片,第一次遍历获取总的字符串长度,据此申请内存,第二次遍历会把字符串逐个拷贝过去。 字符串拼接伪代码如下:
func concatstrings(a []string) string { // 字符串拼接
length := 0 // 拼接后总的字符串长度
for _, str := range a {
length += len(str)
}
s, b := rawstring(length) // 生成指定大小的字符串,返回一个string和切片,二者共享内存空间
for _, str := range a {
// string无法修改,只能通过切片修改
copy(b, str)
b = b[len(str):]
}
return s
}
我画个图,方便理解下。
因为string是无法直接修改的,所以这里使用rawstring()方法初始化一个指定大小的string,同时返回一个切 片,二者共享同一块内存空间,后面向切片中拷贝数据,也就间接修改了string。
rawstring()源代码如下:
func rawstring(size int) (s string, b []byte) { // 生成一个新的string,返回的string和切片共享相同的空间
p := mallocgc(uintptr(size), nil, false)
stringStructOf(&s).str = p
stringStructOf(&s).len = size
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
return
}
问题
为什么字符串不允许修改?
像C++语言中的string,其本身拥有内存空间,修改string是支持的。但Go的实现中,string不包含内存空间,只有一个内存的指针,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。
因为string通常指向字符串字面量,而字符串字面量存储位置是只读段,而不是堆或栈上,所以才有了string不可修改的约定。
[]byte转换成string一定会拷贝内存吗?
byte切片转换成string的场景很多,为了性能上的考虑,有时候只是临时需要字符串的场景下,byte切片转换成
string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存。
比如,编译器会识别如下临时场景:
因为是临时把byte切片转换成string,也就避免了因byte切片同容改成而导致string引用失败的情况,所以此时可以不必拷贝内存新建一个string。
我觉得在byte[]
需要转换为 string
后,这个stirng只读不修改的情况下,这个byte[]不会转为string。(瞎想的)
参考
Go专家编程
Go程序设计语言
作者:翻身都嫌累