golang新手注意事项

原文地址:Golang开发新手常犯的50个错误_gezhonglei2007的博客-CSDN博客

目录

初级

不允许左大括号单独一行

不允许出现未使用的变量

不允许出现未使用的import 若只调用包中的init方法 而不使用包的方法 则为包起别名 "_"

短的变量声明只能在函数内部使用

不能使用短变量声明重复声明

不能使用短变量声明这种方式来设置字段值

不能使用nil初始化一个未指定类型的变量

不能直接使用nil值的slice和map

map使用make分配内存时可指定capacity,但是不能对map使用cap函数

字符串不允许使用nil值,在go中nil只能赋值给指针\channel\func\interface\map或slice类型的变量

数组用于函数传参时是值复制 //方法或函数调用时,传入参数都是值复制,除非是map\slice\channel\指针类型这些特殊类型是引用传递

range关键字返回的是键值对,不是值

slice和array是一维的,但是可以使用原始的一维数组\一维切片来实现多维

从不存在key的map中取值时,返回的总是零值

string的索引返回的是byte或uint8,如想获取字符可使用for range,也可使用unicode/utf8包和golang.org/x/exp/utf8string包的At()方法

字符串并不总是UTF8的文本

len(str)返回的是字符串的字节数,获取字符串的rune数是使用unicode/utf8.RuneCountInString()函数,但注意一些字符也可能由多个rune组成,如é是两个rune组成 /rune类型是Go语言中的一个基本类型,其实就是一个int32的别名,主要用于表示一个字符类型大于一个字节小于等于4个字节的情况下,特别是中文字符

在slice\map\array的多行书写最后的逗号不可省略

内置数据结构的操作并不同步,但可把go提供了并发的特性使用起来:goroutines和channels

使用for range 迭代string 是以rune来迭代的 一个字符,也可以有多个rune组成。需要处理字符,尽量使用 golang.org/x/text/unicode/norm包。

使用for range 迭代map时每次迭代的顺序可能不一样,因为map的迭代是随机的

switch的case默认匹配规则不同于其它语言的是,匹配case条件后默认退出,除非使用fallthrough继续匹配,而其它语言是默认继续匹配,除非使用break退出匹配

只有后置自增(i++),后置自减 不存在前置自增(++i),前置自减

位运算的非操作是^ 跟异或位运算一样,有别于其它语言的~

位运算(与、或、异或、取反)优先级高于四则运算(加、减、乘、除、取余),有别于C语言

结构体在序列化时非导出字段(以小写字母开头的字段名)不会被encode,因此在decode时这些非导出字段的值为零值

程序不等所有goroutine结束就会退出,可以通过channel实现主协程(main goroutine)等待所有goroutine完成

对于无缓存区的channel,写入channel的goroutine会阻塞直到被读取,读取channel的goroutine会阻塞直到有数据写入

从一个closed状态的channel读数据是安全的,可通过返回状态判断是否关闭,而向一个closed状态的channel写数据会导致panic

向一个nil值的channel发送或读取数据,会导致永远阻塞

方法接收者是类型(T),接收者只是原对象的值复制,在方法中修改接收者不会修改原始对象的值;如果方法接收者是指针类型(*T),是对原对象的引用,方法中对其修改当然是原对象修改

log包中的log.fatal和log.panic不仅仅记录日志,还会终止程序 它不同于logging库

for range总是尝试将字符串解析成utf8的文本,对于它无法解析的字节,它会返回oxfffd的rime字符,因此,任何包含非utf8的文本,一定要先将其转换成字符切片([]byte) 

字符串与[]byte之间的转换是复制,可以用map[string][]byte建立字符串与[]byte之间映射,也可以range来避免内存分配来提高性能

意外的变量幽灵

中级

关闭HTTP的Response.Body

关闭http连接

全局关闭http连接重用

json反序列化数字到interface{}类型的值中,默认解析为float64类型,在使用时要注意

struct array slice map 比较

从panic中恢复

在slice array map 的for range子句中修改和引用数据项

slice中的隐藏数据

slice超范围数据覆盖

slice增加元素重新分配内存导致的怪事

类型重定义与方法继承

从 for switch 和for select 代码块中跳出

在for迭代过程中没迭代变量会一直保留,只是每次迭代值不一样,因此在for循环中在闭包里直接引用迭代变量,在执行时直接取迭代变量的值,而不是闭包所在迭代的变量值.如果闭包要取所在迭代变量的值,就需要for中定义一个变量来保存所在迭代的值,或者通过闭包函数传参

defer函数调用参数

defer语句调用实在当前函数结束之后调用,而不是变量的作用范围

失败的类型断言

阻塞的goroutine与资源泄漏

高级

nil值的interface{}不等于nil

变量内存的分配

GOMAXPROCS、Concurrency和Parallelism

读写操作排序

优先调度

更新map值的字段

用值实例上调用接受者为指针的方法


初级
不允许左大括号单独一行
package main

import "fmt"


//错误示范,此时会提示意想不到的 "{"
func main()
{
	fmt.Println(1)
}


//正确示范
func main(){

	fmt.Println(1)
}
不允许出现未使用的变量
package main

import "fmt"

func main() {
    //错误示范
	//此时会报错 未使用的变量n
	var n int
	fmt.Println(1)
}


func main(){
    //正确示范 此时正确打印出结果为: string
    var str = "string"
    fmt.Println(str)
}
不允许出现未使用的import 若只调用包中的init方法 而不使用包的方法 则为包起别名 "_"
//新建utils包
package utils

import "fmt"

// 该函数会在所有程序执行开始前被调用,每个包可以包含多个init函数,所有被编辑器识别到的init函数都会在main函数执行前被调用
func init() {
	fmt.Println("utils init")
}

//注意 被外部引用的方法首字母必须大写 go语言中函授的首字母大小写决定了它是否可以被外部引用
func Pr(){
    fmt.Println("utils pr")
}



//错误示范
package main

import (
	"fmt"
	"gettingstarted/utils"  //只引入这个包而不调用包中的方法,此时报错 未使用的导入
)

func main() {
	fmt.Println(123)
}


//正确示范

//第一种情况
package main
import (
	"fmt"
	"gettingstarted/utils"  
)

func main() {
	fmt.Println(123)
    //引入utils包,并且至少调用了一次utils包中的一个方法
    utils.Pr()
}

//此时输出 
utils init
123     
utils pr



//另外一种情况
package main

import (
	"fmt"
	_ "gettingstarted/utils"  //此时引入了utils包并给包声明 "_" 表示只运行这个包中的init方法,并且无法使用包中的其他方法,若使用其他方法 则需要去掉 "_"的声明
)

func main() {
	fmt.Println(123)
}

//此时输出 
utils init
123   

//关于init函数 会单独讲解 本例只用来展示import使用方法
短的变量声明只能在函数内部使用
package main

import "fmt"

//错误示范
str := "hello"
//正确示范
var str2 = "hello"

func main() {
    str3:= "hello"
    fmt.Println(str3)
	fmt.Println(str2)
}
不能使用短变量声明重复声明
package main

import "fmt"

func main() {
	str := "hello"
	// 错误示范 此时报错 No new variables on the left side of ':=' 
	str := "world"
	fmt.Println(str)
}
不能使用短变量声明这种方式来设置字段值
package main

import (
	"fmt"
)

type Result struct {
	Name string
}

func main() {

	//错误示范
	var res Result
	res.Name  := "张三"

	fmt.Printf("res: %+v\n", res)

}
不能使用nil初始化一个未指定类型的变量
package main

import (
	"fmt"
)

func main() {

	//错误示范,nil只能用来声明已知类型的零值 若未声明类型,编辑器就无法得知它的具体类型
	var res = nil
	fmt.Println(res)

}
不能直接使用nil值的slice和map
package main

func main() {

	var m map[string]int

	m["string"] = 1
	// panic: assignment to entry in nil map

	var s []int
	s[0] = 1
	// panic: assignment to entry in nil slice
}
map使用make分配内存时可指定capacity,但是不能对map使用cap函数
//在Go 语言中,map是一种引用类型,并且没有固定的容量概念,因此不能使用cap()函数获取其容量

package main

func main() {  
    m := make(map[string]int,1)
    //报错
    cap(m) 
}
字符串不允许使用nil值,在go中nil只能赋值给指针\channel\func\interface\map或slice类型的变量
package main

