第1章 类型

本文详细介绍了Go语言的基本语法和特性,包括变量的赋值规则、常量的定义与使用、基本类型及其区别、引用类型的工作原理、类型转换的注意事项、字符串的特性和操作、指针的运用以及自定义类型的创建。此外,还讨论了堆栈空间、内存管理和类型转换的细节,展示了Go语言的内存管理策略和类型系统的灵活性。
摘要由CSDN通过智能技术生成

1.1变量

多变量赋值时,先计算所有相关值,然后再从左到右依次赋值

data, i := [3]int{0, 1, 2}, 0
fmt.Printf("初始化变量 data=%v, i=%v \n", data, i)
i, data[i] = 2, 100
fmt.Printf("赋值后的变量 data=%v, i=%v", data, i)
/*
	初始化变量 data=[0 1 2], i=0
	赋值后的变量 data=[100 1 2], i=2
*/

重新赋值和定义新的同名变量是有区别的,同名的变量需要在不同层次的代码块中

s := "abc"
fmt.Printf("申明新的变量s的变量地址=%v \n", &s)

s, y := "hello", 20 //重新赋值,将先前申明的s变量重新赋值
fmt.Printf("重新赋值s的变量地址=%v, y变量=%v \n", &s, y)

{
	s, z := 1000, 30 //在不同层次的代码块中定义同名变量
	fmt.Printf("不同层次代码块中赋值的新变量s的地址=%v, 变量z=%v", &s, z)
}
/*
	申明新的变量s的变量地址=0xc000088220
	重新赋值s的变量地址=0xc000088220, y变量=20
	不同层次代码块中赋值的新变量s的地址=0xc0000aa058, 变量z=30
*/

1.2常量

在常量组中,如不提供类型和初始化值,那么视作与上一常量相同

const (
	s = "abc"
	x 			//x="abc"
)

如果常量类型足以存储初始化值,那么不会引发溢出错误。

const (
	a byte = 100  	//int to byte
	b int  = 1e20 	//float64 to int, overflows
)
//此时的b常量引发了溢出错误

关于枚举,关键字iota定义常量组中从0开始按行计数的自增枚举值。

const (
	_        = iota             //iota=0
	KB int64 = 1 << (10 * iota) //iota=1 此时KB=2e10=1024
	MB                          //与KB表达式相同,但iota=2
	GB
	TB
)

如果iota自增被打断,需要显式恢复。

const (
	A = iota //iota=0
	B        //iota=1
	C = "c"  //C=c
	D        //D=c,与上一行相同
	E = iota //iota=4,显式恢复,注意计数包含了C、D两行
	F        //iota=5
)

可通过自定义类型来实现枚举类型限制

type Color int

const (
	Black Color = iota
	Red
	Blue
)

func PrintColor(c Color) {
	fmt.Printf("打印颜色为 %v \n", c)
}
func main() {
	c := Black
	PrintColor(c)

	// x := 1
	// PrintColor(x) //cannot use x (type int) as type Color in argument to PrintColor

	PrintColor(1) //常量会被编译器自动转换
}

1.3基本类型

有符号和无符号的区别在于举个栗子:
int8范围在-128~127之间
unit8范围在0~255之间
总的个数为2e8=256个数值

值类型:声明变量之后可以直接使用
array 列表 值类型
struct 结构体 值类型
string UTF-8字符串 值类型

引用类型:对于引用类型,若使用var进行声明变量,必须使用make函数对其进行分配内存。若不初始化,该变量默认值为nil,向其添加元素时会导致panic。
slice 切片 引用类型
map 字典 引用类型
channel 通道 引用类型
interface 接口 引用类型

1.4引用类型

引用类型包括slice、map、channel、interface,它们有复杂的内部结构,除了申请内存外还需要初始化相关属性。
内置函数new计算类型大小,为其分配零值内存,返回指针。
而make会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针

a := []int{0, 0, 0} //初始化时提供初始化值
a[1] = 10

b := make([]int, 3) //make slice,初始化值和内存地址
b[1] = 10

c := new([]int) //仅仅只是分配地址,并没有分配内存
c[1] = 10       //Error: invalid operation: c[1] (type *[]int does not support indexing)

1.5类型转换

不支持隐式转换,即便是从窄向宽转换也不行。

var b byte = 100
// var n int = b //Error: cannot use b (type byte) as type int in assignment
var n int = int(b) //显式转换
fmt.Printf("申明变量n=%v", n)

使用括号避免优先级错误,也不能将其他类型当bool值使用

*Point(p) 		// 相当于*(Point(p))
(*Point)(p)
<-chan int(c) 	//相当于<-(chan int(c))
(<-chan int)(c)

a := 100
if a {					//Error: non-bool a (type int) used as if condition
	fmt.Printf("true")
}

