Go语言自学笔记(2.3-2.6)
历史:
Go语言自学笔记(0-1.2)
Go语言自学笔记(1.3)
Go语言自学笔记(1.4-1.5)
Go语言自学笔记(1.7-2.2)
github地址
2.3.变量
var 变量名字 类型 = 表达式
零值初始化:数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
可以在一个声明语句中同时声明一组变量
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:
var f, err = os.Open(name) // os.Open returns a file and an error
2.3.1.简短变量声明
以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。
var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方
i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
简短变量声明语句也可以用来声明和初始化一组变量:
i, j := 0, 1
这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。
简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
2.3.2. 指针
如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int
,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p
表达式对应p指针指向的变量的值。一般*p
表达式读取指针指向的变量的值,这里为int类型的值,同时因为*p
对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
2.3.3. new函数
另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
。
p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
2.3.4. 变量的生命周期
变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
一个变量的有效周期只取决于是否可达,一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
Go编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,
🔹1. 栈和堆的区别
- 栈(stack):快速分配/释放,但生命周期仅限于函数调用期间。
- 堆(heap):内存持续时间更长,但分配/回收较慢,需要垃圾回收器(GC)处理。
🔹2. var
和 new
并不决定内存位置
很多语言(比如 C/C++)中,你用 new
就会在堆上分配,用局部变量就会在栈上分配。但 Go 不这样 —— 它通过编译器分析代码来决定变量到底是在堆上还是栈上分配。
这意味着:
✅ 你写 var x int
,它可能在堆上
✅ 你写 y := new(int)
,它可能在栈上
这个决定 不由你代码的语法决定,而是由编译器对变量是否“逃逸”进行判断决定的。
🔹3. 什么是“逃逸”?
“逃逸”指的是一个变量在函数执行完毕后,依然可能被函数外部的代码访问。
如果一个变量在函数返回之后还要继续用,它就不能放在栈上,因为函数返回时,栈空间就会被释放。这样就必须把它放在堆上。
🔹具体例子分析:
🧩 示例 1:
go复制编辑
var global *intfunc f() {var x intx = 1global = &x
}
x
是f
函数内部的变量。- 但我们把
x
的地址&x
存到了全局变量global
中。 - 所以 即使函数
f
返回了,global
依然可以访问x
的值。 - 所以
x
的生命周期超出了函数作用域,也就是“逃逸”了。 - 编译器就会 把
x
放到堆上。
🧩 示例 2:
go复制编辑
func g() {y := new(int)*y = 1
}
- 这里用了
new(int)
,看起来是“堆分配”,但这并不是关键。 y
是局部变量,*y
也只在函数g
中使用。g
返回后,没人再用这个变量了,它不会“逃逸”。- 所以编译器可能 优化成栈上分配,而不是放到堆上。
2.4.赋值
使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。
x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale 或 count[x] *= scale
// 数组、slice或map的元素赋值
数值变量也可以支持++
递增和--
递减语句(译注:自增和自减是语句,而不是表达式
v := 1
v++ // 等价方式 v = v + 1;v 变成 2
v-- // 等价方式 v = v - 1;v 变成 1
2.4.1. 元组赋值
元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。例如我们可以这样交换两个变量的值:
x, y = y, xa[i], a[j] = a[j], a[i]
有些表达式会产生多个值,比如调用一个有多个返回值的函数。
f, err = os.Open("foo.txt") // function call returns two values
和变量声明一样,我们可以用下划线空白标识符_
来丢弃不需要的值。
_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T) // 只检测类型,忽略具体值
2.5.类型
在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份;一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度;一个字符串可以用来表示一个密码或者一个颜色的名称。
type 类型名字 底层类型
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconvimport "fmt"//定义了两个类型,底层都是float64,它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度const (AbsoluteZeroC Celsius = -273.15 // 绝对零度FreezingC Celsius = 0 // 结冰点温度BoilingC Celsius = 100 // 沸水温度
)/*Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用,类型转换不会改变值本身,但是会使它们的语义发生变化。
CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。*/
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0)
)。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。
下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
2.6.包和文件
练习 2.1: 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。
目录结构:
❯ tree
.
├── go.mod
├── main.go
└── tempconv├── conv.go├── go.mod└── tempconv.go
go mod init local/tempconv
go.mod
module local/myappgo 1.24.4replace local/tempconv => ./tempconvrequire local/tempconv v0.0.0-00010101000000-000000000000 // indirect
main.go
package mainimport ("fmt""local/tempconv" // 注意这里的模块名必须与你 go.mod 中的一致
)func main() {k := tempconv.Kelvin(273.15)c := tempconv.KToC(k)fmt.Println("k:", k) // 输出:273.15Kfmt.Println("k to c :", c) // 输出:0°C
}
conv.go
package tempconv// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }func CToK(c Celsius) Kelvin { return Kelvin(c + 273.15) }func KToC(k Kelvin) Celsius { return Celsius(k - 273.15) }
tempconv.go
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconvimport "fmt"type Celsius float64
type Fahrenheit float64
type Kelvin float64const (AbsoluteZeroC Celsius = -273.15FreezingC Celsius = 0BoilingC Celsius = 100AbsoluteZeroK Kelvin = 0
)func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
func (k Kelvin) String() string { return fmt.Sprintf("%gK", k) }
练习 2.2: 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。
// 从输入参数进行重量转换
package mainimport ("fmt""os""strconv"
)type ounce float64
type gram float64func (o ounce) String() string { return fmt.Sprintf("%g oz", o) }
func (g gram) String() string { return fmt.Sprintf("%g g", g) }func OToG(o ounce) gram { return gram(o * 28.3495) }func GToO(g gram) ounce { return ounce(g / 28.3495) }func main() {for _, args := range os.Args[1:] {t, err := strconv.ParseFloat(args, 64)if err != nil {fmt.Fprintf(os.Stderr, "weightconv: %v\n", err)os.Exit(1)}o := ounce(t)g := OToG(o)fmt.Printf("%s = %s\n", o, g)g2 := GToO(g)fmt.Printf("%s = %s\n", g, g2)}
}