func main() {
	var s  string
    //错误
	s = nil
    //正确
	s = ""
}
数组用于函数传参时是值复制 //方法或函数调用时,传入参数都是值复制,除非是map\slice\channel\指针类型这些特殊类型是引用传递
package main

import (
	"fmt"
)

// 函数接受一个数组作为参数,尝试修改数组的值
func modifyArray(arr [3]int) {
	arr[0] = 100
}

// 函数接受一个切片作为参数,修改切片中的值,影响原始数组
func modifySlice(slice []int) {
	slice[0] = 200
}

// 函数接受一个指向数组的指针作为参数,修改数组的值,影响原始数组
func modifyArrayWithPointer(ptr *[3]int) {
	ptr[0] = 300
}

func main() {
	// 初始化一个数组
	array := [3]int{1, 2, 3}

	// 将数组作为参数传递给函数,修改不会影响原始数组
	modifyArray(array)
	fmt.Println("Array after modifyArray:", array)

	// 将数组的切片作为参数传递给函数,修改会影响原始数组
	slice := array[:]
	modifySlice(slice)
	fmt.Println("Array after modifySlice:", array)

	// 将数组的指针作为参数传递给函数,修改会影响原始数组
	modifyArrayWithPointer(&array)
	fmt.Println("Array after modifyArrayWithPointer:", array)
}
range关键字返回的是键值对,不是值
package main

import "fmt"

func main() {
    numbers := []int{2, 4, 6, 8, 10}

    // 使用 range 遍历切片
    for index, value := range numbers {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }
}

//打印
Index: 0, Value: 2
Index: 1, Value: 4
Index: 2, Value: 6
Index: 3, Value: 8
Index: 4, Value: 10
slice和array是一维的,但是可以使用原始的一维数组\一维切片来实现多维
package main

import "fmt"

func main() {
    // 创建一个切片的切片
    var matrix [][]int

    // 初始化一个3x3的矩阵
    for i := 0; i < 3; i++ {
        row := make([]int, 3)
        for j := 0; j < 3; j++ {
            row[j] = i*3 + j + 1
        }
        matrix = append(matrix, row)
    }

    // 打印矩阵
    for i := 0; i < len(matrix); i++ {
        fmt.Println(matrix[i])
    }
}

//打印结果
[1 2 3]
[4 5 6]
[7 8 9]
从不存在key的map中取值时,返回的总是零值
package main

import "fmt"

func main() {
    // 创建一个空的 map
    myMap := make(map[string]int)

    // 尝试从不存在的键中获取值
    value := myMap["nonexistent"]

    fmt.Println("Value:", value) // 打印零值,这里是 0  请注意  零值并非指的是0 每个类型都有与之对应的零值,此处value的类型是int,int的零值为0
}
string的索引返回的是byte或uint8,如想获取字符可使用for range,也可使用unicode/utf8包和golang.org/x/exp/utf8string包的At()方法
package main

import (
	"fmt"
	"golang.org/x/exp/utf8string"
	"unicode/utf8"
)

func main() {
	str := "你好,世界!"
	for _, char := range str {
		fmt.Printf("%c\n", char)
	}

}
func main() {

	str := "Hello, 世界!"
	index := 7
	char, _ := utf8.DecodeRuneInString(str[index:])
	fmt.Printf("Character at index %d: %c\n", index, char)

}

func main() {
	str := "Hello, 世界!"
	index := 7
	s := utf8string.NewString(str)
	b := s.At(index)
	fmt.Println("Character at index :", string(b))
}
字符串并不总是UTF8的文本
package main

import "fmt"

func main() {
    // 使用字节序列创建一个非 UTF-8 字符串
    // 这个字节序列可能代表其他字符集的文本
    nonUTF8Str := []byte{0xE6, 0x97, 0xA0, 0xE5, 0xA5, 0xBD} // "你好" 的 GBK 编码

    // 将字节序列转换为字符串
    str := string(nonUTF8Str)

    // 输出字符串
    fmt.Println("Non UTF-8 String:", str)
}
len(str)返回的是字符串的字节数,获取字符串的rune数是使用unicode/utf8.RuneCountInString()函数,但注意一些字符也可能由多个rune组成,如é是两个rune组成 /rune类型是Go语言中的一个基本类型,其实就是一个int32的别名,主要用于表示一个字符类型大于一个字节小于等于4个字节的情况下,特别是中文字符
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	str := "Hello, 世界! é" // 包含英文、中文和一个复杂字符 é

	// 获取字符串的字节数
	byteCount := len(str)
	fmt.Println("Byte Count:", byteCount) // 输出 19

	// 获取字符串的 rune 数
	runeCount := utf8.RuneCountInString(str)
	fmt.Println("Rune Count:", runeCount) // 输出 10

	// 遍历字符串获取每个字符的 rune 和字节数
	fmt.Println("Characters and their bytes:")
	for i, runeValue := range str {
		fmt.Printf("Character %d: %c (Bytes: %d)\n", i, runeValue, utf8.RuneLen(runeValue))
	}

	// 使用 RuneCountInString 获取字符数,而不仅仅是字节数
	// 一些字符可能会由多个 rune 组成,如 é
	// 注意,这个库函数并不完全适用于所有语言和字符
	// 特殊情况可能需要额外的处理
	complexStr := "é" // 包含一个复杂字符 é
	complexRuneCount := utf8.RuneCountInString(complexStr)
	fmt.Println("Complex Rune Count:", complexRuneCount) // 输出 1
}
在slice\map\array的多行书写最后的逗号不可省略
package main

import "fmt"

func main() {
	// slice 的多行书写
	slice := []int{
		1,
		2,
		3, // 最后一个元素后面必须有逗号
	}

	// map 的多行书写
	myMap := map[string]int{
		"one":   1,
		"two":   2,
		"three": 3, // 最后一个键值对后面必须有逗号
	}

	// array 的多行书写
	array := [3]int{
		1,
		2,
		3, // 最后一个元素后面必须有逗号
	}

	fmt.Println("Slice:", slice)
	fmt.Println("Map:", myMap)
	fmt.Println("Array:", array)
}
内置数据结构的操作并不同步,但可把go提供了并发的特性使用起来:goroutines和channels
package main

import (
	"fmt"
	"time"
)

func main() {
	data := []int{1, 2, 3, 4, 5}

	// 创建一个通信用的 channel
	resultChan := make(chan int)

	// 启动多个 goroutine 并发处理数据
	for _, num := range data {
		go process(num, resultChan)
	}

	// 从 channel 中获取结果
	for i := 0; i < len(data); i++ {
		result := <-resultChan
		fmt.Println("Processed:", result)
	}
}

func process(num int, resultChan chan int) {
	// 模拟处理耗时
	time.Sleep(time.Second)

	// 将处理结果发送到 channel
	resultChan <- num * 2
}

//Go 语言的内置数据结构并不是线程安全的,这意味着在多个 goroutine 中并发访问这些数据结构可能会导致竞态条件和不确定的行为。然而,Go 语言提供了并发的特性,如 goroutines 和 channels,来帮助你在多个 goroutine 之间协调和同步数据的访问,从而避免竞态条件和提供更好的并发性能
使用for range 迭代string 是以rune来迭代的 一个字符,也可以有多个rune组成。需要处理字符,尽量使用 golang.org/x/text/unicode/norm包。
package main

import (
	"fmt"
	"golang.org/x/text/unicode/norm"
)

func main() {
	str := "Café" // "Café",其中的 é 是一个由多个 rune 组成的字符

	// 使用 NFC 规范化字符串
	nfc := norm.NFC.String(str)
	fmt.Println("NFC:", nfc) // 输出 "Café"

	// 使用 NFD 规范化字符串
	nfd := norm.NFD.String(str)
	fmt.Println("NFD:", nfd) // 输出 "Café"

	// 使用 NFKC 规范化字符串
	nfkc := norm.NFKC.String(str)
	fmt.Println("NFKC:", nfkc) // 输出 "Café"

	// 使用 NFKD 规范化字符串
	nfkd := norm.NFKD.String(str)
	fmt.Println("NFKD:", nfkd) // 输出 "Café"
}
使用for range 迭代map时每次迭代的顺序可能不一样,因为map的迭代是随机的
package main

import (
	"fmt"
)

func main() {
	myMap := map[string]int{
		"one":   1,
		"two":   2,
		"three": 3,
		"four":  4,
		"five":  5,
		"six":   6,
		"seven": 7,
		"eight": 8,
	}

	fmt.Println("Map Iteration:")
	for key, value := range myMap {
		fmt.Printf("Key: %s, Value: %d\n", key, value)
	}
}

