• 周六. 3月 2nd, 2024

    Go语言引用数据类型

    root

    10月 14, 2019 #Golang, #数据类型

    5.1 字符串

    字符串是不可变字节(byte)序列, 其本身是一个复合结构
    package main
    
    import "unsafe"
    
    type stringStruct struct {
        str unsafe.Pointer
        len int
    }
    
    头部指针指向字节数组,但没有NULL结尾。默认以UTF-8编码存储Unicode字符,字面量允许使用十六进制,八进制和UTF编码格式
    字符串默认值不是nil,而是””
    使用”`”定义不做转义处理的原始字符串(raw string),支持跨行
    package main
    func main() {
    	s := `line\r\n,
    line2`
    	println(s)
    }
    
    允许索引号访问字节数组(非字符),但不能获取元素地址。
    func main() {
        s := "abc"
        println(s[1])
        println(&s[1])  //错误:cannot take the address of s[1]
    }
    
    要修改字符串,须将其转为可变类型([]rune或[]byte),待完成后再转回来。但不管如何转换,都须重新分配内存,并复制数据。
    package main
    
    import (
    	"fmt"
    	"reflect"
    	"unsafe"
    )
    
    func pp(format string, ptr interface{}) {
    	p := reflect.ValueOf(ptr).Pointer()
    	h := (*uintptr)(unsafe.Pointer(p))
    	fmt.Printf(format, *h)
    }
    
    func main() {
    	s := "hello, world!"
    	pp("s:%x\n", &s)
    
    	bs := []byte(s)
    	s2 := string(bs)
    
    	pp("string to []byte, bs:%x\n", &bs)
    	pp("[]byte to string, s2:%x\n", &s2)
    
    	rs := []rune(s)
    	s3 := string(rs)
    	pp("string to []rune, rs:%x\n", &rs)
    	pp("[]rune to string, s3:%x\n", &s3)
    }
    
    
    除类型转换外,动态构建字符串也容易造成性能问题。
    用加法操作符拼接字符串时,每次都须重新分配内存。在构建“超大”字符串时,性能就显得极差
    改进思路是预分配足够的内存空间。常用方法是用strings.Join函数,它会统计所有参数长度,并一次性完成内存分配操作
    字符串操作通常在堆上分配内存,这会对Web等高并发应用会造成较大影响,会有大量字符串对象要做垃圾回收。建议使用[]byte缓存池,或在栈上自行拼装等方式来实现zero-garbage
    类型rune专门用来存储Unicode码点。它是int32的别名,使用单引号的字面量,其默认类型就是rune
    func main() {
        r := '我'
        fmt.Printf("%T\n", r)  //输出 int32
    }
    
    func main() {
        r := '我'
        s := string(r)  //rune to string
        b := byte(r)    //rune to byte
        
        s2 := string(b) //byte to string
        r2 := rune(b)   //byte to rune
        
        fmt.Println(s, b, s2, r2)
    }
    
    标准库unicode里提供了丰富的操作函数,除验证函数外,还可用RuneCountInString代替len返回准确的Unicode字符数量
    func main() {
        s := "中.国"
        println(len(s), utf8.RuneCountInString(s))
    }
    

    5.2 数组

    func main() {
        var d1 [3]int
        var d2 [2]int
        d1 = d2 //错误:cannot use d2(type [2]int) as type [3]int in assignment
        
        var a [4]int //元素自动初始化为零
        b := [4]int{2, 5} //未提供初始值的元素自动初始化为0
        c := [4]int{5, 3: 10} //可指定索引位置初始化
        d := [...]int{1,2,3}    //编译器按初始化值数量确定数组长度
        e := [...]int{10, 3: 100} //支持索引初始化,但注意数组长度与此有关
        
        fmt.Println(a,b,c,d,e)
    }
    
    对于结构等复合类型,可省略元素初始化类型标签
    func main() {
        type user struct {
            name string
            age byte
        }
        
        d := [...]user{
            {"Tom", 20},
            {"Mary", 18},
        }
        fmt.Printf("%#v\n", d)
    }
    
    在定义多维数组时,仅第一维允许使用”…”
    内置函数len和cap都返回第一维度长度
    如元素类型支持==, != 操作符,那么数组也支持此操作
    func main() {
    	var a, b [2]int
    	println(a == b)
    
    	c := [2]int{1, 2}
    	d := [2]int{0, 1}
    	println(c != d)
    
    	var e, f [2]map[string]int
    	println(e == f)		//invalid operation: e == f ([2]map[string]int cannot be compared)
    }
    
    指针数组是指元素为指针类型的数组,数组指针是获取数组变量的地址
    func main() {
    	x, y := 10, 20
    	a := [...]*int{&x, &y}	//元素为指针的指针数组
    	p := &a 				// 存储数组地址的指针
    
    	fmt.Printf("%T, %v\n", a, a)
    	fmt.Printf("%T, %v\n", p, p)
    }
    
    可获取任意元素地址
    func main() {
    	a := [...]int{1, 2}
    	println(&a, &a[0], &a[1])
    }
    /**
    0xc00003df78 0xc00003df78 0xc00003df80
    **/
    
    数组指针可直接用来操作元素
    func main() {
        a := [...]int{1,2}
        p := &a
        p[1] += 10
        println(p[1])
    }
    // 12
    
    与C数组变量隐式作为指针使用不同,Go数组是值类型,赋值和传参操作都会复制整个数组
    如果需要,可改用指针或切片,以此避免数据复制

    5.3 切片

    切片(slice)本身并非动态数组或数组指针。它内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内.
    type slice struct {
        array unsafe.Pointer
        len int
        cap int
    }
    
    切片本身是个只读对象,其工作机制类似数组指针的一种包装。
    可基于数组或数组指针创建切片,以开始和结束索引位置确定所引用的数组片段。不支持反向索引,实际范围是一个右半开区间。
    属性cap表示切版所引用数组片段的真实长度,len用于限定可读的写元素数量。另外,数组必须addressable,否则会引发错误
    和数组一样,切片同样使用索引号访问元素内容。起始索引为0,而非对应的底层数组真实索引位置
    可直接创建切片对象,无须预先准备数组。因为是引用类型,须使用make函数或显式初始化语句,它会自动完成底层数组内存分配
    func main() {
    	s1 := make([]int, 3, 5)	//指定len,cap,底层数组初始化为0
    	s2 := make([]int, 3) //省略cap和len相等
    	s3 := []int{10, 20, 5: 30}	//按初始化元素分配底层数组,并设置len, cap
    
    	fmt.Println(s2, len(s2), cap(s2))
    	fmt.Println(s1, len(s1), cap(s1))
    	fmt.Println(s3, len(s3), cap(s3))
    }
    /**
    [0 0 0] 3 3
    [0 0 0] 3 5
    [10 20 0 0 0 30] 6 6
    **/
    
    func main() {
    	var a []int
    	b := []int{}
    
    	fmt.Printf("a:%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&a)))
    	fmt.Printf("b:%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)))
    	fmt.Printf("a size:%d\n", unsafe.Sizeof(a))
    	fmt.Printf("b size:%d\n", unsafe.Sizeof(b))
    }
    /**
    a:&reflect.SliceHeader{Data:0x0, Len:0, Cap:0}
    b:&reflect.SliceHeader{Data:0x5851f8, Len:0, Cap:0}
    a size:24
    b size:24
    **/
    
    不支持比较操作,就算元素类型支持也不行,仅能判断是否为nil
    可获取元素地址,但不能向数组那样直接用指针访问元素内容
    如果元素类型也是切片,就可以实现类似交错数组功能
    func main() {
    	x := [][]int {
    		{1, 2},
    		{10, 20, 40},
    		{100},
    	}
    	fmt.Println(x[1])
    
    	x[2] = append(x[2], 300, 200)
    	fmt.Println(x[2])
    }
    /*
    [10 20 40]
    [100 300 200]
    */
    
    reslice 将切片视作[cap]slice数据源,据此创建新切片对象。不能超过cap.但不受len限制。
    新建切片对象依旧指向原底层数组,也就是说修改对所有关联切片可见
    func main() {
    	d := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    	s1 := d[3:7]
    	s2 := s1[1:3]
    
    	for i := range s2 {
    		s2[i] += 100
    	}
    
    	fmt.Println(d)
    	fmt.Println(s1)
    	fmt.Println(s2)
    }
    /*
    [0 1 2 3 104 105 6 7 8 9]
    [3 104 105 6]
    [104 105]
    */
    
    利用reslice操作,很容易就能实现一个栈式数据结构
    func main() {
    	//栈最大容量5
    	stack := make([]int, 0, 5)
    	//入栈
    	push := func(x int) error {
    		n := len(stack)
    		if n== cap(stack) {
    			return errors.New("stack is full")
    		}
    		stack = stack[:n+1]
    		stack[n] = x
    		return nil
    	}
    
    	//出栈
    	pop := func() (int, error){
    		n := len(stack)
    		if n==0 {
    			return 0, errors.New("stack is empty")
    		}
    		x := stack[n-1]
    		stack = stack[:n-1]
    		return x, nil
    	}
    
    	//入栈测试
    	for i:=0; i<7; i++ {
    		fmt.Printf("push %d: %v, %v\n", i, push(i), stack)
    	}
    
    	//出栈测试
    	for i := 0; i < 7; i++ {
    		x, err := pop()
    		fmt.Printf("pop:%d, %v, %v\n", x, err, stack)
    	}
    }
    /*
    push 0: <nil>, [0]
    push 1: <nil>, [0 1]
    push 2: <nil>, [0 1 2]
    push 3: <nil>, [0 1 2 3]
    push 4: <nil>, [0 1 2 3 4]
    push 5: stack is full, [0 1 2 3 4]
    push 6: stack is full, [0 1 2 3 4]
    pop:4, <nil>, [0 1 2 3]
    pop:3, <nil>, [0 1 2]
    pop:2, <nil>, [0 1]
    pop:1, <nil>, [0]
    pop:0, <nil>, []
    pop:0, stack is empty, []
    pop:0, stack is empty, []
    */
    
    append 向切片尾部添加数据,返回新的切片对象
    func main() {
    	s := make([]int, 0, 5)
    
    	s1 := append(s, 10)
    	s2 := append(s1, 20, 30)
    
    	fmt.Println(s, len(s), cap(s))
    	fmt.Println(s1, len(s1), cap(s1))
    	fmt.Println(s2, len(s2), cap(s2))
    }
    
    数据被追加到原底层数组。如超出cap限制,则为新切片对象重新分配数组
    func main() {
    	s := make([]int, 0, 100)
    	s1 := s[:2:4]
    	s2 := append(s1, 1, 2, 3, 4, 5, 6)	//超出s1 cap限制,分配新的底层数组
    
    	fmt.Printf("s1:%p:%v\n", &s1[0], s1)
    	fmt.Printf("s2:%p:%v\n", &s2[0], s2)
    
    	fmt.Printf("s data:%v\n", s[:10])
    	fmt.Printf("s1 cap:%d, s2 cap: %d\n", cap(s1), cap(s2))
    }
    /**
    s1:0xc000096000:[0 0]
    s2:0xc00007a080:[0 0 1 2 3 4 5 6]
    s data:[0 0 0 0 0 0 0 0 0 0]
    s1 cap:4, s2 cap: 8
    **/
    
    注意:
    • 是超出切片cap限制,而非底层数组长度限制, 因为cap可小于数组长度
    • 新分配数组长度是原cap的2倍,而非原数组的2倍
    • 并非总是2倍,对于较大的切片,会尝试扩容1/4, 以节约内存
    向nil切片追加数据时,会为其分配底层数组内存
    在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠。最终所复制长度以较短的切片长度(len)为准
    func main() {
    	s := []int{0, 1, 2,3,4,5,6,7,8,9}
    
    	s1 := s[5:8]
    	n := copy(s[4:], s1)	//在同一底层数组的不同区间复制
    	fmt.Println(n, s)
    
    	s2 := make([]int, 6) //在不同数组间复制
    
    	n = copy(s2, s)
    	fmt.Println(n, s2)
    }
    /**
    3 [0 1 2 3 5 6 7 7 8 9]
    6 [0 1 2 3 5 6]
    **/
    
    还可直接从字符串中复制数据到[]byte
    func main() {
    	b := make([]byte, 3)
    	n := copy(b, "abcde")
    	fmt.Println(n, b)
    }
    /*
    3 [97 98 99]
    */
    

    5.4 字典(哈希表)

    无序键值对集合,字典要求key必须是支持相等运算符(==、!=)的数据类型,比如,数字、字符串、指针、数组、结构体,以及对应接口类型。
    字典是引用类型,使用make函数或初始化表达语句来创建
    func main() {
    	m := make(map[string]int)
    
    	m["a"] = 1
    	m["b"] = 2
    
    	m2 := map[int] struct {
    		x int
    	}{
    		1: {x:100},
    		2: {x:200},
    	}
    	fmt.Println(m, m2)
    }
    
    字典被设计成”not addressable”,故不能直接修改value成员(结构或数组)
    func main() {
        type user struct {
            name string
            age byte
        }
        m := map[int]user{
            1: {"Tom", 19},
        }
        m[1].age += 1 //错误: cannot assign to struct field m[1].age in map
    }
    
    正确做法是返回整个value, 待修改后再设置字典键值,或直接用指针类型。
    在迭代期间删除或新增键值是安全的
    字典对象本身就是指针的包装,传参时无须再次取地址
    func test(x map[string]int) {
    	fmt.Printf("X: %p\n", x)
    }
    
    func main() {
    	m := make(map[string]int)
    	test(m)
    	fmt.Printf("m:%p, %d\n", m, unsafe.Sizeof(m))
    
    	m2 := map[string]int{}
    	test(m2)
    	fmt.Printf("m2:%p, %d\n", m2, unsafe.Sizeof(m2))
    }
    /*
    X: 0xc00005e360
    m:0xc00005e360, 8
    X: 0xc00005e390
    m2:0xc00005e390, 8
    */
    
    在创建时预先准备足够空间有助于提升性能,减少扩张时的内存分配和重新哈希操作

    5.5 结构

    结构体将多个不同类型的命名字段序列打包成一个复合类型
    字段名必须唯一,可用“_“补位,支持使用自身指针类型成员。字段名,排列顺序属类型组成部分。除对齐处理外,编译器不会优化,调整内存布局
    type node struct {
    	_ int
    	id int
    	next *node
    }
    func main() {
    	n1 := node{
    		id:1,
    	}
    	n2 := node {
    		id:2,
    		next:&n1,
    	}
    	fmt.Println(n1, n2)
    }
    
    使用命名方式初始化指定字段
    func main() {
    	type user struct {
    		name string
    		age byte
    	}
    
    	u1 := user{"Tom", 12}
    	u2 := user{"Tom"}		//错误: too few values in struct initializer
    	fmt.Println(u1, u2)
    }
    
    推荐用命名初始化。这样在扩充结构字段或调整字段顺序时,不会导致初始化语句出错。
    可直接定义匿名结构类型变量,或用作字段类型。但因其缺少类型标识。在作为字段类型时无法直接初始化。稍显麻烦
    func main() {
    	u := struct {	//直接定义匿名结构变量
    		name string
    		age byte
    	}{
    		name:"Tom",
    		age:12,
    	}
    	type file struct {
    		name string
    		attr struct {		//定义匿名结构类型字段
    			owner int
    			perm int
    		}
    	}
    
    	f := file {
    		name:"test.dat",
    		/*attr:{	//错误missing type in composite literal
    			owner:1,
    			perm:0755,
    		},*/
    	}
    
    	f.attr.owner = 1	//正确方式
    	f.attr.perm = 0755
    
    	fmt.Println(u, f)
    }
    
    只有在所有字段类型全部支持时,才可做相等操作
    func main() {
    	type data struct {
    		x int
    		y map[string]int
    	}
    	d1 := data{
    		x:100,
    	}
    	d2 := data{
    		x:100,
    	}
    	println(d1 == d2)	//无效操作,struct containing map[string]int cannot be compared
    }
    
    可使用指针直接操作结构字段,但不能是多级指针
    func main() {
    	type user struct {
    		name string
    		age int
    	}
    
    	p := &user{
    		name:"Tom",
    		age:20,
    	}
    	p.name = "Mary"
    	p.age++
    
    	p2 := &p
    	*p2.name = "Jack"	//错误p2.name undefined(type **user  has no field or method name)
    }
    
    空结构(struct{})是指没有字段的结构类型。它比较特殊,因为无论是自身,还是作为数组元素,其长度都为零
    尽管没有分配数组内存,但依然可以操作元素,对应切片len,cap属性也正常.
    这类“长度”为零的对象通常都指向runtime.zerobase变量。
    空结构可以作为通道元素类型,用于事件通知.
    func main() {
    	exit := make(chan struct{})
    
    	go func() {
    		println("hello world")
    		exit <- struct{}{}
    	}()
    
    	<- exit
    	println("end.")
    }
    
    所谓匿名字段,是指没有名字,仅有类型的字段,也被称作嵌入字段或嵌入类型。
    从编译器角度看,这只是隐式地以类型名作为字段名字。可直接引用匿名字段的成员,但初始化时须当作独立字段.
    type attr struct {
    	perm int
    }
    type file struct {
    	name string
    	attr			//仅有类型名
    }
    func main() {
    
    	f := file{
    		name:"test.dat",
    		attr:attr{			//显式初始化匿名字段
    			perm:0755,
    		},
    	}
    	f.perm = 0644	//直接设置匿名字段成员
    	println(f.perm)	//直接读取匿名字段成员
    }
    
    如嵌入其他包中的类型,则隐式字段名字不包括包名
    type data struct {
    	os.File
    }
    func main() {
    	d := data {
    		os.File{},
    	}
    	fmt.Printf("%#v\n", d)
    }
    
    不仅仅是结构体,除接口指针和多级指针以外的任何命名类型都可作为匿名字段。
    type data2 struct {
    	*int		//嵌入指针类型
    	string
    }
    
    func main() {
    	x := 100
    	d := data2 {
    		int:&x,			//使用基础类型作为字段名
    		string:"abc",
    	}
    	fmt.Printf("%#v\n", d)
    }
    
    不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同
    type data struct {
        *int
        int         //错误:duplicate field int
    }
    
    虽然可以像普通字段那样访问匿名字段成员,但会存在重名问题。默认情况下,编译器从当前显式命名字段开始,逐步向内查找匿名字段成员。如匿名字段成员被外层同名字段遮蔽,那么必须使用显式字段名。
    type file struct {
    	name string
    }
    
    type data struct {
    	file
    	name string	//与匿名字段file.name重名
    }
    
    func main() {
    	d := data{
    		name:"data",
    		file:file{"file"},
    	}
    
    	d.name = "data2"		//访问data.name
    	d.file.name = "file2"   //使用显式字段名访问data.file.name
    
    	fmt.Println(d.name,d.file.name)
    }
    
    如果多个相同层级的匿名字段成员重名,就只能使用显式字段名访问,因为编译器无法确定目标
    type file struct {
    	name string
    }
    type log struct {
    	name string
    }
    type data struct {
    	file
    	log
    }
    func main() {
    	d := data{}
    	d.name = "name"	//错误: ambiguous selector d.name
    	d.file.name = "file"
    	d.log.name = "log"
    }
    
    字段标签(tag)并不是注释,而是用来对字段进行描述的元数据。
    在运行期,可用反射获取标签信息。它常被用作格式校验,数据库关系映射等。
    import (
    	"fmt"
    	"reflect"
    )
    
    type user struct {
    	name string `昵称`
    	sex byte `性别`
    }
    func main() {
    	u := user{"Tome",1}
    	v := reflect.ValueOf(u)
    	t := v.Type()
    
    	for i, n :=0, t.NumField(); i<n; i++ {
    		fmt.Printf("%s:%v\n", t.Field(i).Tag, v.Field(i))
    	}
    }
    /**
    昵称:Tome
    性别:1
    **/

    root

    发表回复