学习一下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,如此等等。

规则

很多书上或博客描述的规则是这样的:

  1. iota在const关键字出现时被重置为0
  2. 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字段则表示该行声明中定义的所有常量名称。比如,在第一行声明中如果有两个常量(如AB),那么这一行对应的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编码的文本。

另外,还提到了两点,非常重要:

  • string可以为空(长度为0),但不会是nil;
  • string对象不可以修改。
  • 数据机构

    源码包 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)
    }

    需要注意的是这种转换需要一次内存拷贝。

    转换过程如下:

    1. 跟据切片的长度申请内存空间,假设内存地址为p,切片长度为len(b);
    2. 构建string(string.str = p;string.len = len;)
    3. 拷贝数据(切片中数据拷贝到新申请的内存空间)

    其实就是深拷贝,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)指向切片的内存。

    比如,编译器会识别如下临时场景:

  • 使用m[string(b)]来查找map(map是string为key,临时把切片b转成string);
  • 字符串拼接,如”<” + “string(b)” + “>”;
  • 字符串比较:string(b) == “foo”
  • 因为是临时把byte切片转换成string,也就避免了因byte切片同容改成而导致string引用失败的情况,所以此时可以不必拷贝内存新建一个string。

    我觉得在byte[] 需要转换为 string 后,这个stirng只读不修改的情况下,这个byte[]不会转为string。(瞎想的)

    参考

    Go专家编程

    Go程序设计语言

    作者:翻身都嫌累

    物联沃分享整理
    物联沃-IOTWORD物联网 » go之struct、iota和string

    发表回复