//随机打印
Map Iteration:
Key: four, Value: 4 
Key: five, Value: 5 
Key: six, Value: 6  
Key: seven, Value: 7
Key: eight, Value: 8
Key: one, Value: 1  
Key: two, Value: 2  
Key: three, Value: 3
switch的case默认匹配规则不同于其它语言的是,匹配case条件后默认退出,除非使用fallthrough继续匹配,而其它语言是默认继续匹配,除非使用break退出匹配
package main

import (
	"fmt"
)

func main() {
	num := 2

	fmt.Println("Switch without fallthrough:")
	switch num {
	case 1:
		fmt.Println("Case 1")
	case 2:
		fmt.Println("Case 2")
	case 3:
		fmt.Println("Case 3")
	default:
		fmt.Println("Default")
	}

	fmt.Println("Switch with fallthrough:")
	switch num {
	case 1:
		fmt.Println("Case 1")
	case 2:
		fmt.Println("Case 2")
		fallthrough
	case 3:
		fmt.Println("Case 3")
		fallthrough
	default:
		fmt.Println("Default")
	}
}

/*打印结果
Switch without fallthrough:
Case 2                  
Switch with fallthrough:
Case 2                  
Case 3                  
Default   */
只有后置自增(i++),后置自减 不存在前置自增(++i),前置自减
package main

import "fmt"

func main() {
    var i int = 5
    var j int = 10

    // 后置自增
    i++
    fmt.Println("i after increment:", i) // 输出 6

    // 后置自减
    j--
    fmt.Println("j after decrement:", j) // 输出 9
    // 以下操作是非法的
    // ++i
    // --i
    fmt.Println("i:", i)
}
位运算的非操作是^ 跟异或位运算一样,有别于其它语言的
package main

import "fmt"

func main() {
    // 按位取反
    a := 5
    b := ^a
    fmt.Printf("Bitwise NOT of %d is %d\n", a, b) // 输出 "Bitwise NOT of 5 is -6"

    // 异或操作
    x := 12
    y := 7
    z := x ^ y
    fmt.Printf("XOR of %d and %d is %d\n", x, y, z) // 输出 "XOR of 12 and 7 is 11"
}
位运算(与、或、异或、取反)优先级高于四则运算(加、减、乘、除、取余),有别于C语言
package main

import "fmt"

func main() {
    a := 5
    b := 3

    result := a + b&2
    fmt.Println("Result:", result) // 输出 7,因为先进行了位运算 b&2,然后进行了加法运算

    anotherResult := a | b + 2
    fmt.Println("Another Result:", anotherResult) // 输出 7,因为位运算的优先级高于加法运算
}
结构体在序列化时非导出字段(以小写字母开头的字段名)不会被encode,因此在decode时这些非导出字段的值为零值
package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name      string
	Age       int
	isPrivate bool // 非导出字段
}

func main() {
	p := Person{
		Name:      "Alice",
		Age:       30,
		isPrivate: true,
	}

	// 序列化(编码)结构体为 JSON
	data, err := json.Marshal(p)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("Serialized Data:", string(data))

	// 反序列化(解码)JSON为结构体
	var decoded Person
	err = json.Unmarshal(data, &decoded)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("Decoded Struct:", decoded)
}
//在 Go 语言中,导出的字段可以被外部包访问,而非导出字段只能在同一个包内访问。为了确保序列化和反序列化的一致性,非导出字段会被忽略,不参与序列化
程序不等所有goroutine结束就会退出,可以通过channel实现主协程(main goroutine)等待所有goroutine完成
package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()

	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d completed\n", id)
}

func main() {
	const numWorkers = 3

	var wg sync.WaitGroup

	for i := 1; i <= numWorkers; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}

	// 等待所有子协程完成
	wg.Wait()

	fmt.Println("All workers completed. Main goroutine exiting.")
}
对于无缓存区的channel,写入channel的goroutine会阻塞直到被读取,读取channel的goroutine会阻塞直到有数据写入
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)

	go func() {
		fmt.Println("Trying to send data...")
		ch <- 42 // 此处会阻塞,直到有 goroutine 准备好读取数据
		fmt.Println("Data sent")
	}()

	time.Sleep(time.Second)

	data := <-ch // 此处会阻塞,直到有 goroutine 准备好写入数据
	fmt.Println("Received:", data)
}

//写入阻塞: 当一个 goroutine 试图向无缓冲的 channel 写入数据时,如果没有其他的 goroutine 准备好从 channel 中读取数据,写入的 goroutine 会被阻塞,直到另一个 goroutine 准备好读取数据为止。

//读取阻塞: 当一个 goroutine 试图从无缓冲的 channel 读取数据时,如果没有其他的 goroutine 准备好向 channel 中写入数据,读取的 goroutine 会被阻塞,直到另一个 goroutine 准备好写入数据为止。
从一个closed状态的channel读数据是安全的,可通过返回状态判断是否关闭,而向一个closed状态的channel写数据会导致panic
package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)

	close(ch)

	// 从已关闭的 channel 读取数据是安全的
	data, ok := <-ch
	fmt.Println("Read Data:", data, "Channel Open:", ok) // 输出 "Read Data: 0 Channel Open: false"

	// 向已关闭的 channel 写入数据会导致 panic
	// ch <- 42 // 解除注释会导致 panic
}

//这种设计是为了保证安全性和可预测性。
向一个nil值的channel发送或读取数据,会导致永远阻塞
package main

import "fmt"

func main() {
	var ch chan int // 声明一个 nil 值的 channel

	// 发送操作会永久阻塞
	// ch <- 42

	// 读取操作会永久阻塞
	// <-ch

	fmt.Println("Reached end of program") // 由于上述操作永久阻塞,此行不会被执行
}
//当你尝试在一个 nil 值的 channel 上进行发送或接收操作时,程序会永远阻塞在那里,因为没有底层的 channel 结构来处理数据的传递。
方法接收者是类型(T),接收者只是原对象的值复制,在方法中修改接收者不会修改原始对象的值;如果方法接收者是指针类型(*T),是对原对象的引用,方法中对其修改当然是原对象修改
package main

import (
	"fmt"
)

type Person struct {
	Name string
}

func (p Person) ValueMethod() {
	p.Name = "Value Method"
}

func (p *Person) PointerMethod() {
	p.Name = "Pointer Method"
}

func main() {
	p1 := Person{Name: "Alice"}
	p1.ValueMethod()
	fmt.Println("Value Method:", p1.Name) // 输出 "Value Method"

	p2 := Person{Name: "Bob"}
	(&p2).PointerMethod() // 实际上可以省略 & 符号,Go 会自动转换
	fmt.Println("Pointer Method:", p2.Name) // 输出 "Pointer Method"
}

//值接收者(T): 当方法的接收者是类型(T)时,方法中对接收者的修改不会影响原始对象。因为方法接收的是原对象的副本,对副本的修改不会影响原对象。

//*指针接收者(T): 当方法的接收者是指针类型(*T)时,方法中对接收者的修改会影响原始对象。因为方法接收的是原对象的引用,对引用指向的对象的修改会直接影响原对象。
log包中的log.fatal和log.panic不仅仅记录日志,还会终止程序 它不同于logging库
package main

import (
	"log"
)

func main() {
	// 使用 log.Fatal
	log.Println("Starting program")
	log.Fatal("Fatal error occurred")
	log.Println("This line won't be reached")

	// 使用 log.Panic
	log.Println("Starting panic example")
	log.Panic("Panic error occurred")
	log.Println("This line won't be reached")
}

//log.Fatal: 这个函数会记录错误日志,并在记录日志后调用 os.Exit(1),终止程序的执行。它适用于在发生严重错误时立即终止程序,因为它不会触发 panic。

//log.Panic: 这个函数会记录错误日志,并在记录日志后调用内建的 panic 函数,导致程序中断。和普通的 panic 不同,log.Panic 还会记录日志。这个函数通常用于在一些无法修复的错误情况下抛出 panic。
for range总是尝试将字符串解析成utf8的文本,对于它无法解析的字节,它会返回oxfffd的rime字符,因此,任何包含非utf8的文本,一定要先将其转换成字符切片([]byte) 
package main

import (
	"fmt"
)

