一、基础数据类型:Go语言的积木块
1.1 数字类型全家福
package mainimport ("fmt"
)func main() {// 有符号整数类型var a int = 42 // int 类型,自动选择32或64位var b int8 = 127 // int8 类型,8位有符号整数var c int16 = 32767 // int16 类型,16位有符号整数var d int32 = 2147483647 // int32 类型,32位有符号整数var e int64 = 9223372036854775807 // int64 类型,64位有符号整数// 无符号整数类型var f uint = 42 // uint 类型,自动选择32或64位var g uint8 = 255 // uint8 类型,8位无符号整数var h uint16 = 65535 // uint16 类型,16位无符号整数var i uint32 = 4294967295 // uint32 类型,32位无符号整数var j uint64 = 18446744073709551615 // uint64 类型,64位无符号整数// 浮点数类型var k float32 = 3.14 // float32 类型,32位浮点数var l float64 = 3.141592653589793 // float64 类型,64位浮点数// 复数类型var m complex64 = 1 + 2i // complex64 类型,实部和虚部都是 float32var n complex128 = 1 + 2i // complex128 类型,实部和虚部都是 float64// 打印每个变量的类型fmt.Printf("%T %T %T %T %T\n", a, b, c, d, e)fmt.Printf("%T %T %T %T %T\n", f, g, h, i, j)fmt.Printf("%T %T\n", k, l)fmt.Printf("%T %T\n", m, n)
}
1.2 字符与布尔类型
var ch byte = 'A' // ASCII字符
var unicodeCh rune = '中' // Unicode字符
var flag bool = true
1.3 字符串的特别之处
Go语言的字符串是不可变的,也就是说,一旦创建后就无法修改其中的内容。如果需要修改字符串的内容,通常的做法是将字符串转换成一个可变的字节切片([]byte
)或字符切片([]rune
),对切片进行修改后再转换回字符串。
s := "hello"
b := []byte(s) // 将字符串转换为字节切片
b[0] = 'H' // 修改字节切片中的内容
s2 := string(b) // 将修改后的字节切片转换回字符串
fmt.Println(s2) // 输出: "Hello"
二、复合数据类型:构建复杂结构
2.1 数组:刻板的容器
特点:
- 长度固定
- 值类型(赋值会复制整个数组)
- 类型包含长度信息:[3]int ≠ [5]int
var arr1 [3]int // [0 0 0]
arr2 := [...]int{1,2,3} // 编译器推导长度
arr3 := [2][3]int{{1,2}, {3}} // 多维数组
痛点时刻:当我们需要动态大小时怎么办?
2.2 切片:灵活的舞者
关键特性:
- 底层引用数组
- 自动扩容机制
- 长度 vs 容量
slice1 := make([]int, 3, 5) // 长度3,容量5
slice2 := arr2[1:3] // 基于数组创建
slice3 := []int{1,2,3} // 直接初始化// 动态操作
slice3 = append(slice3, 4) // 自动扩容
copy(slice1, slice2) // 复制元素
扩容条件:
- 当向切片中追加元素(使用
append
函数)时,如果切片的长度未超过容量,则直接在底层数组上进行追加。 - 如果追加元素后长度超过容量,Go会创建一个新的底层数组,并将旧切片的数据复制到新数组中。
扩容策略(目的:平衡内存使用和性能,避免频繁的内存分配和数据拷贝):
- 如果切片的当前容量小于1024,新的容量一般是原来的2倍。
- 如果切片的当前容量大于等于1024,新的容量会增加为原来的1.25倍(具体倍数可能会根据实现细节有所不同)。
注意事项:
- 扩容会导致新的内存分配和数据拷贝,因此如果预先知道切片的最大容量,最好在创建切片时指定容量以减少扩容的开销。
- 由于扩容会创建新的底层数组,因此在扩容后,旧的切片和新的切片将不再共享同一个底层数组。
2.3 映射:闪电查询专家
特点:
- 键的类型必须是可比较的,而值可以是任意类型。
- Map是引用类型,赋值操作不会复制整个映射。
- 内置函数
make
用于初始化map
,使用时需注意防止空指针异常。 map
本身并不是线程安全的。具体来说,如果多个goroutine
同时对一个map
进行读写操作而没有采取适当的同步措施,那么程序可能会出现竞态条件(race condition),这可能导致不可预测的行为或崩溃。
m1 := make(map[string]int)
m2 := map[string]int{"apple": 5,"pear": 3,
}// 安全操作
if count, exists := m2["banana"]; exists {fmt.Println(count)
}// 遍历(无序!)
for k, v := range m2 {fmt.Println(k, v)
}
2.4 结构体:自定义数据蓝图
type Person struct {Name stringAge intContact struct {Phone stringEmail string}
}p := Person{Name: "Alice",Age: 30,
}
p.Contact.Phone = "123-4567"
三、变量 vs 常量:变与不变的哲学
3.1 变量声明三剑客
特点:
- 作用域:变量的作用域由声明的位置决定,在函数内声明的变量属于局部作用域,全局声明的变量则属于包级作用域。
- 生命周期:局部变量随函数调用结束而销毁,而全局变量则在整个程序运行期间一直存在。
- 可变性:变量在程序运行过程中其值是可以被修改的。
// 标准式
var x int = 10// 类型推导
var y = "hello"// 短声明(这种方式语法简洁,只能在函数内部使用,非常适合局部变量的声明,但不适用于全局变量。)
z := 3.14// 分组声明(不推荐)
var (a = 1b = true
)
3.2 常量的奥妙
常量的特点:
- 不可变性:常量一旦赋值便不能再被修改,适用于需要保持固定值的场景。
- 作用域:与变量类似,常量也有作用域的概念,取决于它们的声明位置。
- 编译期常量:常量的值在编译期间确定,因此在运行时具有更高的效率。
- 无地址分配:常量没有在内存中占据一个特定的地址,因为它们的值是直接嵌入到使用它们的代码中的。
const PI = 3.1415
const (Red = iota // 0Green // 1Blue // 2
)// 编译期计算
const MatrixSize = 10 * 10
关键差异:
特性 | 变量 | 常量 |
---|---|---|
可修改 | ✅ | ❌ |
内存地址 | ✅ | ❌ |
类型推导 | 支持 | 必须显式 |
作用域 | 相同 | 相同 |
3.3 生命周期探秘
var globalVar = "永生" // 包生命周期func demo() {localVar := "临时工" // 函数执行期间存在fmt.Println(localVar)
}
内存小剧场:当函数执行结束,"临时工"会被垃圾回收器请出场外~
四、互动与思考
在学习过程中,我鼓励大家不断思考并动手实践。以下是一些问题和讨论话题,希望你能参与进来:
- 数据类型选择:在你以往的开发经历中,有没有遇到过数据类型选择不当导致性能问题的情况?你认为如何在保证代码简洁的前提下选择合适的数据类型?
- 复合数据类型使用场景:你觉得在何种场景下数组更合适,何种场景下又应该优先选择切片或映射?分享你的思考和实际案例吧!
- 变量和常量的管理:在大型项目中,如何合理规划变量和常量的作用域和生命周期?这不仅关乎代码质量,也涉及到团队协作和维护成本。
欢迎大家在评论区留言讨论,我们一起分享心得,共同进步!
学习检查清单
✅ 能说出3种数字类型的区别
✅ 会正确使用切片扩容
✅ 能区分 var 与 := 的使用场景
✅ 知道常量为什么不能取地址