1.6字符串

字符串是不可变值类型,内部用指针指向UTF-8字节数组
不能用序号获取字节元素指针,如:&s[i]非法
不可变类型,无法修改字节数组
字节数组尾部不包含NULL

struct String
{
	byte*	str;
	intgo	len;
};

使用“`”定义不做转移处理的原始字符串,支持跨行。

s := `a
b\r\n\x00
c`
fmt.Printf("使用“`”定义不做转移处理的原始字符串,s=%v", s)

连接跨行字符串时,“+”必须在上一行末尾,否则导致编译错误

s := "Hello, " +
		"word!"
fmt.Printf("字符串s=%v \n", s)
s1 := "Hello, "
+"word!" 	//Error:invalid operation: +"word!" (operator + not defined on untyped string)
fmt.Printf("字符串s1=%v \n", s1)

字符串支持两个索引号返回子串,但是子串依然指向原字节数组,仅修改了指针和长度属性。
单引号字符常量表示Unicode Code Point,支持\uFFFF、\U7FFFFFFF、\xFF格式,对应rune类型(rune 是int32的别名),UCS-4
注意:

  1. 值得一提的是Unicode 是国际通用字符集,涵盖了全球所有字符,并规定了每个字符对应到唯一的代码值(code point),代码值 从 0000 ~ 10FFFF 共 1114112 个值 。
  2. 而对这些字符进行编码的方式则多种多样。真正存储的时候需要多少个字节是由具体的编码格式决定的。比如:字符 「A」用 UTF-8 的格式编码来存储就只占用1个字节,用 UTF-16 就占用2个字节,而用 UTF-32 存储就占用4个字节。
fmt.Printf("字符串'a'的类型=%T \n", 'a')
var c1, c2 rune = '\u6211', '们'
fmt.Printf("(c1=='我')=%v,(string(c2)=='\xe4\xbb\xac')=%v \n", c1 == '我', string(c2) == "\xe4\xbb\xac")
/*
	字符串'a'的类型=int32 
	(c1=='我')=true,(string(c2)=='们')=true
*/

要修改字符串,可先将其转换成[]rune或[]byte,完成后再转换为string,无论哪种转换,都会重新分配内存,并复制字节数组。

s := "abcd"
bs := []byte(s)

bs[1] = 'B'
fmt.Printf("字符串bs=%v \n", string(bs))

u := "电脑"
us := []rune(u)

us[1] = '话'
fmt.Printf("字符串us=%v \n", string(us))
/*
	字符串bs=aBcd
	字符串us=电话
*/

用for循环遍历字符串时,也有byte和rune两种方式。
注意:Go语言中byte和rune实质上就是uint8和int32类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point

s := "abc汉字"

for i := 0; i < len(s); i++ { //byte
	fmt.Printf("%c,", s[i])
}

fmt.Println()
for _, r := range s { //rune
	fmt.Printf("%c,", r)
}

/*
	a,b,c,æ,±,,å,­,,
	a,b,c,汉,字,
*/

1.7指针

支持指针类型T,指针的指针**T。
默认值为nil,没有NULL常量
操作符“&”取变量地址,“
”透过指针访问目标对象。
不支持指针运算,不支持“->”运算符,直接用“.”访问目标成员。
在这里插入图片描述注意

  1. 具体类型的指针,如*int,*string。
  2. unsafe.Pointer,在unsafe下面。
  3. uintptr,能存储任何类型的指针的类型。
  4. 不能对指针做加减法等运算。
type data struct {
		a int
	}

var d = data{1234}
var p *data

p = &d

fmt.Printf("%p, %v \n", p, p.a)

/*
	0xc000014098, 1234
*/

可以在unsafe.Pointer和任意类型指针间进行转换。

x := 0x12345678
p := unsafe.Pointer(&x) //将x的地址转换成通用的指针类型 *int -> Pointer
n := (*[4]byte)(p)      //Pointer -> *[4]byte

for i := 0; i < len(n); i++ {
	fmt.Printf("%X ", n[i])
}

/*
	78 56 34 12
*/

unsafe.Pointer 和 uintptr的区别

  1. uintptr是一个整数类型。即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收。
  2. unsafe.Pointer是一个指针类型。但是unsafe.Pointer值不能被取消引用。如果unsafe.Pointer变量仍然有效,则由unsafe.Pointer变量表示的地址处的数据不会被GC回收。unsafe.Pointer是一个通用的指针类型,就像* int等。

由于uintptr是一个整数类型,uintptr值可以进行算术运算。 所以通过使用uintptr和unsafe.Pointer,我们可以绕过限制,* T值不能在Golang中计算偏移量

注意:GC把uintptr当成普通整数对象,它无法阻止“关联”对象被回收。