func main() {
	text := "Hello, 世界! \xfe" // 包含一个非 UTF-8 字节

	// 直接使用 for range
	fmt.Println("Using for range:")
	for _, r := range text {
		fmt.Printf("%c ", r)
	}
	fmt.Println()

	// 将文本转换成字节切片再使用 for range
	fmt.Println("Using bytes and for range:")
	bytes := []byte(text)
	for _, b := range bytes {
		fmt.Printf("%x ", b)
	}
	fmt.Println()
}

//在 Go 语言中,for range 循环会尝试将字符串解析成 UTF-8 编码的文本。如果遇到无法解析的字节,它会将这些字节替换为 Unicode 替代字符 U+FFFD,即 �,来保证在循环中不会因为非 UTF-8 字节而中断。

//这个特性意味着如果你需要处理可能包含非 UTF-8 字符的文本,应该首先将文本转换成字节切片([]byte),然后再进行处理。
字符串与[]byte之间的转换是复制,可以用map[string][]byte建立字符串与[]byte之间映射,也可以range来避免内存分配来提高性能
package main

import (
	"fmt"
)

func main() {
	stringToBytesMap := make(map[string][]byte)

	str := "hello"
	bytes := []byte(str)
	stringToBytesMap[str] = bytes

	fmt.Println("Bytes from map:", stringToBytesMap[str])

	// 使用 for range 遍历字符串并转换成字节切片,避免内存分配
	for _, b := range str {
		fmt.Printf("%c", b)
	}
	fmt.Println()
}

//使用 map[string][]byte 建立映射: 如果你需要频繁地在字符串和字节切片之间进行转换,可以使用一个映射来缓存这些转换结果,从而避免重复的内存分配。这在需要频繁转换的场景中可能会提高性能。

//使用 for range 来避免内存分配: 使用 for range 遍历字符串时,Go 会将字符串转换为 []byte 的切片,从而避免了内存分配。这在性能敏感的场景中可能会有所帮助,特别是当你需要在不修改字符串的情况下进行操作时。
意外的变量幽灵
//短变量声明语法,很好用,但是代码块中使用短变量声明与外部相同的变量时,没有语法编译错误,但是代码块中同名短变量声明从声明开始到代码块结束,对变量的修改将不会影响到外部变量
package main

import "fmt"

func main() {  
    x := 1
    fmt.Println(x)     //prints 1
    {
        fmt.Println(x) //prints 1
        // x = 3
        x := 2         // 不会影响到外部x变量的值
        fmt.Println(x) //prints 2
        //x = 5        // 不会影响到外部x变量值
    }
    fmt.Println(x)     //prints 3
}
//这种现象称之为幽灵变量,可以使用 go tool vet -shadow filename.go 检查幽灵变量
//使用 go-ynet 命令会执行更多幽灵变量的检测
中级
关闭HTTP的Response.Body
package main

import (
	"fmt"
	"net/http"
)

func main() {
	resp, err := http.Get("https://example.com")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer resp.Body.Close() // 使用 defer 确保在函数返回时关闭 Body

	// 在这里使用 resp.Body 进行操作
}

//在 Go 语言中,对于 HTTP 响应的 Response.Body 字段,你需要确保在使用完毕后关闭它,以便释放相关资源和避免内存泄漏。

//关闭 Response.Body 的一种推荐的方式是在使用完毕后调用 defer 关键字,确保在函数返回时关闭它。这可以避免在使用过程中发生错误而导致没有关闭 Body

//在这个示例中,defer resp.Body.Close() 语句会确保在函数返回时关闭 HTTP 响应的 Body。这种方式可以帮助你避免在处理过程中忘记关闭 Body。

//总之,确保在使用完毕后关闭 HTTP 响应的 Body 是一种良好的实践,有助于避免资源泄漏问题。
关闭http连接
package main

import (
	"fmt"
	"net/http"
)

func main() {
	client := &http.Client{} // 创建一个自定义的 HTTP 客户端

	req, err := http.NewRequest("GET", "https://example.com", nil)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer resp.Body.Close() // 关闭响应体,释放连接

	// 在这里使用 resp.Body 进行操作
}

//在 Go 语言的 net/http 包中,HTTP 连接是由客户端维护的,并且在默认情况下不需要显式地关闭连接。当你使用 http.Get、http.Post 等函数进行 HTTP 请求时,底层的连接会被自动管理,请求完成后会被释放。

//然而,如果你正在开发一个更复杂的应用程序,可能会需要手动管理连接的生命周期,特别是在使用 Keep-Alive 持久连接或长轮询等情况下。

//在这些情况下,你可以通过调用 http.Response 的 Close 方法来显式地关闭连接。但是需要注意,一般情况下你不需要这样做。

//在这个示例中,我们创建了一个自定义的 HTTP 客户端 client,并通过手动创建请求 http.NewRequest 来发送请求。然后,在处理完响应后使用 defer resp.Body.Close() 来关闭连接。

//需要注意,如果你在请求中使用了 Keep-Alive,连接可能会在一段时间后被重新使用,而不会立即关闭。

//总的来说,Go 的 net/http 包在大多数情况下会自动管理连接的生命周期,你不必手动关闭连接,除非你遇到特殊需求。
全局关闭http连接重用
//在 Go 的 net/http 包中,默认情况下会启用 HTTP 连接的重用,也就是使用 Keep-Alive 功能来在多次请求之间重用已建立的连接。这有助于提高性能,减少连接的建立和关闭开销。

//然而,在某些情况下,你可能需要控制全局的 HTTP 连接的重用行为,例如关闭全局的连接重用。你可以通过自定义 http.Transport 并设置 DisableKeepAlives 字段来实现这一点。

package main

import (
	"fmt"
	"net/http"
)

func main() {
	// 自定义 Transport,并设置 DisableKeepAlives 为 true
	transport := &http.Transport{
		DisableKeepAlives: true,
	}

	client := &http.Client{
		Transport: transport,
	}

	req, err := http.NewRequest("GET", "https://example.com", nil)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer resp.Body.Close()

	// 在这里使用 resp.Body 进行操作
}
//在这个示例中,我们创建了一个自定义的 http.Transport,并将其中的 DisableKeepAlives 字段设置为 true,从而全局关闭了连接的重用。然后,我们在自定义的 http.Client 中使用这个 Transport。

//需要注意的是,关闭全局的连接重用可能会影响性能,因为每次请求都需要重新建立连接。这种做法一般在某些特殊需求下使用,如果你只是希望关闭单个请求的连接重用,那么可以在单个请求中设置 req.Close = true。

//总的来说,Go 的 net/http 包提供了灵活的方式来控制连接重用的行为,你可以根据实际需求进行设置。
json反序列化数字到interface{}类型的值中,默认解析为float64类型,在使用时要注意
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	jsonData := []byte(`{"number": 42}`)

	var data map[string]interface{}
	err := json.Unmarshal(jsonData, &data)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// 使用断言确保正确的类型
	number, ok := data["number"].(float64)
	if !ok {
		fmt.Println("Error: Unexpected type")
		return
	}

	fmt.Println("Number:", number)
}
struct array slice map 比较
//Struct(结构体):
//结构体是一种自定义的复合数据类型,可以包含不同类型的字段。
//适用于描述具有不同属性的对象。
//字段在内存中是连续存储的,适用于存储相对稳定的数据结构。
//值类型,赋值或传递时会复制整个结构体。


//Array(数组):
//数组是一个固定大小的数据结构,所有元素类型必须相同。
//适用于已知大小且固定的数据集合。
//在内存中是连续存储的,访问速度较快。
//值类型,赋值或传递时会复制整个数组。


//Slice(切片):
//切片是一个动态大小的、可以包含任意类型元素的数据结构。
//适用于动态增长的数据集合。
//切片底层引用一个数组,自动进行动态扩容。
//引用类型,赋值或传递时复制指向底层数组的引用。

//Map(映射):
//映射是一种存储键值对的数据结构。
//适用于需要快速查找、关联关系的数据。
//底层是哈希表,提供快速的键值查找。
//引用类型,赋值或传递时复制指向底层数据的引用。


