• 周五. 11 月 13th, 2026

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注