ps: 本书时间有点老(15年),读的时候需慎重- 本文章只是对这本书的读后感,感兴趣请购买正版
- 阅读本文之前,希望你已经基本了解了
go的基本数据类型以及流程控制
一. 入门
-
1.1 变量声明
s := "" // 只作用于当前作用域,即{}内
var s string // 依赖string类型的内部初始化机制,初始化为空字符串
var s = "" // 很少使用
var s string = "" // 显示标明变量类型
// 通常用到第一种第二种为主
package main
import "fmt"
func main() {
var a = 1
b := 2
{
b = 3
a = 4
fmt.Println(a)
fmt.Println(b)
// 3,4
}
func() {
a = 5
b = 6
fmt.Println(a)
fmt.Println(b)
// 5,6
}()
fmt.Println(a)
fmt.Println(b)
// 5,6
}
-
1.2 map迭代
map_exm = make(map[string]int)
map_exm["test"] = 1
map_exm["test_ii"] = 2
for k,v := range map_exm {
fmt.Println(k,v)
}
对map进行range循环的时候,其迭代顺序是不确定的,从实践上看,可能每次运行结果都不一样
-
1.3 fmt.printf(),fmt.sprintf()
%d int
%x, %o, %b 16进制,8进制,2进制度
%f, %g, %e 浮点数 e是科学计数法
%t 布尔变量
%c rune,unicode
%s string
%q 带双引号string或者单引号的rune
%v 变量
%T 类型
-
1.4 简单的go demo
// 获取单个
func TestGetUrlBody(){
url := "http://yourtest.com"
resp,err := http.Get(url)
if err != nil {
// os.Stderr 输出在屏幕
// os.Stdout 输出在文件
fmt.Fprintf(os.Stderr, "%v", err)
// 让当前程序退出,并且不会执行defer函数
os.Exit(1)
}
b,err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "%s:%v", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
// goroutine 同时获取多个
func TestGetUrlBody(t *testing.T) {
url := "http://www.baidu.com"
resp, err := http.Get(url)
if err != nil {
// os.Stderr 输出在屏幕
// os.Stdout 输出在文件
fmt.Fprintf(os.Stderr, "%v", err)
t.Logf("%v", err)
// 让当前程序退出,并且不会执行defer函数
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "%s:%v", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
func TestGetUrlBodyAsyncs(t *testing.T) {
url := "http://www.baidu.com"
ch := make(chan string, 10)
for i := 0; i < 2; i++ {
go asyncHandle(url, ch)
}
for temp := range ch {
fmt.Println(temp)
}
}
func asyncHandle(url string, ch chan string) {
resp, err := http.Get(url)
if err != nil {
// os.Stderr 输出在屏幕
// os.Stdout 输出在文件
ch <- fmt.Sprintf("%v", err)
// 让当前程序退出,并且不会执行defer函数
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
ch <- fmt.Sprintf("%v", err)
os.Exit(1)
}
ch <- string(b)
}
二. 程序结构
-
1.指针
指针的值是变量的内存空间地址。
func f() *int {
v := 1
return &v
}
var p = f()
fmt.Println(p == f());
/*
这段代码的输出结果是 `false`。
在这段代码中,`f()` 函数返回一个指向局部变量 `v` 的指针。
当我们在 `main` 函数中调用 `f()` 时,
它会返回一个指向新的 `v` 变量的指针。
因此,`p` 和 `f()` 返回的指针是不同的,它们指向不同的变量。
所以 `p == f()` 的结果是 `false`。
*/
-
2.new函数
p := new(int)
*p = 2
fmt.Println(*p)
/**
这段代码的输出结果是 `2`。
在这段代码中,我们使用 `new` 函数创建了一个新的 `int` 类型的指针 `p`,
并将其初始化为 `nil`。
然后,我们将 `p` 指向的内存地址上的值设置为 `2`,
并使用 `*p` 访问该值。
最后,我们使用 `fmt.Println` 函数将该值打印到控制台上。
*/
p := new(int)
q := new(int)
fmt.Println(p == q) // false
fmt.Println(*p == *q) // true
/**
在这段代码中,我们使用 `new` 函数创建了两个新的 `int` 类型的指针 `p` 和 `q`,
并将它们初始化为 `nil`。
然后,我们使用 `==` 运算符比较 `p` 和 `q`,
由于它们指向不同的内存地址,所以比较结果为 `false`。
接着,我们使用 `*` 运算符访问 `p` 和 `q` 指向的内存地址上的值,
由于它们都是新创建的 `int` 类型变量,所以它们的值相等,比较结果为 `true`。
*/
-
3.变量的生命周期与作用域
在 Go 中,变量的生命周期由其作用域和存储位置决定。当一个变量被声明时,它会被分配到内存中的某个位置,并在其作用域内可见。当变量超出其作用域时,它将被销毁并释放其占用的内存。 在GO中,变量的作用域是指该变量名的有效范围
以下是 Go 变量的生命周期的一些常见情况:
- 局部变量:当一个局部变量被声明时,它会被分配到栈上,并在其作用域内可见。当函数返回时,该变量将被销毁并释放其占用的内存。
- 全局变量:全局变量在程序启动时被分配到内存中的静态存储区,并在整个程序的执行期间都可见。当程序结束时,该变量将被销毁并释放其占用的内存。
- 堆上分配的变量:当使用
new或make函数创建一个变量时,它将被分配到堆上,并在整个程序的执行期间都可见。当该变量不再被引用时,它将被垃圾回收器回收并释放其占用的内存。 - 闭包变量:当一个变量被捕获到闭包中时,它将被分配到堆上,并在整个闭包的执行期间都可见。当闭包不再被引用时,它将被垃圾回收器回收并释放其占用的内存。
总之,Go 变量的生命周期由其作用域和存储位置决定。在函数内部声明的变量通常在函数返回时被销毁,而在堆上分配的变量通常在不再被引用时被垃圾回收器回收。
以下是go变量作用域的一些常见情况: 在 Go 中,变量的作用域由它们声明的位置决定。一般来说,变量的作用域可以分为以下几种情况:
- 函数内部的变量只在该函数内部可见,称为局部变量。
- 函数外部的变量在整个包内可见,称为全局变量。
- 函数参数的作用域只在该函数内部可见,称为形式参数。
package main
import "fmt"
var globalVar = "global"
func main() {
localVar := "local"
fmt.Println(localVar)
fmt.Println(globalVar)
printVar()
}
func printVar() {
fmt.Println(globalVar)
}
/**
打印的结果为:
local
global
global
*/
-
4.赋值
v, ok = m[key]
// 对于非接口类型的变量,不能使用类型断言。
v, ok = x.(T)
v, ok = <- ch
v, ok = m[key] 用于从 map 中获取指定 key 对应的 value 值,如果 key 存在,则将其对应的 value 值赋值给 v,并将 ok 设为 true;如果 key 不存在,则将 v 设为该类型的零值,将 ok 设为 false。
v, ok = x.(T) 用于将接口类型 x 转换为类型 T,如果转换成功,则将转换后的值赋值给 v,并将 ok 设为 true;如果转换失败,则将 v 设为该类型的零值,将 ok 设为 false。
v, ok = <- ch 用于从通道 ch 中接收一个值,如果通道没有关闭且有值可接收,则将接收到的值赋值给 v,并将 ok 设为 true;如果通道已关闭或没有值可接收,则将 v 设为该类型的零值,将 ok 设为 false。
-
5.类型转换T
在 Go 中,类型转换可以使用 T() 的形式进行。其中 T 表示要转换的目标类型,可以是内置类型、自定义类型或接口类型。
如果要将一个值 x 转换为类型 T,则可以使用 T(x) 的形式进行转换。如果 x 的类型和 T 的类型不兼容,则会在编译时产生错误。
下面是一个示例,将一个 int 类型的值转换为 float64 类型:
package main
import "fmt"
func main() {
x := 42
y := float64(x)
fmt.Printf("x = %d, y = %f\n", x, y)
}
在 Go 中,类型转换 T() 有一些需要注意的事项:
- 只能在兼容的类型之间进行转换。例如,不能将一个字符串转换为整数类型,因为它们的类型不兼容。
- 转换后的值可能会发生截断或精度丢失。例如,将一个浮点数转换为整数类型时,小数部分将被截断。
- 对于指针类型,可以将一个指向某个类型的指针转换为指向另一个类型的指针。但是,需要注意的是,这种转换可能会导致指针指向的内存地址发生变化。
- 对于接口类型,可以将一个实现了某个接口的类型转换为该接口类型。但是,需要注意的是,这种转换可能会导致接口值的动态类型和动态值发生变化。
三. 基础数据类型
1.rune,byte,unitptr
package main
import "fmt"
func main() {
// rune 类型表示一个 Unicode 码点
var r rune = '世'
// %c用来表示一个字符,如果表示字符串,使用%s
fmt.Printf("rune: %c, type: %T\n", r, r)
// byte 类型表示一个字节
var b byte = 'A'
fmt.Printf("byte: %c, type: %T\n", b, b)
// uintptr 类型表示一个指针的整数值
var x int = 42
var p uintptr = uintptr(unsafe.Pointer(&x))
fmt.Printf("uintptr: %d, type: %T\n", p, p)
}
/**
输出结果为:
rune: 世, type: int32
byte: A, type: uint8
uintptr: 824634956288, type: uintptr
*/
在这个示例中,我们可以看到:
rune类型可以用来表示 Unicode 码点,它的实际类型是int32。byte类型可以用来表示一个字节,它的实际类型是uint8。uintptr类型可以用来表示一个指针的整数值,它的实际类型是一个无符号整数类型,大小和指针的大小相同。
需要注意的是,uintptr 类型通常用于底层编程,不建议在普通的应用程序中使用。
在Go中,rune和byte都是基本数据类型,但它们有着不同的用途和特点。
rune跟byte的详细区别:
rune是一个32位的Unicode字符,它可以表示任何Unicode码点。在Go中,rune通常用于处理文本和字符串,因为它可以表示任何语言的字符。例如,可以使用rune类型来遍历字符串中的每个字符,或者将字符串转换为rune切片以进行更高级的文本处理。
byte是一个8位的无符号整数,它通常用于处理二进制数据或字节流。在Go中,byte类型通常用于读取和写入文件、网络连接或其他数据源中的字节流。例如,可以使用byte类型来读取文件中的字节,或者将字符串转换为byte切片以进行网络传输。
请注意,rune和byte类型之间可以进行转换,但是这通常需要进行显式的类型转换。在处理文本和字符串时,应该使用rune类型,而在处理二进制数据和字节流时,应该使用byte类型。
2.算数运算符的抛出问题
package main
import "fmt"
func main() {
var x uint8 = 255
x = x + 1
fmt.Println(x)
}
// 结果为0
package main
import "fmt"
func main() {
var x int32 = 2147483647
x = x * 2
fmt.Println(x)
}
// 结果为-2
3.整型以及浮点数打印的问题
package main
import "fmt"
func main() {
x := 12345
fmt.Printf("%05d\n", x) // 输出:12345
fmt.Printf("%010d\n", x) // 输出:0000012345
x := 3.141592653589793
fmt.Printf("%.2f\n", x) // 输出:3.14
fmt.Printf("%.5f\n", x) // 输出:3.14159
// 定义自定义位数的浮点数
// 例如定义99位
x := new(big.Float).SetPrec(99).SetFloat64(3.141592653589793)
fmt.Println(x)
}
4 常量
const Pi64 float32 = math.Pi
var x float64 = float64(Pi64)
var z complex128 = complex128(Pi64)
fmt.Println(x)
// 3.141592653589793
fmt.Println(z)
// 3.141592653589793+0i
fmt.Println(Pi64)
// 3.1415927
我们可以明显看到,常量可以是无类型的,当常量被赋值给一个变量的时候,无类型的常量将 会被隐性的转换为对应的类型。
下面是常量的一些注意事项:
- 常量必须在声明时进行初始化。
- 常量的值必须是编译时可确定的,例如数字、字符串或布尔值。
- 常量可以是任何基本类型,如整数、浮点数、字符串和布尔值。
- 常量的名称应该使用大写字母,以便于区分变量和常量。
- 常量可以在函数内部声明,但是只能在函数内部使用。
- 常量可以用于枚举类型,例如定义一组相关的常量。
四 复合数据类型
1 数组,slice
数组是定长的
// 一个简单的例子
func TestArr(t *testing.T) {
var a = [...]int{1, 2, 3, 4, 5, 6, 7}
b := a[3:6]
c := a[2:5]
a[3] = 23
// 在go run test的时候打印b和c的值
t.Errorf("b should be [4 5 23], but got %v", b)
// 23 5 6
t.Errorf("c should be [3 4 23], but got %v", c)
// 3 23 5
t.Log("All tests passed!")
}
2.map
- map,slice的比较
// (1)手动比较
func equalMaps(a, b map[string]int) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if b[k] != v {
return false
}
}
return true
}
// (2)使用deepequal
func equalStructs(a, b MyStruct) bool {
return reflect.DeepEqual(a, b)
}
func equalStructs(a, b map[string]int) bool {
return reflect.DeepEqual(a, b)
}
需要注意的是,使用`reflect.DeepEqual`函数比较map时,
它会比较map的键和值的顺序。
因此,如果两个map的键和值相同但顺序不同,
`DeepEqual`函数也会返回`false`。
// (3)可以使用cmp.Equal比较
func TestEqual(t *testing.T) {
a := map[string]int{"foo": 1, "bar": 2}
b := map[string]int{"bar": 2, "foo": 1}
if !cmp.Equal(a, b) {
t.Errorf("a and b should be equal, but they are not")
}
c := Person{Name: "Alice", Age: 30}
d := Person{Name: "Alice", Age: 30}
if !cmp.Equal(c, d) {
fmt.Println("c and d are not equal")
}
e := []int{1, 2, 3}
f := []int{1, 2, 3}
if !cmp.Equal(e, f) {
fmt.Println("e and f are not equal")
}
}
- map的元素,并不是一个变量,我们无法对map的元素进行取址操作
- map的有序遍历
package main
import (
"fmt"
"sort"
)
func main() {
// 定义一个map
m := make(map[string]int)
// 填充元素
m["apple"] = 2
m["banana"] = 3
m["orange"] = 1
// 获取map的key列表
var keys []string
for k := range m {
keys = append(keys, k)
}
// 对key列表进行排序
sort.Strings(keys)
// 遍历有序的key列表,并输出对应的value
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
}
-
判断某个元素是否存在
test,ok := tests["one"] -
思考一:
var m = make(map[int]int,10)
m1 := m[1]
m2,ok := m[2]
fmt.Println(m1,m2,ok)
// 0 0 false
- 思考二 map扩容之后delete会缩容吗
map的元素数量减少时,Go 会自动缩容 map,以便释放未使用的内存。因此,不需要手动触发 map 的缩容。 map 的缩容是在运行时进行的,并且不保证立即发生。当 map 中的元素数量减少时,Go 可能会等待一段时间,直到有足够的空闲空间来缩小 map 的容量。
- 思考三 一些map内置的函数
3.struct与json
- 字段名首字母大写:在Go语言中,如果一个struct的字段名首字母大写,那么它就是可导出的(exported),可以被其他包访问。如果字段名首字母小写,则它是不可导出的(unexported),只能在当前包内访问
- 进行
==|!=的时候,字段类型必须是可比较的:在Go语言中,struct的字段类型必须是可比较的,这意味着它们必须支持==和!=操作符。例如,slice、map和function类型都不是可比较的,因此不能直接用作struct的字段类型。
// 可比较的类型
type Person struct {
Name string
Age int
}
type Book struct {
Title string
Author string
}
type Point struct {
X int
Y int
}
type Date struct {
Year int
Month int
Day int
}
// 不可比较的类型
type Employee struct {
Name string
Age int
Salary []int // slice类型不可比较
}
// 例如下面
type Employee struct {
Name string
Age int
[]int // 就会出现编译错误
}
type Car struct {
Brand string
Model string
Engine func() // function类型不可比较
}
type Location struct {
Latitude float64
Longitude float64
Altitude float64
Time time.Time // time.Time类型不可比较
}
- 嵌入其他struct:在Go语言中,可以通过嵌入其他struct来实现继承。嵌入的struct可以访问其字段和方法,就像它们是当前struct的字段一样。嵌入的struct可以是指针类型或非指针类型。
- 匿名字段:在Go语言中,可以使用匿名字段来简化struct的定义。匿名字段是指没有字段名的字段,只有字段类型。在访问匿名字段时,可以直接使用字段类型作为字段名
// 示例1:嵌入非指针类型的struct
type Person struct {
Name string
Age int
}
type Employee struct {
Person
Salary float64
}
func main() {
emp := Employee{
Person: Person{
Name: "Alice",
Age: 30,
},
Salary: 5000.0,
}
fmt.Println(emp.Name) // 直接访问嵌入的Person struct的Name字段
fmt.Println(emp.Age) // 直接访问嵌入的Person struct的Age字段
fmt.Println(emp.Salary)
}
// 示例2:嵌入指针类型的struct
type Person struct {
Name string
Age int
}
type Employee struct {
*Person
Salary float64
}
func main() {
p := &Person{
Name: "Alice",
Age: 30,
}
emp := Employee{
Person: p,
Salary: 5000.0,
}
fmt.Println(emp.Name) // 直接访问嵌入的Person struct的Name字段
fmt.Println(emp.Age) // 直接访问嵌入的Person struct的Age字段
fmt.Println(emp.Salary)
}
// 示例3:嵌入多个struct
type Address struct {
Province string
City string
}
type Person struct {
Name string
Age int
Address // 匿名字段
}
type Employee struct {
*Person
Salary float64
}
func main() {
p := &Person{
Name: "Alice",
Age: 30,
Address: Address{
Province: "Guangdong",
City: "Shenzhen",
},
}
emp := Employee{
Person: p,
Salary: 5000.0,
}
fmt.Println(emp.Name) // 直接访问嵌入的Person struct的Name字段
fmt.Println(emp.Age) // 直接访问嵌入的Person struct的Age字段
fmt.Println(emp.Province) // 直接访问嵌入的Address struct的Province字段
fmt.Println(emp.City) // 直接访问嵌入的Address struct的City字段
fmt.Println(emp.Salary)
}
4.context
func main() {
func0()
}
func func0() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func1(ctx)
go func2(ctx)
time.Sleep(1 * time.Second)
}
func func1(ctx context.Context) {
fmt.Println("这里是func1")
}
func func2(ctx context.Context) {
fmt.Println("这里是func2")
go func3(ctx)
}
func func3(ctx context.Context) {
fmt.Println("这里是func3")
}
// 这里是func2
// 这里是func1
// 这里是func3
五 函数与方法
1.函数
- 函数的形参与又名返回值作为函数最外层的局部变量,被存储在相同的语法块中
2.多返回值
- 如果一个函数将所有的返回值都命名为变量名,那么该函数直接可以
return进行返回,我们称之为bare return - 如果有多处返回的时候,容易丢失原有语境,因此本书并不建议过度使用bare return
3.错误
- 包错误只向上返回,最终在main中进行panic
- 错误传递途中,如果需要对错误进行处理,尽量使用fmt.Errorf()
4.匿名函数
- 在go中可以使用匿名函数(anonymous function)来定义没有名字的函数。匿名函数可以直接赋值给变量、作为参数传递给其他函数、或者在其他函数内部定义和调用。
- 在匿名函数中,变量的生命周期并不由它的作用域决定。变量的生命周期取决于该匿名函数什么时候在栈中被销毁。
// 示例1:将匿名函数赋值给变量
add := func(a, b int) int {
return a + b
}
sum := add(1, 2)
fmt.Println(sum) // 输出3
// 示例2:将匿名函数作为参数传递给其他函数
func apply(f func(int) int, x int) int {
return f(x)
}
square := func(x int) int {
return x * x
}
result := apply(square, 3)
fmt.Println(result) // 输出9
// 示例3:在其他函数内部定义和调用匿名函数
func main() {
x := 1
func() {
x++
fmt.Println(x) // 输出2
}()
fmt.Println(x) // 输出1
}
六 go的内存管理
1.内存分配
- 内存分配
自动allocator(分配),自动collector(回收)
-
内存分配的三个角色
Mutator:指程序中的执行部分,也就是分配和使用内存的代码。Mutator 会在堆上分配内存,创建对象,并在对象之间建立引用关系。
Allocator:指垃圾回收器中的分配器,负责为 Mutator 分配内存。Allocator 会维护一个空闲内存池,当 Mutator 请求内存时,Allocator 会从空闲内存池中分配一块内存,并返回给 Mutator。
Collector:指垃圾回收器中的收集器,负责回收不再使用的内存。Collector 会定期扫描堆上的对象,标记出所有仍然被引用的对象,然后回收所有未被标记的对象。
-
- 进程虚拟内存分布
- 举例32位linux内存分布
2.Allocator基础
- 基本的分配器类型
- Bump Allocator 线性分配器
- Free List Allocator 空闲链表分配器(演示地址)
- First-Fit
- Next-Fit
- Best-Fit
- Segregated-Fit(分级匹配算法,go就是这种算法的改进版本)
3.Malloc基础
Malloc实际上也是first-fit的算法的一种实现
brk是调整其堆顶的位置从而调整其虚拟内存的大小 mmap是从虚拟堆的任意一个位置调整其虚拟内存大小
- 引申,malloc的应用实践
可以了解一下c的tcmalloc,jemalloc
TCMalloc 是由 Google 开发的内存分配器,旨在提高多线程应用程序的性能。它使用了一些高级技术,如线程本地缓存、中心缓存和分级分配器,以减少锁竞争和内存碎片。TCMalloc 还提供了一些工具,如堆分析器和内存泄漏检测器,以帮助开发人员诊断和解决内存问题。
jemalloc 是由 Facebook 开发的内存分配器,旨在提高多线程应用程序的性能和可扩展性。它使用了一些高级技术,如分配器缓存、分配器锁和分配器事件,以减少锁竞争和内存碎片。jemalloc 还提供了一些工具,如堆分析器和内存泄漏检测器,以帮助开发人员诊断和解决内存问题。
需要注意的是,TCMalloc 和 jemalloc 都是第三方库,需要在应用程序中显式地链接和使用。在使用这些库时,需要仔细阅读它们的文档,并根据应用程序的需求进行配置和调整。
- 思考,为什么现在不再人工控制Allocator以及Malloc
[示例](▶ dangling pointer - memory management && garbage collection (figma.com))
4.go内存逃逸分析
推荐cms/compile/internal/gc/escape.go的逃逸分析源码以及
github.com/golang/go/tree/master/test中关于escape的测试用例
5.Go语言内存分配
- 连续堆->稀疏堆
[示例]分配稀疏堆模拟
- 分级内存
- Tiny : size < 16 bytes && has no pointer(noscan)
- Small :has pointer(scan) || (size >= 16 bytes && size <= 32 KB)
- Large : size > 32 KB
七 .垃圾收集
1.常用的几种垃圾回收算法
垃圾回收图示Visualizing Garbage Collection Algorithms (atomicobject.com)
- 最后清除算法
- 引用计数算法
- 标记清扫算法
- 标记压缩算法
- 复制算法清除
2.go语言垃圾回收使用的标记清扫算法mark_sweep
- runtime.gc
- runtime.mallocgc(内存增长率过高,会在内存分配的时候被动触发)
- forcegchelper(定时gc,大概预估时间是2min)
3.go语言垃圾回收的标记过程
-
思考:假如在标记的过程中,该标记的对象被应用程序修改应该怎么办 使用