//如果struct结构体的所有字段都能够使用==操作比较,name结构体变量也能够使用==比较;但是如果struct字段不能使用==比较,结构体变量使用==比较会导致编译错误
//array同上
//go提供了一些用于比较不能直接用==比较的函数,常用的是reflect.DeepEqual()函数
//DeepEqual()函数对于nil值的slice与空元素的slice是不相等的,这点不同于bytes.Equal()函数
//DeepEqual()函数并不总是能够正确处理slice
//如果要忽略大小写来比较包含文字数据的字节切片(byte slice)
//不建议使用bytes包和strings包里的ToUpper()\ToLower()这些函数转换后再用 ==\byte.Equal()\bytes.Compare()等比较 ToUpper ToLower 只能处理英文文字,对其它语言无效,建议使用strings.EqualFold()和bytes.EqualFold()
// 如果要比较用于验证用户数据密钥信息的字节切片时,使用reflact.DeepEqual() bytes.Equal() bytes.Compare()会使应用程序遭受计时攻击,可以使用crypto/subtle.ConstantTimeCompare()避免泄漏时间信息
//Struct(结构体):
//结构体的值相等意味着所有字段的值都相等。
//可以使用 == 运算符进行结构体值的比较。
//Array(数组):
//两个数组的值相等意味着对应索引位置上的元素值都相等。
//可以使用 == 运算符进行数组值的比较。
//Slice(切片):
//切片是引用类型,不能直接使用 == 运算符进行比较。如果你想比较切片的值是否相等,需要遍历切片并逐个比较元素。
//Map(映射):
//由于映射是引用类型,不能直接使用 == 运算符进行比较。同样,需要遍历映射并逐个比较键值对。

package main

import (
	"fmt"
)

func main() {
	// Struct
	type Person struct {
		Name string
		Age  int
	}
	person1 := Person{Name: "Alice", Age: 30}
	person2 := Person{Name: "Alice", Age: 30}
	fmt.Println("Struct comparison:", person1 == person2)

	// Array
	numbers1 := [3]int{1, 2, 3}
	numbers2 := [3]int{1, 2, 3}
	fmt.Println("Array comparison:", numbers1 == numbers2)

	// Slice
	colors1 := []string{"red", "green", "blue"}
	colors2 := []string{"red", "green", "blue"}
	fmt.Println("Slice comparison:", areSlicesEqual(colors1, colors2))

	// Map
	scores1 := map[string]int{"math": 90, "english": 85}
	scores2 := map[string]int{"math": 90, "english": 85}
	fmt.Println("Map comparison:", areMapsEqual(scores1, scores2))
}

func areSlicesEqual(slice1, slice2 []string) bool {
	if len(slice1) != len(slice2) {
		return false
	}
	for i := range slice1 {
		if slice1[i] != slice2[i] {
			return false
		}
	}
	return true
}

func areMapsEqual(map1, map2 map[string]int) bool {
	if len(map1) != len(map2) {
		return false
	}
	for key, value := range map1 {
		if map2Value, ok := map2[key]; !ok || map2Value != value {
			return false
		}
	}
	return true
}
//需要注意的是,切片和映射的比较涉及到元素的顺序,因此需要进行逐个比较。同样,如果你的结构体中包含引用类型(如切片、映射等),比较的结果可能会因为引用的不同而不相等。
从panic中恢复
package main

import (
	"fmt"
)

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered:", r)
		}
	}()

	// 触发 panic
	panic("Something went wrong!")
	fmt.Println("This will not be executed")
}
``}

//在这个示例中,我们使用了 defer 和 recover 来捕获发生的 panic。当 panic 发生时,recover 将停止 panic 流程,并将触发 panic 的值返回。这允许我们在恢复函数内部执行一些特定的操作。

//需要注意的是,recover 只能在相同的 goroutine 中捕获 panic。如果在不同的 goroutine 中发生了 panic,你将无法通过 recover 来捕获它。
在slice array map 的for range子句中修改和引用数据项
package main

import (
	"fmt"
)

func main() {
	// Slice
	slice := []int{1, 2, 3}
	for i, val := range slice {
		slice[i] = val * 2 // 修改的是副本,不会影响原始数据
	}
	fmt.Println("Modified slice:", slice) // 输出 [2 4 6]

	// Array
	array := [3]int{1, 2, 3}
	for i, val := range array {
		array[i] = val * 2 // 修改的是副本,不会影响原始数据
	}
	fmt.Println("Modified array:", array) // 输出 [1 2 3]

	// Map
	m := map[string]int{"a": 1, "b": 2, "c": 3}
	for key, val := range m {
		m[key] = val * 2 // 修改的是副本,不会影响原始数据
	}
	fmt.Println("Modified map:", m) // 输出 map[a:1 b:2 c:3]
}

//Slice 和 Array:
//在 for range 遍历 slice 和 array 时,会迭代每个元素的副本,而不是原始元素本身。
//如果你尝试修改 slice 或 array 中的元素,修改只会作用于副本,不会影响原始数据。
//Map:
//在 for range 遍历 map 时,迭代的是键值对的副本,而不是原始键值对本身。
//如果你尝试修改 map 中的值,修改只会作用于副本,不会影响原始数据。
//如果你需要修改 slice、array 或 map 中的元素,可以通过索引或键来直接访问原始数据,并进行修改。
//在上述示例中,尝试修改 slice、array 和 map 中的值都只会影响到副本,不会对原始数据产生影响。如果你需要修改原始数据,可以通过索引或键直接访问。
slice中的隐藏数据
package main

import (
	"fmt"
)

func main() {
	source := []int{1, 2, 3, 4, 5}
	slice := source[1:4]

	fmt.Printf("Slice: %v\n", slice)
	fmt.Printf("Length: %d, Capacity: %d\n", len(slice), cap(slice))
}
//在 Go 语言中,切片(slice)实际上是一个引用类型,它引用一个底层数组,并且具有长度(length)和容量(capacity)属性。这种引用关系意味着在切片中有一个“隐藏”的数据结构,其中包含了指向底层数组的指针、长度和容量等信息。
//切片的底层数组通常会大于或等于切片的长度。当你创建一个切片时,底层数组的一部分会被引用到,这部分决定了切片的长度,而底层数组的全部决定了切片的容量。
//这个“隐藏”的数据结构使得切片可以动态增长,同时也是 Go 中切片高效的实现方式之一,因为它不需要每次都创建新的数组。
//在这个示例中,我们从一个源切片 source 中创建了一个新的切片 slice。通过 len(slice) 和 cap(slice),我们可以看到 slice 的长度为 3,容量为 4,这意味着 slice 引用了 source 切片的索引 1 到 3 的部分。这个底层的引用关系是 Go 切片的重要特性,它使得切片在不同场景下的使用非常灵活和高效。
slice超范围数据覆盖
package main

import (
	"fmt"
)

func main() {
	slice := []int{1, 2, 3, 4, 5}

	// 尝试访问超出切片范围的索引
	index := 10
	value := slice[index] // 这里会触发运行时错误

	fmt.Printf("Value at index %d: %d\n", index, value)
}

/*在 Go 语言中,当你使用切片进行数据访问或操作时,如果索引超出切片的有效范围,将会导致运行时错误。这种情况下会触发数组越界(slice bounds out of range)错误。

这是 Go 语言的一个安全机制,旨在防止未定义的行为和数据访问错误。数组越界可能会导致程序崩溃或产生不可预测的结果,因此在编写代码时应该避免发生这种情况。
在这个示例中,我们尝试访问索引 10 处的值,而切片 slice 的长度为 5,所以这会触发运行时错误。为了避免这种错误,你应该始终确保访问切片的索引在有效范围内。可以通过检查索引的范围或使用 len(slice) 来进行判断。*/
slice增加元素重新分配内存导致的怪事
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	slice := make([]int, 0, 5)

	wg.Add(2)

	go func() {
		defer wg.Done()

		for i := 0; i < 10; i++ {
			slice = append(slice, i)
			fmt.Println("Appended in Goroutine 1:", i)
		}
	}()

	go func() {
		defer wg.Done()

		for i := 10; i < 20; i++ {
			slice = append(slice, i)
			fmt.Println("Appended in Goroutine 2:", i)
		}
	}()

	wg.Wait()

	fmt.Println("Final Slice:", slice)
}


/*
在 Go 语言中,切片的长度和容量是动态管理的。当切片的长度超过其容量时,如果需要继续追加元素,Go 会重新分配内存,并创建一个新的底层数组,将原始数据复制到新的数组中。这可能会导致一些奇怪的现象,特别是在并发环境中
在这个示例中,我们在两个并发的 goroutine 中向切片 slice 追加元素。因为切片的容量只有 5,而追加的元素数量超过了容量,这会导致切片进行重新分配内存。由于并发的性质,可能会出现竞态条件,因此最终结果可能与你预期的不同。

这种情况下,你可能会看到一些奇怪的输出,例如一些元素被重复添加或缺失。这是因为在切片重新分配内存期间,多个 goroutine 可能会同时修改切片的状态,从而导致不一致。

要解决这个问题,可以使用互斥锁等并发控制机制来确保在切片修改期间只有一个 goroutine 在操作。这将确保数据的一致性和正确性。
*/
类型重定义与方法继承
package main