a := [4]int{0, 1, 2, 3}
p1 := unsafe.Pointer(&a[1])
p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
*(*int)(p3) = 6
fmt.Println("a =", a) // a = [0 1 2 6]

type Person struct {
   name   string
   age    int
   gender bool
}

who := Person{"John", 30, true}
pp := unsafe.Pointer(&who)
pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
*pname = "Alice"
*page = 28
*pgender = false
fmt.Println(who) // {Alice 28 false}

返回局部变量指针是安全的,编译器会根据需要将其分配在GC Heap上。

func test() *int {
	x := 100
	return &x		//在堆上分配x内存,但在内联时,也可能直接分配在目标栈。
}

堆和栈空间

  1. 栈空间(stack),我们在程序中所定义的定义的局部变量int、局部数组等都是存储在栈空间中。栈空间具有一个鲜明的特点:函数内定义的变量出了函数范围,其所占用的内存空间自动释放。但是,栈空间的尺寸有最大限制,不适合分配大空间使用;所以,因为栈空间出了函数范围就释放,所以不适合要给其他地方使用的内存需求。其最大的好处就在于:不用程序员手动释放内存。因此通常建议不要把局部变量的指针做为返回值返回,但是在go语言中是允许的。
  2. 堆空间(heap),一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收。它与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表。用于存放程序运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
  3. 对于go语言来说,Go的编译器会决定在哪(堆or栈)分配内存,保证程序的正确性。Go的编译器很聪明(自作聪明),它还会做逃逸分析(escape analysis),如果它发现变量的作用域没有跑出太远,它就可以在栈上分配空间而不是堆。比如如下这段代码,就不会在堆上分配内存,即使我们用new分配。
const Width, Height = 640, 480
type Cursor struct {
    X, Y int
}

func Center(c *Cursor) {
    c.X += Width / 2
    c.Y += Height / 2
}

func CenterCursor() {
    c := new(Cursor)
    Center(c)
    fmt.Println(c.X, c.Y)
}
//经过变量逃逸分析
/*
go tool compile -m test.go

test.go:17: can inline Center
test.go:24: inlining call to Center
test.go:25: c.X escapes to heap
test.go:25: c.Y escapes to heap
test.go:23: CenterCursor new(Cursor) does not escape
test.go:25: CenterCursor ... argument does not escape
test.go:17: Center c does not escape
*/
//参数-m是打印出编译优化。从输出上看,它说new(Cursor)没有escape,于是在栈上分配了。
//Go一方面会把一些,看上去会在栈上分配的东西,移到堆上分配;另一方面又会把看上去会在堆上分配的东西,在栈上分配。由编译优化那边做逃逸分析来控制。

1.8自定义类型

可将类型分为命名和未命名两大类。命名类型包括bool、int、string等,而array、slice、map等和具体元素类型、长度等有关,属于未命名类型。具有相同声明的未命名类型被视为同一类型。

具有相同基类型的指针
具有相同元素类型和长度的array
具有相同元素类型的slice
具有相同键值类型的map
具有相同元素类新型和传送方向的channel
具有相同字段序列(z字段名、类型、标签、顺序)的匿名struct
签名相同(参数和返回值,不包括参数名称)的function
方法集相同(方法名、方法签名相同,和次序无关)的interface

var a struct {
	x int `a`
}
var b struct {
	x int `ab`
}
b = a 	//Error: cannot use a (type struct { x int "a" }) as type struct { x int "ab" } in assignment

可用type在全局或函数内定义新类型

type bigint int64
var x bigint = 100
fmt.Printf("新类型变量值=%v", x)

新类型不是原类型的别名,除拥有相同数据存储结构外它们之间没有任何关系,不会持有原类型任何信息,除非目标类型是未命名类型,否则必须显示转换

type bigint int64

x := 1234
var b bigint = bigint(x) //必须显示转换,除非是常量
var b2 int64 = int64(b)
fmt.Printf("int64变量b2=%v \n", b2)

type myslice []int
   
var s myslice = []int{1, 2, 3} //未命名类型,隐式转换
var s2 []int = s
fmt.Printf("int变量s2=%v", s2)

有趣的是,虽然所有数值类型如整形int、字符串类型string、布尔类型bool,都是defined type;defined type意味着必须要显示转换
但是map、数组、切片、结构体、channel等原生复合类型(composite type)都不是defined type。意味着这些符合类型却可以支持隐式转换。

package main
 
type MyInt int
type MyMap map[string]int
 
func main() {
    var x MyInt
    var y int 
    x = y     // 会报错: cannot use y (type int) as type MyInt in assignment
    _ = x 
 
    var m1 MyMap
    var m2 map[string]int
    m1 = m2 // 不会报错
    m2 = m1 // 不会报错
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值