import (
	"fmt"
)

// 原始类型
type Point struct {
	X, Y int
}

// 基于 Point 进行类型重定义,并为新类型添加方法
type EnhancedPoint Point

func (ep EnhancedPoint) Distance() float64 {
	return float64(ep.X*ep.X + ep.Y*ep.Y)
}

// 创建一个新类型,嵌入原始类型,实现方法继承
type ColoredPoint struct {
	Point
	Color string
}

func main() {
	// 使用类型重定义的方法
	enhanced := EnhancedPoint{X: 3, Y: 4}
	fmt.Println("Distance:", enhanced.Distance())

	// 使用方法继承
	colored := ColoredPoint{Point: Point{X: 1, Y: 2}, Color: "red"}
	fmt.Println("Point:", colored.Point)
	fmt.Println("Color:", colored.Color)
}


/*
类型重定义:

类型重定义是指基于一个已有的类型创建一个新的类型,但是它们在类型系统中是不同的类型。
通过类型重定义,你可以为已有的类型创建更具有表达性的名称,或者为它添加一些新的方法。
重定义类型本质上是基于已有类型进行封装,但在使用上仍然需要进行类型转换。
方法继承:

在 Go 语言中,并没有直接的类继承的概念,但你可以通过组合和嵌套类型来实现类似的效果。
你可以在一个新类型中嵌入已有类型,然后在新类型上定义方法,实现方法的继承。
嵌入类型的方法可以在新类型上直接调用,就好像这些方法是新类型自己的一样。
在这个示例中,我们首先对 Point 类型进行了类型重定义,创建了一个新的类型 EnhancedPoint,并为其添加了一个 Distance 方法。然后,我们创建了一个新类型 ColoredPoint,嵌入了 Point 类型,并在其上定义了方法继承。通过嵌入 Point,ColoredPoint 可以直接使用 Point 类型的方法和属性。

需要注意,尽管类型重定义和方法继承可以达到类似于继承的效果,但在 Go 语言中它们是不同的概念,因为 Go 并没有经典的类继承模型。*/
从 for switch 和for select 代码块中跳出
//使用break
package main

import (
	"fmt"
)

func main() {
	// 从 for 循环中跳出
	for i := 0; i < 5; i++ {
		if i == 3 {
			break
		}
		fmt.Println(i)
	}

	// 从 switch 语句中跳出
	switch x := 2; x {
	case 1:
		fmt.Println("One")
	case 2:
		fmt.Println("Two")
		break
	case 3:
		fmt.Println("Three")
	}

	// 从 select 语句中跳出
	ch := make(chan int)
	go func() {
		ch <- 1
	}()
	select {
	case val := <-ch:
		fmt.Println("Received:", val)
		break
	}

	fmt.Println("Done")
}
/*
在 Go 语言中,从 for、switch 和 select 代码块中跳出可以使用 break 和 goto 语句。然而,使用 goto 是不被鼓励的,因为它可能导致代码不易读和维护。在许多情况下,使用函数或标志位来控制循环的执行会更加清晰和可维护。*/

//使用goto
package main

import (
	"fmt"
)

func main() {
	// 从 for 循环中跳出
	for i := 0; i < 5; i++ {
		if i == 3 {
			goto EndLoop
		}
		fmt.Println(i)
	}
EndLoop:

	// 从 switch 语句中跳出
	switch x := 2; x {
	case 1:
		fmt.Println("One")
	case 2:
		fmt.Println("Two")
		goto EndSwitch
	case 3:
		fmt.Println("Three")
	}
EndSwitch:

	// 从 select 语句中跳出
	ch := make(chan int)
	go func() {
		ch <- 1
	}()
	select {
	case val := <-ch:
		fmt.Println("Received:", val)
		goto EndSelect
	}
EndSelect:

	fmt.Println("Done")
}
在for迭代过程中没迭代变量会一直保留,只是每次迭代值不一样,因此在for循环中在闭包里直接引用迭代变量,在执行时直接取迭代变量的值,而不是闭包所在迭代的变量值.如果闭包要取所在迭代变量的值,就需要for中定义一个变量来保存所在迭代的值,或者通过闭包函数传参
package main

import (
	"fmt"
)

func main() {
	slice := []int{1, 2, 3, 4, 5}

	// 使用新变量解决闭包问题
	for _, val := range slice {
		v := val // 创建一个新变量,将循环变量的值复制给它
		func() {
			fmt.Println("Using new variable:", v)
		}()
	}

	// 使用闭包参数解决闭包问题
	for _, val := range slice {
		func(v int) {
			fmt.Println("Using closure parameter:", v)
		}(val) // 将循环变量作为参数传递给闭包函数
	}
}

/*
在一个 for 循环中使用闭包时,如果闭包中引用了循环变量,它实际上会捕获循环变量的地址,而不是其值。由于循环迭代非常快,因此当闭包在以后的某个时间点执行时,循环变量可能已经改变,导致闭包不按预期的方式工作。

解决这个问题的一种方法是在循环内部创建一个新的变量,将循环变量的值复制给这个新变量,然后在闭包中引用这个新变量。这将确保闭包捕获的是新变量的地址,而不是循环变量的地址。另一种方法是将循环变量作为闭包函数的参数传递,以确保闭包捕获的是参数的值。
*/
defer函数调用参数
package main

import (
	"fmt"
)

func foo(a int) {
	fmt.Println("Value of a in foo:", a)
}

func main() {
	a := 5

	defer foo(a) // 参数 a 在 defer 语句执行时进行计算,值为 5

	a = 10 // 修改 a 的值

	fmt.Println("Value of a in main:", a)
}


/*
在 Go 语言中,defer 语句在函数返回之前调用,但其中的函数参数在 defer 语句执行时会被计算,而不是在函数返回时计算。

这意味着,如果在 defer 语句中调用函数并传递参数,这些参数将使用当前时刻的值进行函数调用。即使函数执行期间参数值发生了变化,被 defer 调用的函数仍然会使用 defer 语句执行时的参数值。
在这个示例中,尽管在 main 函数执行期间 a 的值从 5 更改为 10,但由于 defer 语句中调用 foo 函数时使用的是 defer 语句执行时的参数值,因此最终输出将是:
Value of a in main: 10
Value of a in foo: 5
*/
defer语句调用实在当前函数结束之后调用,而不是变量的作用范围
package main

import "fmt"

func main() {
	for i := 0; i < 3; i++ {
		defer fmt.Println("Deferred in loop:", i)
	}

	fmt.Println("Outside loop")
}
/*
在 Go 语言中,defer 语句会在包含它的函数执行完成(函数的正常返回或发生了 panic)之后调用,而不是在变量的作用范围结束之后调用。

这意味着,如果你在循环中使用了 defer,那么在每次循环迭代时,defer 语句都会被执行,但它们实际上会被推迟到整个函数执行完成之后才真正调用。这可能会导致某些意外的结果,特别是在涉及循环变量的情况下。
在这个示例中,defer 语句会在循环中的每次迭代时执行,但它们实际上会在整个 main 函数执行完成后调用。因此,最终的输出将是:
Outside loop
Deferred in loop: 2
Deferred in loop: 1
Deferred in loop: 0
defer 语句实际上是在函数结束之后调用,而不是在变量的作用范围结束之后调用*/
失败的类型断言
package main

import "fmt"

func main() {
	var i interface{} = "Hello"

	// 正确的类型断言
	s, ok := i.(string)
	if ok {
		fmt.Println("Type assertion successful:", s)
	} else {
		fmt.Println("Type assertion failed")
	}

	// 错误的类型断言
	f, ok := i.(float64)
	if ok {
		fmt.Println("Type assertion successful:", f)
	} else {
		fmt.Println("Type assertion failed")
	}
}

//在 Go 语言中,类型断言用于判断一个接口类型的实际底层类型,并可以将其转换为具体的类型。如果类型断言失败,也就是底层类型与所期望的类型不匹配,Go 会在运行时产生 panic。为了避免这种情况,可以使用类型断言的“comma, ok”模式,这允许在类型断言失败时,通过额外的变量检测失败并执行相应的操作。
阻塞的goroutine与资源泄漏
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)

	go func() {
		// 这里我们一直等待通道关闭,但通道未被关闭,导致阻塞
		for range ch {
			fmt.Println("Received from channel")
		}
		fmt.Println("Goroutine finished")
	}()

	// 主 Goroutine 没有关闭通道,导致上面的 Goroutine 一直阻塞
	time.Sleep(2 * time.Second)
	fmt.Println("Main Goroutine finished")
}


/*
对于资源泄漏问题,主要关注的是内存泄漏,即当对象不再使用时,内存没有被正确释放。这与 Goroutine 的阻塞没有直接关系。

阻塞的 Goroutine 会占用内存资源,但是它们不会导致真正的资源泄漏。这是因为 Go 运行时会注意到这些阻塞的 Goroutine,并在适当的时候进行垃圾回收,释放这些 Goroutine 占用的内存资源。

然而,虽然阻塞的 Goroutine不会导致真正的资源泄漏,但是它们可能会导致应用程序中的内存占用持续增加,直到垃圾回收运行并释放这些 Goroutine 占用的内存。如果这些 Goroutine 的阻塞导致大量内存占用,可能会影响应用程序的性能和稳定性。

总之,阻塞的 Goroutine 不会引起真正的资源泄漏,但可能会导致内存占用增加,直到垃圾回收运行。因此,开发人员应该小心处理阻塞的 Goroutine,以确保应用程序的内存使用保持在合理的范围内。
*/
高级
nil值的interface{}不等于nil
package main

import (
	"fmt"
)

func main() {
	var data *byte
	var in interface{}

	fmt.Println(data, data == nil)
	fmt.Println(in, in == nil)

	in = data
	fmt.Println(in, in == nil)
}


//在 Go 中,一个 interface{} 变量包含两个部分:一个动态的类型(Type)和一个动态的值(Value)。当你将一个指针赋值给 interface{} 变量时,这个变量会持有指针的类型和值
//当 data 被声明时,它的值是 nil,因为它是一个指针,并且 data == nil 返回 true。

//当 in 被声明时,它的值也是 nil,同样地,in == nil 返回 true。

//在将 data 赋值给 in 后,in 持有了 data 的类型和值,而 data 的值是 nil,但是 in 的类型是 *byte,值是 nil ,所以 in == nil 返回 false。

//这个现象是因为在判断 in 是否为 nil 时,Go 实际上是判断了它的值是否为 nil,而忽略了类型的信息。因此,即使 in 的值是 nil,它也不会被判断为 nil。

//需要注意的是,这种行为是 Go 语言特有的。在其他一些编程语言中,可能会根据类型来判断 interface{} 是否为 nil,但在 Go 中并非如此
变量内存的分配
package main

import "fmt"

func main() {
    // 在Go中,声明变量并初始化时,会自动根据上下文分配内存
    a := 10         // 分配整数变量 a 在栈上
    b := "Hello"    // 分配字符串变量 b 在栈上
    c := new(int)   // 使用 new 函数在堆上分配整数变量 c

    *c = 20 // 对堆上分配的整数变量 c 进行赋值

    fmt.Println(a) // 打印整数变量 a
    fmt.Println(b) // 打印字符串变量 b
    fmt.Println(*c) // 打印堆上分配的整数变量 c 的值

    // 当离开作用域时,这些变量的内存会被自动释放
}

//当我们在 Go 中声明变量时,内存的分配通常由运行时系统自动处理
//在这个示例中,我们声明了三个变量 a、b 和 c。Go 运行时会根据上下文自动将变量 a 和 b 分配在栈上,而使用 new 函数创建的变量 c 会被分配在堆上。在离开作用域时,这些变量的内存会被自动释放,无需我们手动管理内存的分配和释放。

//请注意,Go 语言的内存分配和释放是由运行时系统自动管理的,这使得代码更加简洁和安全
GOMAXPROCS、Concurrency和Parallelism
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	// 设置使用两个操作系统线程来执行并行任务
	// 这将允许两个 goroutine 同时执行
	// 注意:实际应用中不一定需要手动设置 GOMAXPROCS
	// Go 1.5 以后,默认值已经是 CPU 核心数
	// runtime.GOMAXPROCS(2)

	// 使用 WaitGroup 来等待所有 goroutine 完成
	var wg sync.WaitGroup

	wg.Add(2) // 增加等待的 goroutine 数量

	fmt.Println("Start")

	// 第一个并发任务
	go func() {
		defer wg.Done() // 减少等待的 goroutine 数量
		for i := 0; i < 5; i++ {
			fmt.Println("Task 1:", i)
			time.Sleep(time.Millisecond * 500)
		}
	}()

	// 第二个并发任务
	go func() {
		defer wg.Done() // 减少等待的 goroutine 数量
		for i := 0; i < 5; i++ {
			fmt.Println("Task 2:", i)
			time.Sleep(time.Millisecond * 300)
		}
	}()

	// 等待所有 goroutine 完成
	wg.Wait()

	fmt.Println("Done")
}

/*
GOMAXPROCS: GOMAXPROCS 是一个环境变量,它用于控制同时执行的操作系统线程数。在 Go 语言中,一个操作系统线程对应一个物理处理器核心。默认情况下,Go 程序会使用与计算机的逻辑核心数相等的操作系统线程。通过设置 GOMAXPROCS,你可以控制并发的程度。但是请注意,并不是设置得越高越好,过多的线程可能会造成上下文切换带来的开销,降低性能。

并发(Concurrency): 并发是指在同一时间段内处理多个任务,它强调不同任务之间的相互配合和交替执行。在 Go 语言中,通过使用 goroutines 和 channels 实现并发,goroutines 是轻量级的线程,而 channels 则是用于不同 goroutines 之间的通信和数据同步。

并行(Parallelism): 并行是指在同一时刻执行多个任务,它实际上是并发的一个特例。并行是在多核处理器上的多个物理线程同时执行不同的任务,以提高处理能力。在 Go 中,可以使用 goroutines 和 GOMAXPROCS 来实现并行。通过设置 GOMAXPROCS 为大于 1 的值,可以让多个 goroutines 在多个操作系统线程上同时执行。

在这个示例中,我们使用了两个 goroutine 来执行两个任务(Task 1 和 Task 2)。通过使用 sync.WaitGroup 来等待所有的 goroutine 完成。注意,我们没有显式地设置 GOMAXPROCS,因为在 Go 1.5 之后,默认值已经是 CPU 核心数。

在运行此示例时,你会看到两个任务的输出交替出现,这是因为它们在不同的 goroutine 中并发执行。这就是并发的效果。如果你希望更多地看到并行的效果,可以取消 runtime.GOMAXPROCS(2) 的注释,这会让这两个任务在两个不同的操作系统线程上并行执行。

需要注意的是,并发和并行的效果在不同的计算机和环境中可能会有所不同。在真实的应用中,需要根据实际需求和硬件性能来进行调整

* Go 1.4及以下版本每个操作系统线程只使用一个执行上下文execution context)。这意味着每个时间片,只有一个goroutine执行。
* 从Go 1.5开始可以设置执行上下文的数量为CUP内核数量runtime.NumCPU(),也可以通过GOMAXPROCS环境变量来设置,还可调用runtime.GOMAXPROCS()函数来设置。
* 注意,GOMAXPROCS并不代表Go运行时能够使用的CPU数量,它是一个小256的数值,可以设置比实际的CPU数量更大的数字*/
读写操作排序
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	counter int
	mutex   sync.Mutex // 互斥锁
)

func main() {
	var wg sync.WaitGroup
	const numGoroutines = 10

	wg.Add(numGoroutines)

	for i := 0; i < numGoroutines; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 10; j++ {
				incrementCounter()
				time.Sleep(time.Millisecond)
			}
		}()
	}

	wg.Wait()
	fmt.Println("Final counter:", counter)
}

func incrementCounter() {
	mutex.Lock()
	defer mutex.Unlock()
	counter++
}


/*
Go可能会对一些操作排序,但它保证在goroutine的所有行为保持不变,但是,它无法保证在跨多个goroutine时的执行顺序
在并发编程中,为了避免竞态条件(Race Condition)和数据不一致等问题,我们需要对读写操作进行合理的排序和同步。以下是一些常见的读写操作排序和同步方法:

互斥锁(Mutex): 使用互斥锁可以保证同一时刻只有一个 goroutine 可以访问被锁定的资源。在读写操作混合的情况下,使用读写锁(RWMutex)可以提供更高的并发性。读锁允许多个 goroutine 同时读取,写锁则保证只有一个 goroutine 能够写入。

原子操作(Atomic Operations): Go 提供了一些原子操作函数,如 atomic.AddInt32、atomic.LoadInt32 等。这些操作能够在不需要锁的情况下进行原子性的读写操作,适用于一些简单的计数器等场景。

Channel 同步: 使用无缓冲的 Channel 可以实现 goroutine 之间的同步。通过发送和接收操作,可以保证在某个操作完成之后再执行另一个操作。

WaitGroup: sync.WaitGroup 可以用来等待一组 goroutine 的完成。通过增加和减少计数器,主线程可以等待所有的 goroutine 完成后再继续执行。

Select 语句: 当有多个 Channel 可读或可写时,使用 select 语句可以对这些 Channel 进行排序和选择,从而控制读写的顺序和时机。

条件变量(Cond): 使用条件变量可以在 goroutine 之间实现更高级的同步逻辑。条件变量允许一个或多个 goroutine 等待一定条件满足后再继续执行。

内存屏障: 在涉及到内存可见性的场景中,可以使用内存屏障操作来保证数据的一致性。Go 提供了 sync/atomic 包中的 atomic.Store 和 atomic.Load 等函数来实现内存屏障。

在实际应用中,根据具体情况选择适合的同步方法非常重要。不同的场景可能需要不同的同步策略来保证数据的一致性和正确性。需要根据并发编程的原则和实践,合理选择合适的同步方法。

在这个示例中,counter 是一个共享的计数器。多个 goroutine 会同时调用 incrementCounter 函数来增加计数器的值。通过使用互斥锁 mutex 来确保在修改 counter 时只有一个 goroutine 能够访问。这样就避免了竞态条件和数据不一致的问题。

需要注意的是,在实际应用中,使用互斥锁可能会降低并发性能,因为每次只有一个 goroutine 能够访问被锁定的资源。因此,在选择同步方法时,需要权衡并发性能和数据的一致性。
*/
优先调度
package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	go longRunningCalculation()
	go shortTask()

	// 等待一段时间,以便观察输出
	time.Sleep(3 * time.Second)
}

func longRunningCalculation() {
	fmt.Println("Starting long running calculation...")
	// 模拟一个长时间的计算
	for i := 0; i < 1000000000; i++ {
		_ = i * 2

		// 在每次循环时使用 Gosched() 让出时间片
		//if i%10000 == 0 {
		//	  runtime.Gosched()
		//}
	}
	fmt.Println("Long running calculation finished.")
}

func shortTask() {
	fmt.Println("Short task is executing.")
}


/*

有一些情况下,某个 goroutine 可能会阻塞调度器的正常工作,从而影响其他 goroutine 的执行。

以下是一些可能会导致调度问题的情况:

长时间的计算: 如果某个 goroutine 执行了一个非常耗时的计算,可能会阻塞调度器,导致其他 goroutine 得不到执行机会。

长时间的循环: 一个包含大量计算的循环可能会导致调度器无法切换到其他 goroutine。

阻塞操作: 当一个 goroutine 执行一个阻塞操作(例如等待一个 channel 或进行系统调用)时,调度器会尝试切换到其他可执行的 goroutine。

不合理的锁操作: 如果一个 goroutine 持有某个锁并且不释放,其他需要这个锁的 goroutine 将无法继续执行。

为了避免这些问题,开发者可以遵循一些最佳实践:

使用适当的并发控制方法,如互斥锁和通道,来确保 goroutine 之间的同步和通信。
避免在 goroutine 中执行过长时间的计算或循环,可以将这些计算分解为更小的任务,以便让调度器有机会切换到其他 goroutine。
如果一个 goroutine 长时间阻塞,确保这是必要的,并考虑采用非阻塞的方式来处理。
在极端情况下,如果某个 goroutine 阻塞了调度器的正常工作,开发者可以使用 runtime.Gosched() 显示地让出时间片,以允许调度器切换到其他等待执行的 goroutine。然而,正常情况下,开发者不需要过于关注调度的细节,因为 Go 语言的调度器会尽力平衡 goroutine 之间的执行,以提供良好的并发性能

在这个示例中,longRunningCalculation 函数模拟了一个长时间的计算,循环了一百万次。由于这个计算非常耗时,它可能会占用大量的 CPU 时间,从而导致调度器无法切换到其他 goroutine。

shortTask 函数是一个执行时间很短的任务。

运行这个示例后,你可能会观察到 longRunningCalculation 开始后,shortTask 会暂时无法执行,因为调度器会优先让长时间计算的 goroutine 继续执行。这可能会导致调度问题,因为 shortTask 的执行受到了阻塞。

为了解决这个问题,可以将长时间的计算任务分解为更小的任务,或者使用并发控制方法,如 goroutine 间的通信和互斥锁,以确保调度器能够更好地切换不同的 goroutine

我们放开 runtime.Gosched这段代码
longRunningCalculation 函数在每次循环的一部分使用了 runtime.Gosched(),以便让出时间片。这样做可以在长时间计算的过程中允许调度器切换到其他 goroutine。

请注意,虽然使用 runtime.Gosched() 可以让出时间片,但这并不是解决问题的唯一方法。在实际开发中,更好的方法通常是将耗时的计算分解为更小的任务,或者使用通信和同步机制来确保调度器的正常切换
*/
更新map值的字段
package main

import "fmt"

type Student struct {
    Name  string
    Score int
}

func main() {
    // 创建一个 map,值类型是结构体类型
    students := make(map[string]Student)

    // 向 map 中添加键值对
    students["Alice"] = Student{Name: "Alice", Score: 95}
    students["Bob"] = Student{Name: "Bob", Score: 82}

    // 从 map 中取出结构体值,并更新字段值
    alice := students["Alice"]
    alice.Score = 98
    students["Alice"] = alice

    // 打印 map 中的结构体值
    fmt.Println("Alice's info:", students["Alice"])

    // 创建一个结构体类型的 slice
    studentSlice := []Student{
        {Name: "Charlie", Score: 70},
        {Name: "David", Score: 88},
    }

    // 更新结构体类型的 slice 中的字段值
    studentSlice[0].Score = 75

    // 打印更新后的结构体类型的 slice
    fmt.Println("Updated student slice:", studentSlice)
}
/*在这个示例中,我们首先创建了一个值类型为 Student 的 map,并向其中添加键值对。然后,我们从 map 中取出结构体值,更新了其中的字段,并将更新后的结构体值再次放回到 map 中。接着,我们创建了一个结构体类型的 slice,并更新了其中的一个结构体的字段值。

需要确保在更新结构体值时,将更新后的值再次放回到 map 中,否则 map 中的值不会被更新。对于结构体类型的 slice,由于它们的元素是直接的结构体值,所以可以直接更新字段值。*/
用值实例上调用接受者为指针的方法
package main

import (
	"fmt"
)

type Point struct {
	X, Y int
}

func (p *Point) Move(dx, dy int) {
	p.X += dx
	p.Y += dy
}

func main() {
	// 可寻址的值变量,直接调用接收者为指针类型的方法
	point := Point{X: 10, Y: 20}
	point.Move(5, 5)
	fmt.Println("Point:", point)

	// 不可寻址的 map 元素,需要先获取到指针后调用方法
	points := map[int]Point{
		1: {X: 10, Y: 20},
	}
	p := &points[1]
	p.Move(5, 5)
	fmt.Println("Map point:", points[1])
}

/*
在 Go 中,对于可寻址的值变量(即指针或包含在结构体或数组中的元素),可以直接调用接收者为指针类型的方法,不需要为可寻址值变量定义以接收对象为值类型的方法。这是因为 Go 在需要的时候会自动将值变量转换为指针类型,以便调用接收者为指针类型的方法。

但是需要注意的是,并不是所有的变量都是可寻址的。像 map 中的元素、不可寻址的结构体字段或数组切片的元素等都不是可寻址的。在这些情况下,如果要调用接收者为指针类型的方法,你需要先获取到该变量的指针。

在这个示例中,我们定义了一个 Point 结构体,并为其定义了一个接收者为指针类型的方法 Move。首先,我们创建了一个可寻址的值变量 point,可以直接调用 Move 方法。然后,我们创建了一个 map points,将 Point 结构体作为其值类型。在这种情况下,我们需要先获取到 map 元素的指针,然后才能调用 Move 方法。*/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值