Golang保姆级知识点讲解

在这里插入图片描述

文章目录


一、命名规范

1.1 Go是一门区分大小写的语言。
命名规则涉及变量、常量、全局函数、结构、接口、方法等的命名。 Go语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。

当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Analysize,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )
1.2 包名称
保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为全小写单词,不要使用下划线或者混合大小写。
在Go语言中,一个包可以包含多个文件,这是Go语言的一种组织代码的方式,有助于保持代码的模块化和可维护性。以下是有关一个包可以包含多个文件的一些重要事项:

  1. 包名一致性:无论一个包包含多少个文件,它们的包声明应该一致,即它们都应该声明为相同的包名。这意味着每个文件的第一行应该是package 包名,并且所有文件都应该使用相同的包名。

  2. 导入声明:在每个Go文件的开头,您需要包含导入声明,以指定该文件依赖的其他包。这些导入声明可以在不同的文件中重复,但它们应该是一致的,以确保代码的一致性。

  3. 分割功能:通常,将一个包的相关功能拆分到多个文件中,以便每个文件都有一个清晰的目的。例如,您可以将与数据库交互的代码放在一个文件中,将HTTP处理程序放在另一个文件中,以此类推。

  4. 包级别变量和常量:包级别的变量和常量可以在包的多个文件中共享。这可以用于在不同的文件中存储包的状态或配置信息。

  5. 测试文件:通常,一个包还可以包含与测试相关的文件,这些文件以_test.go为后缀,并包含用于测试包中代码的测试函数。

  6. 避免循环依赖:当一个包包含多个文件时,要注意避免循环依赖问题。循环依赖可能会导致编译错误或运行时问题。

示例:

myutils/
    ├── myutils.go        // 包的入口文件,包含包级别的导出函数
    ├── helpers.go        // 包含辅助函数
    ├── constants.go      // 包含常量定义
    ├── datastructures.go // 包含数据结构定义
    └── myutils_test.go   // 包含测试函数

在Go语言中,文件名和包名有一些命名规范和最佳实践。以下是一些关于Go文件命名的通用规则和建议:

  1. 文件名应该与包名一致:Go的包名通常应该与文件夹的名称一致,而且在一个文件夹中的所有Go文件的包声明应该相同。这有助于代码的可读性和维护性。

  2. 文件名:文件名使用蛇形命名法比如user_name.go

  3. 变量:采用驼峰命名法,不同于php和python,推崇简短取单词首字母

  4. 接口:命名和结构体一致,接口是以er结尾或者首字母加一个大写的I或者都不用

  5. 常量:全部大写,如果有多个单词使用蛇形命名法,专有名词比如InterestAPI

  6. 结构体:驼峰,首字母大写或者小写

  7. 注释规范://单行注释、/* */大段注释

  8. 项目名:采用蛇形命名规范,使用下划线连接
    示例:

  • 包名为myutils,则文件名可以为myutils.go
  • 包名为database,则文件名可以为database.go
  • 包名为http_server,则文件名可以为http_server.go

这些规则和建议有助于确保Go代码的可读性和一致性,并使项目易于维护。最重要的是,选择一种命名风格并在整个项目中保持一致。

package domain
package main

1.3 文件命名
尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。

approve_service.go

1.4 结构体命名
采用驼峰命名法,首字母根据访问控制大写或者小写

struct 申明和初始化格式采用多行,例如下面:

type MainConfig struct {
    Port string `json:"port"`
    Address string `json:"address"`
}
config := MainConfig{"1234", "123.221.134"}

1.5 接口命名
命名规则基本和上面的结构体类型

单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer 。

type Reader interface {
        Read(p []byte) (n int, err error)
}

1.6 变量命名
和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

如果变量为私有,且特有名词为首个单词,则使用小写,如 appService
若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头

var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool

1.7常量命名
常量均需使用全部大写字母组成,并使用下划线分词

const APP_URL = "https://www.baidu.com"

如果是枚举类型的常量,需要先创建相应类型:

type Scheme string
 
const (
    HTTP  Scheme = "http"
    HTTPS Scheme = "https"
)
  1. 错误处理
    错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来
    尽早return:一旦有错误发生,马上返回
    尽量不要使用panic,除非你知道你在做什么
    错误描述如果是英文必须为小写,不需要标点结尾
    采用独立的错误流进行处理
    // 错误写法
if err != nil {
    // error handling
} else {
    // normal code
}
 
// 正确写法
if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code

4.1. 单元测试
单元测试文件名命名规范为 example_test.go 测试用例的函数名称必须以 Test 开头,例如:TestExample 每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试 。

在Go语言中,一个包(package)中只能包含一个具有main函数的文件。这个main函数是程序的入口点,也是Go编译器用来生成可执行文件的标识。一个Go程序只能有一个入口点,也就是只能有一个main函数。

如果你的Go项目中有多个包,每个包都可以有自己的main函数,但每个包都是一个独立的可执行程序。在这种情况下,你需要使用不同的包名称和文件夹来组织这些可执行程序。

例如,你可以创建一个名为myapp的文件夹,其中包含一个main函数,然后在另一个文件夹中创建一个不同的可执行程序,也包含一个独立的main函数。这两个可执行程序是独立的,它们的main函数分别是不同的入口点。

总之,一个包中只能包含一个main函数,但一个项目可以包含多个独立的包,每个包可以有自己的main函数和可执行程序。

在Go语言中,println 和 fmt 包(特别是 fmt.Println)都用于在控制台输出信息,但它们之间存在一些重要的区别。

println
println 是一个内建的函数,它不需要导入任何包就可以直接使用。
它接受任意数量的参数,并将它们转换为字符串(使用默认的格式),然后打印到标准错误输出(通常是终端)。
println 没有提供格式化输出的功能,例如控制字段宽度、精度、对齐方式等。
由于它打印到标准错误输出,它通常用于调试和诊断目的,而不是用于生成正常的程序输出。
fmt 包
fmt 包是一个功能强大的格式化输入输出包,它提供了许多函数和方法用于生成和解析格式化的字符串。
fmt.Println 是 fmt 包中的一个函数,它接受任意数量的参数,将它们格式化为字符串(根据默认的格式或提供的格式说明符),然后打印到标准输出(通常是终端)。
fmt 包还提供了许多其他函数和方法,例如 fmt.Printf 用于格式化打印到标准输出,fmt.Sprintf 用于生成格式化的字符串但不打印,以及 fmt.Scan、fmt.Scanf、fmt.Scanln 等用于从标准输入读取数据。
fmt 包支持丰富的格式化选项,可以控制字段宽度、精度、对齐方式、占位符等。
总结
对于简单的、临时的或调试目的的输出,可以使用 println。
对于需要更多控制、格式化或灵活性的输出,应该使用 fmt 包中的函数和方法。
在生产代码中,建议使用 fmt 包来生成和解析格式化的字符串,因为它提供了更多的功能和更好的可读性。
调试时候区别:对于切片,println输出切片大小和切片地址,fmt输出切片的内容和python一致

二、占位符

  1. %v:通用占位符,用于格式化各种类型的值,自动选择合适的格式。
  2. %T:用于格式化值的类型。
  3. %d:用于格式化整数(十进制)。
  4. %b:用于格式化整数为二进制。
  5. %o:用于格式化整数为八进制。
  6. %x:用于格式化整数为十六进制(小写字母)。
  7. %X:用于格式化整数为十六进制(大写字母)。
  8. %f:用于格式化浮点数。
  9. %e:用于以科学计数法格式化浮点数。
  10. %E:用于以科学计数法格式化浮点数(大写字母E)。
  11. %s:用于格式化字符串。
  12. %q:用于格式化带有引号的字符串。
  13. %c:用于格式化字符(Unicode码点)。
  14. %p:用于格式化指针。
  15. %U:用于格式化Unicode格式。

以下是一些示例:

num := 42
str := "Hello, World!"
fmt.Printf("数字:%d, 二进制:%b, 十六进制:%x, 字符串:%s\n", num, num, num, str)

这将会输出:“数字:42, 二进制:101010, 十六进制:2a, 字符串:Hello, World!”。

请注意,占位符的具体用法取决于要格式化的值的类型,您可以根据需要选择合适的占位符。

三、数据类型

  1. 整数类型

    • int:根据底层架构的不同,可以是32位或64位。
    • int8:8位有符号整数。
    • int16:16位有符号整数。
    • int32:32位有符号整数。
    • int64:64位有符号整数。
    • uint:根据底层架构的不同,可以是32位或64位的无符号整数。
    • uint8:8位无符号整数。
    • uint16:16位无符号整数。
    • uint32:32位无符号整数。
    • uint64:64位无符号整数。
  2. 浮点数类型

    • float32:32位浮点数。
    • float64:64位浮点数。
  3. 复数类型

    • complex64:包含两个32位浮点数的复数。
    • complex128:包含两个64位浮点数的复数。
  4. 布尔类型

    • bool:表示布尔值,只有truefalse两个取值。
  5. 字符串类型

    • string:表示字符串。
  6. 字符类型

    • byte:别名为uint8,用于表示ASCII字符。
    • rune:别名为int32,用于表示Unicode字符。
  7. 指针类型

    • *T:表示指向类型T的指针。
  8. 数组类型

    • [n]T:表示包含n个类型T的元素的数组。
  9. 切片类型

    • []T:表示可变长度的序列,其中的元素类型为T
  10. 字典类型

    • map[K]V:表示键值对的集合,其中键的类型为K,值的类型为V
  11. 结构体类型

    • struct:用户自定义的复合类型,可以包含不同类型的字段。
  12. 接口类型

    • interface:用于定义方法集,实现了接口中所有方法的类型被认为实现了该接口。
    • interface{} 是 Go 语言中的一个特殊类型,通常称为空接口(Empty Interface)。它具有特殊的性质,可以包含任何类型的值。

一个空接口不包含任何方法签名,因此它可以用来表示任何类型的值。这使得空接口在处理不同类型的数据时非常灵活,但也需要在使用时小心,因为在运行时需要进行类型断言来访问其内部值。

以下是 interface{} 的主要特点和用法:

  1. 可以包含任何类型的值:空接口可以用来存储任何类型的数据,包括基本数据类型(如整数、字符串、浮点数)、自定义类型、结构体、函数、切片、映射、指针等等。

  2. 类型断言:要访问空接口内部的具体值,需要进行类型断言。这是通过在运行时检查值的实际类型来实现的。

    var i interface{}
    i = 42 // 存储一个整数
    val, ok := i.(int) // 尝试将值断言为整数类型
    if ok {
        fmt.Printf("i 是一个整数: %d\n", val)
    } else {
        fmt.Println("i 不是整数")
    }
    
  3. 常见用途:空接口常用于以下情况:

    • 在函数参数中接受不同类型的数据。
    • 在泛型编程中,实现通用的数据结构或算法。
    • 在处理 JSON 数据时,解析不同类型的字段值。
    • 在与其他类型相互转换时,例如将自定义类型转换为接口类型。

总之,interface{} 提供了一种通用的方式来处理不同类型的数据,但需要小心处理类型断言以及确保类型匹配,以避免运行时错误。

这些是Go语言中的一些常见数据类型。每个类型都有其特定的用途和特性,根据需要选择合适的类型来存储和操作数据。
在Go中,可以在函数的外部创建全局变量。这些全局变量可以在程序的任何地方访问,包括函数内部。全局变量的生命周期与整个程序的生命周期相同,它们在程序启动时创建,在程序结束时销毁。

以下是一个示例,演示了如何在函数的外部创建和使用全局变量:

package main

import "fmt"

// 在函数外部定义全局变量
var globalVar int = 10

func main() {
	// 访问全局变量
	fmt.Println("Global Variable:", globalVar)

	// 修改全局变量的值
	globalVar = 20
	fmt.Println("Modified Global Variable:", globalVar)

	// 在函数内部也可以访问全局变量
	accessGlobalVar()
}

func accessGlobalVar() {
	// 在函数内部访问全局变量
	fmt.Println("Accessed Global Variable from a Function:", globalVar)
}

在这个示例中,globalVar 是一个全局变量,它在函数main的外部定义。我们可以在main函数内部和accessGlobalVar函数内部访问和修改这个全局变量。

需要注意的是,虽然全局变量可以在程序的任何地方访问,但过度使用全局变量可能会导致代码的可维护性和测试性降低,因此在设计程序时应该谨慎使用全局变量,尤其是当它们可能被多个并发的协程访问时。在Go中,通常鼓励使用局部变量和函数参数来尽可能限制变量的作用域,以提高代码的可读性和可维护性。

3.1.ASCII和Unicode字符编码标准

  1. ASCII码(American Standard Code for Information Interchange):ASCII码是最早的字符编码标准,它最初使用7位二进制数来表示128个字符,包括英文字母、数字、标点符号和一些控制字符。ASCII码主要用于英语和一些西欧语言的文本,它在计算机通信和文本处理领域有着重要的历史地位。

  2. Unicode:Unicode是一种更广泛的字符编码标准,它旨在涵盖世界上几乎所有的书写系统,包括各种语言的字符、符号和表情符号。Unicode使用16位或32位二进制数来表示字符,因此可以表示数百万个不同的字符。Unicode的目标是为全球化应用程序和互联网提供一个统一的字符编码方案,使不同语言的文本能够在同一文档或应用程序中共存。

关系:

  • Unicode实际上包括ASCII字符在内,前128个Unicode字符与标准ASCII字符是一样的,因此ASCII字符是Unicode的子集。
  • Unicode的出现解决了ASCII码在只能表示有限字符集的问题,使得各种语言的字符都可以用统一的方式进行编码。
  • 当处理国际化应用程序或文本时,Unicode通常是首选的字符编码标准,因为它能够涵盖全球范围的字符需求,而ASCII码主要用于英语文本或特定要求的场景。

四、运算操作

  1. 算术运算:
   - 加法:+
   - 减法:-
   - 乘法:*
   - 除法:/
   - 取余:%
   - 自增:++
   - 自减:--
  1. 位运算:
   - 按位与:&
   - 按位或:|
   - 按位异或:^
   - 按位取反:~
   - 左移:<<
   - 右移:>>

以下是按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)运算的示例:

假设我们有两个二进制数字:A = 1100 和 B = 1010。

  1. 按位与(&):

    • A & B 的结果是 1000。只有两个数字的对应位都为1时,结果位才为1。
  2. 按位或(|):

    • A | B 的结果是 1110。只要两个数字的对应位中有一个为1,结果位就为1。
  3. 按位异或(^):

    • A ^ B 的结果是 0110。只有两个数字的对应位不相同,结果位才为1。
  4. 按位取反(~):

    • ~A 的结果是 0011。将每个位取反,0 变为1,1 变为0。
  5. 左移(<<):

    • A << 2 的结果是 110000。将A向左移动两位,右侧填充0。
  6. 右移(>>):

    • B >> 1 的结果是 0101。将B向右移动一位,左侧填充0。

这些运算对于处理位级别的操作非常有用,例如位掩码、位标志和二进制数据的处理。
以下是按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)运算的表格示例:

操作描述示例1 (二进制)示例1 (十进制)示例2 (二进制)示例2 (十进制)
&按位与(AND)1100 & 101010001011 & 11011001
|按位或(OR)1100 | 101011101011 | 11011111
^按位异或(XOR)1100 ^ 101001101011 ^ 11010110
~按位取反(NOT)~11000011~10110100
<<左移位(左移)1100 << 20011 00001011 << 31011 0000
>>右移位(右移)1010 >> 101011101 >> 20011

请注意,示例中的二进制数字和十进制数字都只是演示用的示例。您可以用不同的二进制数字来尝试这些操作,根据需要进行计算。这些操作对于位级别的操作和位掩码非常有用。
3. 比较运算:

   - 等于:==
   - 不等于:!=
   - 大于:>
   - 小于:< 
   - 大于等于:>=
   - 小于等于:<=
  1. 逻辑运算:
   - 逻辑与:&&
   - 逻辑或:||
   - 逻辑非:!
  1. 赋值运算:
   - 赋值:=
   - 加法赋值:+=
   - 减法赋值:-=
   - 乘法赋值:*=
   - 除法赋值:/=
   - 取余赋值:%=
  1. 其他运算:
   - 取地址:&
   - 取值:*

在Golang中,各个算数运算符具有一些注意点和行为,以下是一些常见的算数运算符以及相关注意事项:

  1. 加法运算符 +

    • 用于将两个数相加。
    • 如果操作数都是整数,结果也是整数;如果操作数中有浮点数,结果将是浮点数。
    • 注意整数溢出问题,Golang不会自动溢出到更大的数据类型,而是会导致错误。
  2. 减法运算符 -

    • 用于从左侧操作数中减去右侧操作数。
    • 类似加法,如果操作数是整数,结果也是整数;如果有浮点数,结果将是浮点数。
  3. 乘法运算符 *

    • 用于将两个数相乘。
    • 类似加法和减法,如果操作数是整数,结果也是整数;如果有浮点数,结果将是浮点数。
    • 注意整数溢出问题。
  4. 除法运算符 /

    • 用于将左侧操作数除以右侧操作数。
    • 如果两个操作数都是整数,则结果将截断为整数,即舍弃小数部分。
    • 如果其中一个操作数是浮点数,则结果将为浮点数。
  5. 取余运算符 %

    • 用于计算左侧操作数除以右侧操作数的余数。
    • 只能用于整数运算,结果将是整数。
  6. 自增和自减运算符 ++--

    • 用于增加或减少一个变量的值。
    • 可以用作前缀(例如 ++x)或后缀(例如 x++)运算符,它们的行为略有不同。
    • 前缀运算符会在操作之前增加或减少变量的值,而后缀运算符会在操作之后增加或减少变量的值。
  7. 运算符优先级

    • 请注意算术运算符的优先级,以确保表达式按预期计算。通常,乘法和除法的优先级高于加法和减法。
  8. 类型转换

    • 当进行混合类型的算术运算时,需要注意Go的类型转换规则,以避免意外结果或编译错误。

五、流程控制

在Golang中,有几种常见的流程控制结构,包括条件语句、循环语句和分支语句。以下是这些结构的示例:

  1. 条件语句(Conditional Statements):
  • if语句:根据条件执行不同的代码块。变量可以声明在判断表达式里面用;分隔后面跟条件判断,这是局部变量,注意作用域,不允许0、1来表示真假
package main

import "fmt"

func main() {
    age := 18
    if age >= 18 {
        fmt.Println("你已经成年了")
    } else {
        fmt.Println("你未成年")
    }
}
  • switch语句:根据表达式的值选择不同的分支。
package main

import "fmt"

func main() {
    day := "Monday"
    switch day {
    case "Monday":
        fmt.Println("今天是星期一")
    case "Tuesday":
        fmt.Println("今天是星期二")
    default:
        fmt.Println("今天不是星期一或星期二")
    }
}
  1. 循环语句(Loop Statements):
  • for循环:用于重复执行代码块。
package main

import "fmt"

func main() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
    }
}
  • while循环:Go中没有专门的while循环,但可以使用for来模拟。
package main

import "fmt"

func main() {
    sum := 0
    i := 1
    for i <= 5 {
        sum += i
        i++
    }
    fmt.Println("总和是:", sum)
}
  1. 分支语句(Branching Statements):
  • break语句:用于跳出循环。
package main

import "fmt"

func main() {
    for i := 1; i <= 5; i++ {
        if i == 3 {
            break
        }
        fmt.Println(i)
    }
}
  • continue语句:用于跳过当前循环迭代,继续下一次迭代。
package main

import "fmt"

func main() {
    for i := 1; i <= 5; i++ {
        if i == 3 {
            continue
        }
        fmt.Println(i)
    }
}

for range 主要用于迭代切片、数组、映射等数据结构中的元素。以下是一个示例:

for range循环:用于迭代切片(slice)中的元素。

package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    for index, value := range numbers {
        fmt.Printf("索引:%d, 值:%d\n", index, value)
    }
}
  1. 条件语句 (if-else) 注意点

    • Go的if语句中不需要用括号包围条件。
    • 条件表达式的结果必须是布尔值(true或false)。
    • 尽量避免在if语句中定义新的变量,最好在外部定义。
  2. Switch语句 注意点

    • Switch语句自动break,不需要显式使用break关键字。
    • 每个case的值必须唯一且是常量,不会出现隐式的fall-through(即不会顺序执行下一个case)。
    • 可以使用fallthrough关键字来实现fall-through效果,但要小心使用,确保明白其行为。
  3. 循环语句 (for) 注意点

    • Go中只有for循环,但可以模拟其他类型的循环,如while和do-while。
    • 使用for循环来遍历切片、数组、映射等数据结构时,可以使用range关键字来简化迭代过程。
  4. 分支语句 (break, continue) 注意点

    • break用于跳出循环或switch语句。
    • continue用于跳过当前循环迭代,继续下一次迭代。
    • 在循环中使用breakcontinue时,确保它们被合理地使用,以避免无限循环或跳出不正确的范围。
  5. Select语句 注意点

    • select语句用于处理多个通道操作,但不能用于普通的条件判断。
    • select中的case语句通常包括通道操作,例如发送或接收数据。
    • select中使用default语句可以实现非阻塞的通信。
  6. 错误处理 注意点

    • Go通常使用返回值来处理错误,而不是抛出异常。
    • 函数通常返回一个值和一个错误,检查错误是否为nil来判断是否发生了错误。
    • 使用defer语句可以确保资源的释放,例如关闭文件或释放锁。
      for range 循环在不同数据类型上的行为和返回值略有不同。以下是针对不同数据类型的 for range 循环的返回值说明:
  7. 切片(Slices)和数组(Arrays)

    • 在切片和数组上使用 for range 循环会返回两个值,一个是索引,另一个是元素的副本。
    • 例如:
      numbers := []int{1, 2, 3, 4, 5}
      for index, value := range numbers {
          // index 是索引,value 是元素的副本
          fmt.Printf("索引:%d, 值:%d\n", index, value)
      }
      
  8. 映射(Maps)

    • 在映射上使用 for range 循环会返回两个值,一个是键,另一个是键对应的值的副本。
    • 例如:
      person := map[string]string{"姓名": "小明", "年龄": "25"}
      for key, value := range person {
          // key 是键,value 是键对应的值的副本
          fmt.Printf("%s:%s\n", key, value)
      }
      
  9. 字符串(Strings)

    • 在字符串上使用 for range 循环会返回两个值,一个是字符的索引(按字节),另一个是字符的 Unicode 代码点。
    • 例如:
      text := "Hello, 你好"
      for index, char := range text {
          // index 是字节索引,char 是 Unicode 代码点
          fmt.Printf("索引:%d, 字符:%c\n", index, char)
      }
      
  10. 通道(Channels)

    • 在通道上使用 for range 循环会迭代通道中的元素,直到通道关闭。
    • 通道的返回值只有一个,实际上,for range 循环会自动阻塞等待通道的值,并将每个值存储在迭代变量中。

for range 结构在处理通道(channels)时非常有用,它用于迭代通道中的元素,直到通道关闭。这样可以很方便地从通道中读取数据,等待并处理通道中的元素。

以下是一个使用 for range 循环迭代通道的示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个通道
	ch := make(chan int)

	// 启动一个goroutine发送数据到通道
	go func() {
		for i := 1; i <= 5; i++ {
			ch <- i
			time.Sleep(time.Second) // 延迟1秒
		}
		close(ch) // 关闭通道
	}()

	// 使用for range循环迭代通道中的值
	for value := range ch {
		fmt.Println("接收到:", value)
	}
}

在这个示例中,我们创建了一个整数通道 ch,然后启动了一个goroutine,该goroutine向通道发送1到5的整数,并在每次发送后休眠1秒钟。然后,我们在主goroutine中使用 for range 循环迭代通道中的值,并在每次迭代中打印接收到的值。当通道被关闭后,循环会自动退出。

这种使用方式对于从通道中读取数据非常方便,因为它会自动等待并处理通道中的数据,直到通道关闭或没有更多的数据可读。这在并发编程中经常用于处理任务队列或事件通知等场景。

继续说明不同数据类型上使用for range循环的返回值:

  1. 指针数组和指针切片

    • 对于指针数组和指针切片,for range 循环会返回索引和指向元素的指针。
    • 例如:
      nums := []*int{&1, &2, &3}
      for index, ptr := range nums {
          // index 是索引,ptr 是指向元素的指针
          fmt.Printf("索引:%d, 值:%d\n", index, *ptr)
      }
      
  2. 自定义数据类型

    • 如果你有自定义数据类型,你可以为它实现 for range 的接口方法,这样你可以控制循环行为。
    • 例如:
      type MySlice []int
      
      func (m MySlice) rangeBehavior() {
          for i, v := range m {
              // 自定义行为
          }
      }
      

请注意,对于某些数据类型,你可以选择仅使用一个循环变量来获取迭代的元素,而忽略索引或键。这取决于你的具体需求。
在使用Go语言中的break语句时,有一些注意事项和最佳实践:

  1. break仅用于循环和switch语句:break语句主要用于终止循环或switch语句的执行。它不能在其他上下文中使用,例如if语句或函数内部。

  2. 避免滥用:过多的break语句可能会使代码难以维护。尽量编写简洁的循环和代码逻辑,以减少break的使用。

  3. 标签和嵌套循环:Go允许在循环前面加上标签,并在break语句中使用标签来指定要终止的是哪个循环,特别是在嵌套循环中很有用。但请谨慎使用标签,以确保代码的可读性。

outerLoop:
for i := 0; i < 5; i++ {
    for j := 0; j < 5; j++ {
        if someCondition {
            break outerLoop // 终止外部循环
        }
    }
}
  1. switch语句中的break:在switch语句中,break通常用于终止case分支的执行。不过,Go中的switch默认具有自动break行为,因此不必在每个case分支中使用break,除非你想要继续执行下一个case分支。
switch x {
    case 1:
        // 执行一些操作
    case 2:
        // 执行一些操作
    default:
        // 默认操作
}

总之,使用break时要谨慎,确保它们用于逻辑清晰的地方,以提高代码的可读性和可维护性。避免过多的嵌套和标签,除非有必要。
breakfallthrough是Go语言中用于控制switch语句行为的关键字。

  1. break
    • switch语句中,break用于终止当前case分支的执行,防止代码继续执行下一个case分支。
    • 如果不使用break,Go的switch语句默认会在匹配到一个case后自动终止,不会继续执行其他case分支。
    • 使用break可以在满足条件后立即退出switch语句。
switch x {
    case 1:
        // 执行一些操作
        break
    case 2:
        // 这个分支将被跳过
}
  1. fallthrough
    • fallthrough是Go语言中的一个关键字,用于强制执行下一个case分支,无论条件是否匹配。
    • 默认情况下,Go的switch语句在匹配到一个case后会自动终止,不会继续执行其他分支。但如果在一个case分支中使用了fallthrough,则会继续执行下一个case分支,不管条件是否匹配。
switch x {
    case 1:
        // 执行一些操作
        fallthrough // 强制执行下一个分支
    case 2:
        // 这个分支也将被执行
}

请注意,fallthrough通常用得比较少,因为大多数情况下,我们希望switch语句在匹配到一个条件后自动终止。只有在某些特定的逻辑中,你可能需要使用fallthrough来达到预期的效果。要慎重使用fallthrough,以确保清晰的代码逻辑。

5.1.Go语言标签

在Go语言中,标签是可以自定义的 冒号结尾、标签(label)通常与goto语句一起使用,以在代码中实现跳转到标签所在的位置。标签可以用于循环和switch语句,以实现特定的控制流程。然而,Go语言鼓励少使用goto和标签,因为过多的跳转可能会导致代码难以理解和维护。以下是标签的基本用法和一些注意事项:

  1. 创建标签:
    • 标签的命名规则与变量命名相同,由字母、数字和下划线组成,并以冒号:结尾。
    • 通常,标签的命名应该具有描述性,以便于理解标签的用途。
myLabel:
  1. 在循环中使用标签:
    • 标签通常与for循环一起使用,以在特定条件下跳出或跳转回循环的特定位置。
loop:
for i := 0; i < 5; i++ {
    // 一些逻辑
    if someCondition {
        break loop // 跳出循环
    }
}
  1. switch语句中使用标签:
    • 标签还可以用于switch语句,以指定在case分支中的跳转位置。
switch x {
    case 1:
        // 执行一些操作
        goto myLabel // 跳转到标签位置
    case 2:
        // 执行一些操作
    myLabel:
        // 标签位置
}

在Go语言中,标签(label)通常用于循环语句(如for循环)和switch语句中,以帮助控制流程。标签后面可以跟冒号:,然后是代码块、循环、switch语句等。以下是一些示例,展示了标签后面可以跟的内容:

  1. 标签后面跟循环:

    outerLoop:
    for i := 0; i < 3; i++ {
        innerLoop:
        for j := 0; j < 3; j++ {
            if someCondition {
                break outerLoop // 使用标签控制外部循环
            }
        }
    }
    
  2. 标签后面跟switch语句:

    myLabel:
    switch x {
        case 1:
            // 执行一些操作
        case 2:
            // 执行一些操作
    }
    
  3. 标签后面跟代码块:

    myLabel:
    {
        // 一些代码
    }
    

请注意,标签通常用于在嵌套结构中的控制流程,以便在需要时能够跳出外部循环或执行特定的case分支。标签本身不执行任何操作,而是用于标识代码的位置,以便在需要时跳转到该位置。
注意事项:

  • 避免滥用标签和goto,因为过多的跳转会使代码难以维护和理解。
  • 使用标签时要确保代码逻辑清晰,标签名称应具有描述性,以便于理解其用途。
  • 通常情况下,应优先考虑使用其他控制结构(例如,breakcontinuereturn)来管理流程,而不是依赖goto和标签。
  • Go社区一般不鼓励过多使用goto和标签,因此在编写代码时要慎重考虑是否真的需要它们。

5.2.Go语言continue语句

break和continue都用于循环语句中、
在Go语言中,break 和 continue 默认只能用于跳出或继续单层循环。这是因为它们会影响到最内层的循环。如果你在嵌套循环中使用 break 或 continue,它们将仅影响到包含它们的最内层循环,而不会影响外层的循环。
**在Go语言中,continue语句用于跳过当前循环迭代的剩余部分,直接进入下一次迭代。以下是使用continue语句时需要注意的事项:

  1. 用于循环:continue语句通常用于for循环中,以控制循环的执行流程。它不可用于其他类型的控制结构,如if语句或switch语句。

  2. 放置位置:continue语句通常位于循环体内,用于跳过循环中的某些操作,而不是跳出整个循环。它会继续下一次循环迭代。

for i := 0; i < 5; i++ {
    if i == 2 {
        continue // 跳过 i==2 的情况,进入下一次迭代
    }
    // 执行一些操作
}
  1. 避免无限循环:使用continue时要确保它不会导致无限循环。例如,在一个for循环中,如果条件永远不满足,并且continue语句的位置不正确,可能会导致无限循环。
for i := 0; i < 5; i++ {
    // 永远不会满足条件,会导致无限循环
    if i == 10 {
        continue
    }
}
  1. 适当使用:continue是一种强制性的控制流程语句,应该谨慎使用。确保它用于清晰的逻辑,并且有合理的需求来跳过某些迭代。

  2. 可读性:为了提高代码的可读性,建议在使用continue时编写注释,说明为什么要跳过某些迭代,以便其他开发人员更容易理解你的意图。

总之,continue语句是一种有用的工具,用于在循环中控制流程。正确使用它可以使代码更加清晰和高效。**

5.3.Go语言goto语句

goto 是Go语言中的一个关键字,用于无条件地跳转到程序中的标签(label)。然而,goto 是一个强大但容易被滥用的功能,它通常被视为一种不良的编程实践,因此在使用时需要非常谨慎。以下是关于 goto 的注意事项和示例:

注意事项:

  1. 避免滥用: 尽量避免使用 goto。通常情况下,使用结构化的控制流程,如循环和条件语句,能够更清晰地表达程序逻辑,提高代码的可读性和可维护性。

  2. 清晰标签名称:

5.4.Go语言数组

Go语言中的数组是一种固定长度的数据结构,用于存储相同类型的元素。数组的长度是其类型的一部分,这意味着你在声明数组时必须指定其长度。以下是关于Go数组的详细介绍、注意事项和代码示例:

数组的声明:

在Go语言中,数组的声明通常如下所示:

var myArray [5]int // 声明一个包含5个整数的数组

这将创建一个包含5个整数的数组,数组的每个元素都被初始化为其类型的零值,对于整数是0。

数组的初始化:

你可以使用以下方式对数组进行初始化:

myArray := [5]int{1, 2, 3, 4, 5} // 使用指定的元素初始化数组

或者,如果你想让编译器自动计算数组的长度,你可以这样做:

myArray := [...]int{1, 2, 3, 4, 5} // 自动计算数组长度

数组的访问:

你可以通过索引来访问数组的元素,索引从0开始,最大索引为长度减一:

value := myArray[2] // 访问数组的第3个元素(索引为2)

数组的长度:

你可以使用len函数获取数组的长度:

length := len(myArray) // 获取数组的长度

注意事项:

  1. 固定长度: Go数组的长度是固定的,一旦声明后不能更改。如果需要动态大小的数据结构,应该使用切片(Slice)。

  2. 内存开销: 数组的长度在编译时确定,因此可能会占用比实际使用的更多内存。如果不确定数组的长度,应该考虑使用切片。

  3. 遍历数组: 通常使用for循环来遍历数组,或者使用range关键字来迭代数组的元素。

示例代码:

以下是一个示例,演示了如何声明、初始化和遍历数组:

package main

import "fmt"

func main() {
    // 声明并初始化数组
    myArray := [5]int{1, 2, 3, 4, 5}

    // 访问数组元素
    fmt.Println("Element at index 2:", myArray[2])

    // 遍历数组元素
    for i := 0; i < len(myArray); i++ {
        fmt.Println(myArray[i])
    }

    // 使用range遍历数组元素
    for index, value := range myArray {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }
}

这个示例声明了一个包含5个整数的数组,访问了数组元素,然后使用for循环和range遍历了数组元素。请注意,Go数组的长度在声明时确定,因此不需要使用额外的内存管理操作。

5.5.Go语言数组[…]

在Go语言中,[...](省略号)用于数组的声明和初始化,它表示数组的长度由编译器根据提供的初始值推断而来,而不需要显式指定长度。这种方式允许你创建具有动态长度的数组,编译器会根据提供的元素数量自动计算数组的长度。

以下是一个示例:

myArray := [...]int{1, 2, 3, 4, 5}

在这个示例中,[...]表示数组的长度将根据提供的初始值自动确定为5,因为有5个整数元素。这样,你不需要显式指定数组的长度,编译器会自动计算它。

这种省略号表示法非常方便,特别是在你知道数组长度并且不想显式指定它时,它可以让代码更简洁。但请注意,使用省略号的数组是固定长度的,无法动态改变其长度。如果需要动态大小的数据结构,应该使用切片(slice)。

package main

import "fmt"

func main() {
    // 声明并初始化数组
    myArray := [5]int{1, 2, 3, 4, 5}

    // 访问数组元素
    fmt.Println("Element at index 2:", myArray[2])

    // 遍历数组元素
    for i := 0; i < len(myArray); i++ {
        fmt.Println(myArray[i])
    }

    // 使用range遍历数组元素
    for index, value := range myArray {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }
}

5.6.Go语言切片(动态数组)

切片(Slices)是Go语言中非常重要的数据结构,它们允许你在数组、字符串和其他序列类型的基础上创建动态长度的数据结构。以下是有关Go切片的详细介绍、示例使用和注意事项:

1. 切片基础:

  • 切片是对底层数组的引用,可以看作是一个指向数组的窗口。
  • 切片的类型表示了它所引用的元素类型,如[]int表示整数切片。
  • 切片的长度表示它当前包含的元素个数,切片的容量表示它可以容纳的最大元素个数。

2. 创建切片:

  • 使用切片字面量创建切片:mySlice := []int{1, 2, 3, 4, 5}
  • 使用make函数创建切片:mySlice := make([]int, 3, 5),这将创建一个长度为3、容量为5的整数切片。

3. 切片操作:

  • 切片可以通过索引访问和修改元素,例如mySlice[0] = 42
  • 使用append函数向切片追加元素:mySlice = append(mySlice, 6)
  • 使用切片运算符mySubSlice := mySlice[1:4]来创建一个子切片。

4. 注意事项:

  • 切片是引用类型,如果多个切片引用同一个底层数组,修改其中一个切片会影响到其他切片。
  • 注意处理切片的边界,避免越界访问。
  • 当使用append函数追加元素时,要注意切片的容量,如果超过容量,底层数组会重新分配内存,可能导致性能问题。

其他注意点:
切片相当于python的list:列表,go疯狂在动态语言和静态语言中摇摆,注意不能放长度。

1、在 Go 语言的 append 函数签名中,Type 并不是一个具体的类型,而是一个占位符,表示你可以使用任何类型来替代它。这是为了说明 append 函数可以接收任意类型的切片(slice)以及任意数量的同类型元素作为参数。

2、切片左闭右开的[}类似于python、创建预分配的切片,后续可以自动扩容,make创建的时候性能比较高。

3、不能往没有定义长度的切片通过var a[]string ,然后a[0] =“hello”,会报错,因为切片底层还是数组,找不到对应的空间,要使用append,或者make。

4、切片中添加切片使用…将切片打散,切片展开或切片解包切片后面跟…在append函数中如果想要往一个切片中添加另外一个切片,可以将另外一个切片解包使用…。

5、go语言将变量赋值的话他们的地址没有发生改变,指向都是同一个地址,所以不会有性能消耗,如果想要拷贝使用copy函数。//注意 拷贝的对象初始一定有长度,可以用make关键字,不然拷贝是[]空的。

6、切片初始化,3种方式,1、从数组创建,2、使用string{},3、make

切片底层原理:是值传递这里有个切片踩坑的点!!!

在Go语言中,切片(slice)的传递方式是值传递(value semantics),但这里有一个重要的点需要理解:虽然切片本身是值传递的,但切片内部包含了对底层数组的引用(即指针),因此通过切片可以共享和修改底层数组中的数据。

当我们把一个切片赋值给另一个变量或传递给一个函数时,我们实际上是在复制这个切片的结构体。这个结构体包含了指向底层数组的指针、切片的长度(length)和容量(capacity)。因此,尽管是值传递,但两个切片都指向同一个底层数组,所以对其中一个切片所做的修改(只要索引在长度范围内)也会反映到另一个切片上。

以下是一个简单的例子来说明这一点:

go
package main  
  
import "fmt"  
  
func modifySlice(s []int) {  
    s[0] = 99 // 修改切片的第一个元素  
    fmt.Println("Inside function:", s) // 输出修改后的切片  
}  
  
func main() {  
    a := []int{1, 2, 3, 4, 5}  
    fmt.Println("Before function call:", a) // 输出原始切片  
    modifySlice(a) // 传递切片给函数  
    fmt.Println("After function call:", a) // 输出被函数修改后的切片  
}

在这个例子中,尽管modifySlice函数接收的是一个切片的值传递,但当我们修改切片中的元素时,这个修改也会影响到原始切片a,因为两个切片都引用同一个底层数组。

总结来说,Go语言的切片是值传递的,但它们内部包含了对底层数组的引用,因此可以通过切片来共享和修改底层数组中的数据。(除非扩容导致内存的重新分配!!!!!会指向不同的地址)

package main

import (
	"fmt"
	"strconv"
	"unsafe"
)

func updateSlice(a []string) {
	a[0] = "hello" //对原来数据发生改变

	//对原来数据没有发生改变
	for i := 0; i < 10; i++ {
		a = append(a, strconv.Itoa(i))
	}
}

//slice底层是结构体

type slice struct {
	array unsafe.Pointer //用来存放世纪数据的数组指针,指向一块连续的内存
	len   int            //切片中元素的数量
	cap   int            //array数组的长度
}

func main() {

	//	go的slice在函数参数传递的时候,是值传递还是引用传递,效果又呈现出引用传递的效果,但不完全是,其实是值传递

	//ms := slice{
	//	len: 3,
	//	cap: 3,
	//}

	//a := []string{"hi", "h2"} //底层是结构体赋值,这是语法糖写法
	//updateSlice(a)
	//fmt.Println(a)
	//	在底层中,在函数参数传递时候,会拷贝一份slice,但是注意,他们的头指针指向同一个数组,导致引用指向同一个数据结构,这是注意的点
	//扩容会重新分配,指向不同的数据结构,再改变数据,就不会对原来数据结构改变,这也就是为什么append接收一个返回的参数值,append超级有可能引发扩容,地址会发生改变

	//	下面举一个简单的例子

	data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := data[1:6]
	s2 := data[2:7]
	fmt.Println(len(s1), cap(s1))
	fmt.Println(len(s2), cap(s2))
	//没有扩容时候指向同一个数据结构,改变的时候一起改变

	//当发生append导致扩容时候,会重新分配内存,指向不同的数据结构
	s2 = append(s2, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3)
	s2[0] = 12211

	fmt.Println(s1)
	fmt.Println(s2)

}

在这里插入图片描述
切片的扩容机制:
Go语言的切片(slice)扩容机制是基于动态数组的,当切片的容量不足以容纳新元素时,会触发扩容操作。以下是Go切片扩容机制的主要特点和过程:

扩容触发条件:
当切片的长度(length)超过当前容量(capacity)时。
另一种情况下,即使长度没有超过容量,但如果容量小于1024,每次添加新元素时也可能触发扩容,此时容量会翻倍。

扩容过程:
Go语言会创建一个新的底层数组,其容量大于或等于当前切片所需的新容量。
将原始切片中的数据(即底层数组中的部分数据)复制到新数组中。
切片的引用(即切片的指针)会更新为指向新的底层数组。
切片的长度更新为原始长度加上新添加的元素数量(如果是由于添加新元素而触发的扩容)。
切片的容量更新为新数组的容量。

扩容策略:
当切片的原始容量小于1024时,新容量通常是原始容量的两倍。
当原始容量大于等于1024时,扩容策略会有所不同。此时新容量会按原始容量的25%递增,直到新容量大于或等于所需的新容量。
性能影响:
切片扩容操作涉及数组的重新分配和数据的迁移,因此会带来一定的性能开销。其时间复杂度为O(n),其中n为切片的长度。
扩容操作还可能导致切片的内存地址发生变化,因为切片的引用会指向新的底层数组。

优化:
如果知道切片需要存储的元素数量,可以使用make函数创建切片时显式指定容量,以减少扩容的次数。例如,make([]T, length, capacity)可以创建一个长度为length、容量为capacity的切片。
并发安全性:
切片本身并不是并发安全的。在并发环境中对切片进行修改时,需要使用适当的同步机制(如互斥锁)来确保数据的一致性。
请注意,以上描述是基于Go语言的一般实现和常见行为。不同版本的Go语言可能会有细微的差异或优化。
5. 示例使用:

package main

import "fmt"

func main() {
    // 创建一个整数切片
    mySlice := []int{1, 2, 3, 4, 5}

    // 修改切片中的元素
    mySlice[0] = 42

    // 追加元素到切片
    mySlice = append(mySlice, 6)

    // 创建一个子切片
    mySubSlice := mySlice[1:4]

    // 输出切片内容
    fmt.Println(mySlice)    // [42 2 3 4 5 6]
    fmt.Println(mySubSlice) // [2 3 4]
}

在Go语言中,使用make函数来创建切片、映射(map)和通道(channel)。下面是关于如何使用make函数来创建切片的方式:

// 创建一个整数切片,长度为5,容量为10
mySlice := make([]int, 5, 10)

上述代码中的make([]int, 5, 10)表示创建了一个整数切片,初始长度为5,容量为10。这意味着切片中有5个元素,但它可以扩展到最多容纳10个元素。容量是指底层数组的大小,它决定了切片可以增长的最大尺寸,当切片的长度达到容量时,再追加元素会触发底层数组的重新分配和扩展。

通过make函数创建切片时,会分配内存并返回一个指向切片的引用,这个引用可以用于后续的切片操作。

需要注意的是,使用make函数创建切片时,切片中的元素会被初始化为其类型的零值(例如,整数切片中的元素会初始化为0)。如果希望在创建时初始化切片的元素,可以使用切片字面量的方式,如mySlice := []int{1, 2, 3}

5.7.Go语言Map

在Go语言中,你可以使用map来创建关联数组,也就是键值对的集合。下面是如何定义和使用map的基本方式:两种方式,都要初始化
//map 不是线程安全的,如果使用协程对一个map操作是要报错的!!!!!!!!!如果要使用的话使用sync.Map
1. 定义map
你可以使用make函数来创建一个空的map,并指定键和值的类型,或者使用map字面量直接初始化一个map
map必须要初始化,不然不能赋值!!!!!!!但是slice可以不初始如下

	var m []string
	if m == nil {
		fmt.Println("ok")
	}

	m = append(m, "a")

map的初始化如下:

package main

import "fmt"

func main() {
	// map是一个key,value的键值对(无序的集合),主要是查询方便。切片和数组查询值的时候不得不去遍历性能比较低,而map直接可以拿到value通过key

	//初始化方式一:

	var courseMap = map[string]string{

		"go":     "hello",
		"java":   "hi",
		"python": "hi",
	}

	fmt.Println(courseMap)
	//取值
	fmt.Println(courseMap["go"])
	//放值
	courseMap["mysql"] = "原理"
	fmt.Println(courseMap)

	//	注意map的坑点:如果定义map结构体没有初始化的时候,map类型要设置值的时候,必须要初始化,一定要初始化加上打括号!!!!!!!!!
	//panic: assignment to entry in nil map
	//var courseMap1 map[string]string
	//courseMap1["math"] = "2"
	//fmt.Println(courseMap1)

	var courseMap1 = map[string]string{}
	courseMap1["math"] = "2"
	fmt.Println(courseMap1)

	//	方式二:make内置函数,必须初始化才能使用
	var courseMap2 = make(map[string]string, 3)
	courseMap2["mysql"] = "mysql原理"
	fmt.Println(courseMap2)

	var m []string
	if m == nil {
		fmt.Println("ok")
	}

	m = append(m, "a")

}

map的遍历如下:
支持返回key和value或者返回一个key
在Go语言中,map 的键(key)可以是任何可以用作等于比较操作(== 或 !=)的类型。这包括:

整数类型:如 int、int8、int16、int32、int64、uint、uint8(别名 byte)、uint16、uint32、uint64、uintptr。

浮点数类型:虽然 float32 和 float64 可以进行等于比较,但通常不建议将它们用作 map 的键,因为浮点数的精度问题可能导致意外的结果。

字符串类型:string 是 map 键的常用类型。

布尔类型:bool 也可以用作 map 的键,但通常不常见,因为 bool 只有两个值(true 和 false)。

复合类型:如数组、切片、映射或函数不能作为 map 的键,因为这些类型是不可比较的(除了包含可比较类型的数组)。但是,如果数组或结构体只包含可比较的字段,那么这些数组或结构体是可以作为 map 的键的。

接口类型:如果接口的动态类型是可比较的,那么该接口值可以用作 map 的键。空接口 interface{} 可以包含任何类型的值,但如果用作 map 的键,则必须确保存储在其中的值是可比较的。

指针类型:指向可比较类型的指针可以用作 map 的键。

结构体类型:如果结构体的所有字段都是可比较的,则该结构体可以用作 map 的键。

其他自定义类型:只要这些类型满足可比较性的要求(即支持 == 和 != 操作符),就可以用作 map 的键。

需要注意的是,虽然浮点数类型可以用作 map 的键,但由于浮点数的精度问题,通常不推荐这样做。此外,对于复杂的自定义类型(如包含切片或映射的类型),通常也不能作为 map 的键,因为这些类型通常不是可比较的。

总之,当你选择 map 的键类型时,应该选择那些具有明确、可预测和一致的比较结果的类型。

package main

import "fmt"

func main() {

	var courseMap = map[string]string{

		"go":     "hello",
		"java":   "hi",
		"python": "hi",
	}

	fmt.Println(courseMap)

	//for key, value := range courseMap {
	//	fmt.Println(key, ":", value)
	//}
	for value2 := range courseMap {
		fmt.Println(value2)
	}
	//	map是无序的,不能保证每次打印都是有序的顺序

	//第一种 获取map中的元素,如果想要知道在不在里面,如果没有返回的是一个空的值""
	fmt.Println(courseMap["scala"])
	//第二种
	//value, ok := courseMap["scala"]

	//if !ok {
	//	fmt.Println("没有这个key")
	//} else {
	//	fmt.Println(value)
	//}

	//	可以缩写上面代码
	if value, ok := courseMap["scala"]; !ok {
		fmt.Println("not in")
	} else {
		fmt.Println(value)
	}

	//	删除一个元素,内置函数
	delete(courseMap, "python")
	fmt.Println(courseMap)
	//map 不是线程安全的,如果使用协程对一个map操作是要报错的!!!!!!!!!如果要使用的话使用sync.map
}

// 使用make函数创建一个空的map,键是字符串,值是整数
myMap := make(map[string]int)

// 使用map字面量初始化一个map
anotherMap := map[string]string{
    "key1": "value1",
    "key2": "value2",
}

2. 添加和访问元素:
你可以使用map的键来添加和访问元素。

myMap["apple"] = 5 // 添加键值对
count := myMap["apple"] // 访问键值对的值

3. 检查键是否存在:
你可以使用一个额外的变量来检查map中是否存在某个键。

value, exists := myMap["banana"]
if exists {
    fmt.Println("The value is:", value)
} else {
    fmt.Println("Key not found")
}

4. 删除元素:
使用delete函数来从map中删除指定的键值对。

delete(myMap, "apple")

5. 迭代map
使用for range循环来迭代map的所有键值对。

for key, value := range myMap {
    fmt.Println("Key:", key, "Value:", value)
}

6. 注意事项:

  • map是无序的,键值对的顺序不会固定。
  • 使用未初始化的map会引发panic,所以确保在使用之前使用make进行初始化。
  • 如果尝试访问map中不存在的键,会返回零值,你可以使用第二个返回值来检查键是否存在。

map是Go中非常常用的数据结构,用于快速查找和存储键值对数据。你可以根据具体的需求来选择合适的键和值类型。

函数是Go语言中的基本构建块之一,它用于执行特定任务或操作。以下是关于Go函数的介绍、使用方法和一些注意事项:

5.8.Go语List(链表)

在Go语言中,标准库并没有直接提供一个名为List的链表数据结构,但你可以通过自定义结构体和方法来实现链表的功能。不过,Go的container/list包提供了一个双向链表(doubly linked list)的实现,它被称为List。数据插入很方便,但是找值麻烦,要从头指针不停遍历查找。

container/list包中的List类型是一个双向链表,其中的元素被称为Element。每个Element都包含两个指向前一个和后一个元素的指针,以及一个存储元素值的字段。

以下是一些关于container/list包中List的基本用法和特性:

创建链表:
你可以通过list.New()函数来创建一个新的链表。

go
package main  
  
import (  
	"container/list"  
	"fmt"  
)  
  
func main() {  
	l := list.New()  
	// 现在l是一个空的链表  
}

添加元素:
你可以使用PushFront、PushBack、InsertBefore或InsertAfter方法在链表的指定位置添加元素。

go
e1 := l.PushFront("a") // 在链表头部添加元素"a"  
e2 := l.PushBack("b")  // 在链表尾部添加元素"b"

访问元素:
链表中的元素通过Element类型进行访问。你可以通过链表的Front和Back方法获取头尾元素,或者通过遍历链表来访问所有元素。

go
for e := l.Front(); e != nil; e = e.Next() {  
	fmt.Println(e.Value) // 打印链表中的每个元素的值  
}

修改元素:
你可以直接修改Element的Value字段来更改元素的值。注意,Value字段的类型是interface{},因此你可能需要进行类型断言或类型转换。

go
if e := l.Front(); e != nil {  
	e.Value = "new value" // 修改头元素的值  
}

删除元素:
你可以使用Remove方法删除一个元素。

go
e := l.Front() // 获取头元素  
if e != nil {  
	l.Remove(e) // 删除头元素  
}

移动元素:
你可以使用MoveToFront、MoveToBack或调整元素的Prev和Next指针来移动元素在链表中的位置。

链表长度:
你可以使用Len方法来获取链表的长度(即元素数量)。

go
length := l.Len() // 获取链表长度

请注意,虽然container/list包提供了链表的实现,但在很多情况下,切片(slice)在Go中是一个更加灵活和高效的选择,尤其是当你需要动态数组或堆栈(stack)这样的数据结构时。然而,链表在处理大量插入和删除操作(尤其是在列表中间)时可能会更加高效,因为它们的操作时间复杂度通常是O(1),而切片在这种情况下的时间复杂度可能是O(n)。

总结:
go中集合了类型:1、数组:不同长度的数组类型不一样、2、切片->动态数组,用起来方便,性能高3、map、4、list用的少

六.函数

在Go中,函数的定义以关键字 func 开始,后面跟着函数名、参数列表和返回值类型。
go函数支持普通函数,匿名函数和闭包
go中函数是一等公民,1、函数本身可以当作变量,2、匿名函数,闭包,3、函数可以满足接口
函数参数传递的时候,值传递,go语言中全部都是值传递

func functionName(parameter1 type1, parameter2 type2) returnType {
    // 函数体
    // 可以包含一系列操作和逻辑
    return result
}
  • functionName:函数名,用于标识函数。
  • parameter1, parameter2:参数列表,指定函数的输入参数和它们的类型。
  • returnType:返回值类型,指定函数的返回类型。
  • result:函数执行后的返回值,使用 return 语句返回。

2. 函数的使用:
以下是函数的使用示例:

package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func main() {
    result := add(5, 3)
    fmt.Println("Result:", result) // 输出:Result: 8
}

在上面的示例中,add 函数接受两个整数参数并返回它们的和。在 main 函数中调用 add 函数并输出结果。

3. 注意事项:

  • 函数名和参数的命名应该具有描述性,以提高代码的可读性。
  • Go支持多返回值的函数。你可以返回多个值,并在调用函数时接收它们。
  • 函数可以作为参数传递给其他函数,也可以用作匿名函数。
  • 变量的作用域是词法作用域(Lexical Scope),函数内部定义的变量只在函数内可见。
  • 函数可以是递归的,允许函数调用自身。
  • 可变参数函数使用 ... 语法,允许传递不定数量的参数。

以下是一个可变参数函数的示例:

func sum(numbers ...int) int {
    result := 0
    for _, num := range numbers {
        result += num
    }
    return result
}

func main() {
    total := sum(1, 2, 3, 4, 5)
    fmt.Println("Total:", total) // 输出:Total: 15
}

总之,函数是Go语言中的重要构建块,用于封装和组织代码逻辑。良好的函数设计可以提高代码的可维护性和可读性。不同的函数可以执行各种任务,包括数据处理、计算、I/O 操作等。函数是Go语言编程的核心要素之一,因此深入理解它们是编写高效、可维护代码的关键。

6.1.函数和方法区别

方法(Method)和函数(Function)有一些重要的区别,主要集中在它们的定义、调用和用途上。以下是方法和函数在Go中的主要区别:

1. 定义方式:

  • 函数(Function):函数是一段独立的代码块,可以在任何地方定义,包括包级别和函数内部。函数的定义以 func 关键字开始,通常不与任何特定的类型关联。
func functionName(parameters) returnType {
    // 函数体
}
  • 方法(Method):方法是与特定类型关联的函数。它们被定义为类型的一部分,通过为类型创建方法来扩展其行为。方法的定义包括接收者(Receiver),即与之关联的类型。
func (receiverType) methodName(parameters) returnType {
    // 方法体
}

2. 调用方式:

  • 函数(Function):函数通过函数名直接调用,传递参数并接收返回值。
result := functionName(arguments)
  • 方法(Method):方法通过类型的实例调用,使用点符号(.)来访问。实例称为接收者,它会作为方法的第一个参数传递。
result := receiverInstance.methodName(arguments)

3. 使用场景:

  • 函数(Function):函数通常用于执行通用任务,与特定类型无关。它们可以在任何地方使用,不受类型约束。
func calculateSum(numbers []int) int {
    // 计算总和
}
  • 方法(Method):方法用于扩展类型的行为,它们是类型的一部分,并可以访问类型的字段和属性。方法使得类型可以具有自己的行为和操作。
type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

总之,方法和函数在Go中有不同的定义方式、调用方式和使用场景。函数是通用的独立代码块,而方法是与特定类型关联的行为扩展。根据需求,你可以选择使用函数来执行通用任务,或者使用方法来扩展类型的行为。

6.2.函数值传递和地址传递

在Go语言中,函数参数的传递方式分为值传递和地址传递两种方式。这些方式决定了函数内部对参数的操作是否会影响原始变量的值。

1. 值传递(Pass by Value):

在值传递中,函数接收的是原始数据的一份拷贝。这意味着函数内对参数的任何修改都不会影响原始数据的值。在Go中,基本数据类型(例如整数、浮点数、布尔值等)和数组都是以值传递的方式传递给函数的。

示例:

func modifyValue(x int) {
    x = 10
}

func main() {
    value := 5
    modifyValue(value)
    fmt.Println(value) // 输出:5,因为 modifyValue 函数接收到的是 value 变量的拷贝
}

2. 地址传递(Pass by Address):

在地址传递中,函数接收的是原始数据的地址(指针)。这意味着函数内对参数的修改会影响原始数据的值。在Go中,切片、映射和指针等复杂数据类型以及数组切片(Slices)都是以地址传递的方式传递给函数的。

示例:

func modifySlice(slice []int) {
    slice[0] = 10
}

func main() {
    mySlice := []int{5, 6, 7}
    modifySlice(mySlice)
    fmt.Println(mySlice) // 输出:[10 6 7],因为 modifySlice 函数接收到的是 mySlice 变量的地址
}

需要注意的是,虽然切片、映射和指针以地址传递方式传递给函数,但是如果在函数内部重新分配了一个新的切片、映射或指针,原始数据的地址将不再指向同一块内存,因此对新数据的修改不会影响原始数据。

总之,Go语言中的函数参数传递方式分为值传递和地址传递。值传递是将参数的副本传递给函数,不会影响原始数据的值,而地址传递是将原始数据的地址传递给函数,可以在函数内部修改原始数据的值。根据参数类型和需求,你可以选择使用适当的传递方式

6.3.高阶函数

在 Go 语言中,高阶函数是指可以接受一个或多个函数作为参数,并/或返回一个函数作为结果的函数。高阶函数是函数式编程的一个关键概念,它允许你以更灵活的方式操作函数,将函数作为一等公民(First-Class Citizen)对待。以下是高阶函数的介绍和使用示例:

1. 接受函数作为参数:

Go语言中,你可以将函数作为参数传递给其他函数,以实现不同的操作。这样的函数通常用于实现通用的功能,例如排序、过滤、映射等。

func apply(f func(int) int, arr []int) []int {
    result := []int{}
    for _, v := range arr {
        result = append(result, f(v))
    }
    return result
}

func square(x int) int {
    return x * x
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    squared := apply(square, numbers)
    fmt.Println(squared) // 输出:[1 4 9 16 25]
}

2. 返回函数:

Go语言中,函数也可以作为结果返回,这样的函数被称为闭包(Closure)。闭包可以捕获其所在作用域的变量,并在之后的调用中使用这些变量。

func multiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    double := multiplier(2)
    triple := multiplier(3)

    fmt.Println(double(5)) // 输出:10
    fmt.Println(triple(5)) // 输出:15
}

3. 匿名函数:

Go语言支持匿名函数,这是一种没有名称的函数。你可以在需要时声明和使用匿名函数,通常用于一次性操作。

func main() {
    add := func(a, b int) int {
        return a + b
    }

    result := add(3, 5)
    fmt.Println(result) // 输出:8
}

如果你想要在 Go 中实现函数自己调用自己,你可以使用匿名函数和递归来实现这个目标。这种方式被称为“匿名递归函数”。以下是一个示例:

package main

import "fmt"

func main() {
    // 调用匿名递归函数
    result := func(n int) int {
        if n <= 0 {
            return 1
        }
        return n * (n - 1)
    }(5) // 传递参数 5 给匿名函数

    fmt.Println("Result:", result)
}

在上面的示例中,我们定义了一个匿名函数,并在定义后立即调用它。这个匿名函数接受一个整数参数 n,并返回 n * (n - 1) 的结果。我们通过 (5) 将参数 5 传递给匿名函数,因此实际上是在调用匿名函数并将参数传递给它。这种方式可以实现函数自己调用自己的效果。

需要注意的是,这种方法是通过立即调用匿名函数来实现递归,而不是在函数内部直接调用自己。这样的方式适用于一次性的递归操作,但对于更复杂的递归场景,通常建议使用传统的命名函数递归方法。

6.4.函数嵌套(闭包)

是的,Go 中的函数可以嵌套,也就是一个函数内部可以包含另一个函数。嵌套函数可以访问其外部函数(称为父函数)的变量和参数,这种特性称为闭包(Closure)。闭包使得内部函数可以捕获并操作其所在作用域中的变量。

package main

func autoincrement() func() int {

	local := 0
	return func() int {
		local += 1
		return local
	}
}
//闭包,函数内可以调用这个local参数,用完会自动清空
func main() {

	f := autoincrement()

	for i := 0; i < 5; i++ {
		println(f())
	}
}

以下是一个示例:

package main

import "fmt"

func outerFunction() func(int) int {
    x := 10

    innerFunction := func(y int) int {
        return x + y
    }

    return innerFunction
}

func main() {
    inner := outerFunction()
    result := inner(5)
    fmt.Println("Result:", result) // 输出:Result: 15
}

在上面的示例中,outerFunction 是一个父函数,它定义了一个局部变量 x,然后返回了一个内部函数 innerFunction。内部函数 innerFunction 捕获了外部函数的变量 x,并且可以在调用时访问并操作它。

这种函数嵌套和闭包的特性使得在Go中可以创建强大而灵活的函数组合,有助于编写更模块化、可重用的代码。但需要注意在嵌套函数中使用外部变量时,要小心变量的生命周期,以避免意外的行为。
在 Go 中,函数嵌套的方式不仅限于匿名函数,你也可以在函数内部定义具名函数。具名函数是拥有自己的函数名的函数,它们在函数内部定义,但可以在函数内部或外部调用。

以下是一个示例,展示了在函数内部嵌套具名函数的用法:

package main

import "fmt"

func outerFunction() func(int) int {
    x := 10

    // 在函数内部嵌套具名函数
    var innerFunction func(int) int
    innerFunction = func(y int) int {
        return x + y
    }

    return innerFunction
}

func main() {
    inner := outerFunction()
    result := inner(5)
    fmt.Println("Result:", result) // 输出:Result: 15
}

在上面的示例中,outerFunction 内部定义了一个具名函数 innerFunction,它也可以捕获并操作外部函数的变量 x。这个具名函数在外部函数内部使用,并作为 outerFunction 的返回值。

所以,Go 中的函数嵌套既可以是匿名函数,也可以是具名函数,具体取决于你的需求和代码组织方式。匿名函数通常在需要时使用更简洁,而具名函数可以提供更多的可读性和复用性。
闭包(Closure)是指在一个函数内部定义的函数,它可以访问其外部函数的变量。以下是一个简单的 Go 语言闭包示例:

package main

import "fmt"

func outer() func() int {
    x := 10

    // 内部函数是一个闭包,可以访问外部函数的变量 x
    inner := func() int {
        x++
        return x
    }

    return inner
}

func main() {
    // 调用 outer 函数,获取闭包函数 inner
    myClosure := outer()

    // 调用闭包函数多次,它可以访问并修改 x 的值
    fmt.Println(myClosure()) // 输出:11
    fmt.Println(myClosure()) // 输出:12
}

在上面的示例中,outer 函数返回一个闭包函数 inner,该闭包函数可以访问 outer 函数中定义的变量 x。当我们多次调用闭包函数时,它会持续地修改 x 的值并返回新的值,因为闭包函数记住了 x 的状态。

这种特性使得闭包在一些情况下非常有用,例如在函数式编程中用于创建可重用的函数、实现私有变量等。闭包可以捕获其外部作用域的状态,因此它们在 Go 编程中具有广泛的应用。

递归(Recursion)是一种编程技术,指的是函数调用自身的过程。在 Go 语言中,递归可以用来解决一些问题,特别是那些可以分解为较小相似子问题的问题。下面是递归的介绍和一个使用案例。

6.5.递归

递归的特点:

  • 递归函数通常包含两部分:基本情况(Base Case)和递归情况(Recursive Case)。
  • 基本情况是递归的终止条件,当满足这个条件时,递归停止。
  • 递归情况是函数调用自身的部分,通常解决一个较小的子问题。

递归的使用案例:

以下是一个计算阶乘的递归函数的示例:

package main

import "fmt"

func factorial(n int) int {
    // 基本情况:当 n 为 0 或 1 时,阶乘为 1
    if n <= 1 {
        return 1
    }
    // 递归情况:阶乘为 n * (n-1) 的阶乘
    return n * factorial(n-1)
}

func main() {
    result := factorial(5)
    fmt.Println("Factorial of 5 is:", result) // 输出:Factorial of 5 is: 120
}

在上面的示例中,factorial 函数使用递归来计算给定整数 n 的阶乘。基本情况是 n <= 1 时,阶乘为 1,递归情况是 n * factorial(n-1),这会将问题分解为更小的子问题,直到达到基本情况。

需要注意的是,递归可能导致栈溢出,因此在实际应用中需要注意递归深度。对于更复杂的递归问题,还需要谨慎处理性能和内存开销。

6.6.defer关键字

在 Go 语言中,defer 是一个非常有用的关键字,用于在函数执行完毕之前执行一些清理或收尾工作。defer 常常用于确保资源的释放、文件的关闭、锁的解锁等操作,以便代码更加可维护和安全。

//	如果有多个defer,执行顺序是栈的概念,先进后出,所以先后进的defer先出

defer 的语法格式为:

defer 函数调用

其中,函数调用可以是任何合法的函数或方法调用。当函数执行到 defer 语句时,不会立即执行该函数调用,而是将它推迟到包含 defer 语句的函数即将返回时执行。

他是一个栈stack,后定义的先执行,并且的return返回之前开始执行!!
以下是 defer 的一些常见用法示例:

  1. 文件关闭:确保打开的文件在函数执行完毕后被关闭。
file, err := os.Open("example.txt")
if err != nil {
    // 错误处理
    return
}
defer file.Close() // 确保文件在函数返回前关闭
  1. 解锁操作:确保互斥锁在函数返回前解锁。
mu.Lock()
defer mu.Unlock() // 确保锁在函数返回前解锁
  1. 资源释放:确保在函数结束时释放资源,如数据库连接。
db := openDatabaseConnection()
defer db.Close() // 确保数据库连接在函数返回前关闭
  1. 函数调用统计:用于函数调用计时或统计。
func trackTime(start time.Time) {
    fmt.Printf("Function took %v\n", time.Since(start))
}
func myFunction() {
    defer trackTime(time.Now())
    // 函数的主要逻辑
}

需要注意的是,defer 语句是按照后进先出(LIFO)的顺序执行的,这意味着最后一个 defer 语句会在函数即将返回时最先执行,以此类推。

defer 在编写代码时可以提高代码的可读性和维护性,尤其是涉及到资源管理和清理的情况。但要小心避免在循环中过度使用 defer,因为它们只会在函数返回时执行,可能导致资源释放不及时。

6.7.recover异常处理

在Go语言中,recover是一个内建函数,主要用于从panic中恢复并继续执行程序。当程序发生不可恢复的错误时,会引发一个panic,而recover函数则用于捕获这个panic,并进行一些处理,以防止程序崩溃。

recover 函数只能在 defer 语句的函数内部被调用。当程序发生 panic 时,Go 运行时系统会沿着调用栈向上查找是否有 defer 语句的函数,并检查这些函数是否调用了 recover。如果找到了,那么 panic 会停止向上传播,并且会执行该 defer 语句中 recover 调用之后的代码。

recover 的设计目的就是为了在发生 panic 时提供一种恢复机制,使得程序可以继续执行,而不是直接崩溃。但是,它只能在 defer 语句中使用,这是 Go 语言对 recover 的使用所施加的限制。

1、defer需要放在panic函数之前定义,不然别的代码报错了defer那边就是不可达的,另外recover,只有在defer调用的函数中才会生效;
2、recove异常处理之后,逻辑并不恢复到panic的那个点去,因为是defer,函数就执行完了;
3、多个defer会形成栈,后定义的defer会先执行;

具体来说,当panic发生时,Go语言运行时会查找调用栈中的defer函数,并检查是否存在recover函数。如果存在,程序将停止继续向上传播panic,并开始执行recover函数。此时,recover函数会停止当前的panic过程,并返回panic的值。这样可以防止程序因为错误而崩溃,并且可以在错误发生后继续执行其他操作。

需要注意的是,recover函数只能在defer语句中使用,并且只有在发生panic时才会起作用。如果没有发生panic或者在非defer语句中调用recover函数,它将返回nil。

以下是recover函数的一些应用场景:

避免程序崩溃:当程序发生panic时,如果没有进行处理,程序会崩溃。使用recover函数可以捕获panic并恢复程序的控制流,避免程序崩溃。
记录日志:当程序发生panic时,可以使用recover函数捕获panic,并记录日志,以便后续排查问题。
下面是一个使用recover函数的示例代码:

go
package main  
  
import "fmt"  
  
func main() {  
    defer func() {  
        if r := recover(); r != nil {  
            fmt.Println("Recovered:", r)  
        }  
    }()  
    panic("Something went wrong")  
}

在上面的例子中,当程序发生panic时,defer语句中的匿名函数会被调用,并且调用recover函数捕获panic。程序会从panic的位置继续执行,而不是终止执行。同时,recover函数捕获的panic值会被打印出来。

package main

import (
	"errors"
	"fmt"
)

func A() (int, error) {

	//panic("this is a panic")
	fmt.Println("错误")
	return 0, errors.New("this is a error")
}

func main() {
	//	开发函数的人需要又一个返回值去告诉是否调用成功,go设计者我们必须要处理这个err,代码中大量会出现if err != nil ,go设计被认为必须要处理这个error,防御编程。健壮性很好
	//
	//_, err := A()
	//if err != nil {
	//	fmt.Println(err)
	//}

	//	panic:会导致程序退出,程序就会挂掉,在go语言中不推荐随便使用panic,开发中很少会用到,一般我们服务启动的过程中,比如我的服务想要必须有些依赖服务,比如日志,mysql文件没有
	//	问题,如果服务启动过程中发现任何一个不满足,就主动调用panic,但是你的程序一旦启动了,这个时候你的某行代码中,写了panic,那整个程序都会挂了,就会造成重大事故
	//	但是架不住,但是有些地方代码写的不小心,被动触发panic

	//比如以下代码,map没有初始化,被动造成panic,这个时候recover函数出现了,去捕获panic
	defer func() {
		//捕获异常信息
		if re := recover(); re != nil {
			//这边就可以对函数造成的异常捕获然后输出打印了,捕获异常时候要写在前面,不然先panic就直接报错了
			fmt.Println("recover捕获了异常:", re)
		}
	}()

	var names map[string]string

	names["go"] = "go开发工程师"
	fmt.Println("hello")

}

6.8 .init函数

初始化顺序:变量初始化->initial()->man(),并且可以有多个init方法
init 函数是 Go 语言中的一种特殊函数,用于在程序运行时自动执行初始化操作。每个 Go 包都可以包含一个或多个 init 函数,它们会在程序执行时按照导入包的顺序自动执行。

init 函数的特点和用法如下:

  1. 特殊函数init 函数是一种特殊的函数,它不需要被显式调用,而是在程序启动时自动执行。

  2. 包级别的初始化:每个包可以包含一个或多个 init 函数,它们在包级别执行初始化操作。init 函数不能被调用或引用,而是由运行时系统自动调用。

  3. 执行顺序:对于一个包,多个 init 函数的执行顺序是按照它们在源代码中的顺序执行的,即按照从上到下的顺序执行。

  4. 包的导入触发初始化:当一个包被导入到其他包中时,其中的 init 函数会被自动执行。这保证了初始化在包被使用之前完成。

  5. 主程序的 main 函数执行前:所有包的 init 函数都会在主程序的 main 函数执行前完成初始化。

以下是一个示例,展示了 init 函数的使用:

package main

import (
    "fmt"
    "math"
)

func init() {
    fmt.Println("This is the init function of the main package.")
}

func main() {
    fmt.Println("The square root of 16 is:", math.Sqrt(16))
}

在上面的示例中,主程序包含一个 init 函数,它在程序启动时自动执行。然后,在 main 函数中,我们计算并输出了 16 的平方根。

需要注意的是,虽然 init 函数是一个有用的初始化机制,但它应该谨慎使用,避免在其中做过多复杂的工作,以保持代码的可读性。通常,init 函数用于初始化全局变量、执行一次性的初始化操作或注册一些特殊的行为。

init 函数通常用于执行以下类型的初始化操作:

  1. 全局变量的初始化:在 init 函数中可以对包级别的全局变量进行初始化,确保它们在程序启动时具有合适的初始值。

  2. 一次性的初始化init 函数适合执行只需要在程序启动时执行一次的初始化操作,例如读取配置文件、建立数据库连接、加载静态资源等。

  3. 注册行为:某些情况下,你可能希望在导入包时自动注册一些行为或功能。这可以通过在 init 函数中执行注册操作来实现,例如在 Web 框架中注册路由处理程序。

  4. 初始化依赖关系:如果你的包依赖于其他包,可以在 init 函数中进行初始化以确保它们在使用之前已准备好。

  5. 执行一些特殊逻辑:在某些情况下,你可能需要执行一些特殊的逻辑或初始化步骤,例如设置全局配置、初始化日志记录器等。

  6. 性能优化init 函数还可以用于执行性能优化,例如预计算某些数据结构,以减少运行时的计算开销。

总的来说,init 函数用于在程序启动时执行一些必要的初始化操作,以确保程序的正常运行。它是一个有用的工具,但应该谨慎使用,避免在其中包含过多复杂的逻辑,以保持代码的可读性和维护性。

七.指针

指针是一种在编程语言中常见的概念,它用于存储变量的内存地址,允许直接访问内存中的数据。在 Go 语言中,指针是一种强大而灵活的工具,用于管理内存、传递数据以及实现高效的数据操作。以下是关于 Go 语言指针的详细介绍:

//初始化两个关键字,map,channel,slice推荐使用make方法
//指针初始化推荐使用new方法,指针要初始化,否则会出现nil pointer
//map必须初始化
  1. 指针的声明:在 Go 中,使用 * 符号来声明指针。例如,var ptr *int 声明了一个名为 ptr 的整数指针。

  2. 取地址操作符 &:在 Go 中,使用 & 符号可以获取一个变量的内存地址。例如,&x 可以获取变量 x 的地址。

  3. 指针的零值:未初始化的指针具有零值 nil,表示它不指向任何有效的内存地址。

  4. 指针的赋值和解引用

    • 使用 = 将一个变量的地址分配给指针,例如 ptr = &x
    • 使用 * 符号对指针进行解引用,可以访问指针所指向的内存中的值,例如 val := *ptr
  5. 指针的应用

    • 传递指针作为函数参数可以在函数内部修改原始变量的值,而不是传递变量的副本。
    • 使用指针可以减少内存消耗,特别是在处理大型数据结构时。
    • 指针常用于实现数据结构,如链表和树。
  6. 指针与数组:Go 中的数组是值类型,但可以使用指针来传递数组,以避免数组的复制。这通常使用切片(slice)更为灵活。

  7. 指针与结构体:结构体可以包含指针字段,用于引用其他数据结构。这允许在结构体之间创建关联,形成更复杂的数据结构。

  8. 指针与函数:指针可以作为函数参数传递,以便在函数内部修改外部变量的值。这用于实现一些高级的功能,如回调函数和函数闭包。

  9. 指针的安全性:Go 语言提供了垃圾回收机制,因此开发人员通常不必担心内存管理。Go 的指针通常更安全,因为它们受到自动内存管理的保护,避免了许多常见的内存错误。

  10. 指针的注意事项

    • 避免使用未初始化的指针,它们默认为 nil,解引用它们会导致运行时错误。
    • 在操作指针时要确保不会导致空指针异常。
    • 注意指针的生命周期,避免在函数中返回指向局部变量的指针。

指针是 Go 语言的一个重要概念,它可以用于各种情况,从简单的变量引用到复杂的数据结构操作。正确使用指针可以提高代码的性能和灵活性,但也需要小心处理以避免潜在的错误。
以下是一些 Go 语言中使用指针的示例:

  1. 基本指针示例
package main

import "fmt"

func main() {
    x := 42
    ptr := &x  // 获取 x 的地址并分配给 ptr
    fmt.Println("Value of x:", x)
    fmt.Println("Address of x:", ptr)
    fmt.Println("Value pointed to by ptr:", *ptr) // 解引用 ptr 获取 x 的值
}
  1. 传递指针给函数
package main

import "fmt"

func changeValue(ptr *int) {
    *ptr = 100
}

func main() {
    x := 42
    fmt.Println("Before change:", x)
    changeValue(&x) // 传递 x 的地址给函数
    fmt.Println("After change:", x) // x 的值被修改为 100
}
  1. 指针和结构体
package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    p := Point{X: 10, Y: 20}
    ptr := &p // 获取 Point 结构体的地址
    fmt.Println("Original Point:", p)
    
    // 修改结构体字段的值
    ptr.X = 30
    fmt.Println("Modified Point:", p)
}
  1. 切片与指针:切片实际上是一个包含指向底层数组的指针、长度和容量信息的数据结构。
package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 100
}

func main() {
    arr := []int{1, 2, 3}
    fmt.Println("Original Slice:", arr)
    modifySlice(arr)
    fmt.Println("Modified Slice:", arr) // 切片中的元素被修改
}

这些示例展示了指针在 Go 中的不同用法,包括基本指针操作、传递指针给函数、结构体指针和切片与指针的关系。指针允许我们直接访问和修改内存中的数据,是实现许多复杂数据结构和操作的基础。
在 Go 语言中,指针的类型主要取决于指针所指向的数据类型。以下是 Go 语言中常见的指针类型:

  1. 指向整数类型的指针

    • *int: 指向整数的指针。
    • *int8, *int16, *int32, *int64: 指向不同大小的整数的指针。
  2. 指向浮点数类型的指针

    • *float32, *float64: 指向浮点数的指针。
  3. 指向布尔类型的指针

    • *bool: 指向布尔值的指针。
  4. 指向字符串类型的指针

    • *string: 指向字符串的指针。
  5. 指向结构体类型的指针

    • 指向自定义结构体的指针,例如 *MyStruct,其中 MyStruct 是你自己定义的结构体类型。
  6. 指向切片和数组类型的指针

    • *[]T*[N]T,其中 T 是切片或数组中元素的类型,N 是数组的长度(可选)。
  7. 指向函数类型的指针

    • *func(): 指向无参数函数的指针。
    • *func(arg1Type, arg2Type, ...): 指向具有特定参数类型的函数的指针。
  8. 指向接口类型的指针

    • *interface{}: 指向任意接口类型的指针。
  9. 指向指针类型的指针

    • **T: 指向指针的指针,其中 T 是指针指向的数据类型。

这些是 Go 语言中常见的指针类型,你可以根据需要使用不同类型的指针来操作不同类型的数据。指针是 Go 中非常有用的特性,允许你直接访问和修改内存中的数据。

7.1指针数组

在 Go 语言中,指针数组是一个数组,其中的每个元素都是指向特定数据类型的指针。指针数组的声明方式如下:

var ptrArray [5]*int // 声明一个包含 5 个整数指针的数组

上述声明创建了一个包含 5 个整数指针的数组,这些指针可以分别指向不同的整数变量或整数数组的元素。以下是一个使用指针数组的示例:

package main

import "fmt"

func main() {
    // 声明一个包含 5 个整数的数组
    arr := [5]int{10, 20, 30, 40, 50}

    // 声明一个包含 5 个整数指针的指针数组,并将它们分别指向数组元素
    var ptrArray [5]*int
    for i := 0; i < len(arr); i++ {
        ptrArray[i] = &arr[i]
    }

    // 通过指针数组访问数组元素的值
    for i := 0; i < len(ptrArray); i++ {
        fmt.Printf("Element %d: Address=%p, Value=%d\n", i, ptrArray[i], *ptrArray[i])
    }
}

在上面的示例中,我们首先声明了一个包含 5 个整数的数组 arr,然后声明了一个包含 5 个整数指针的指针数组 ptrArray。通过循环,我们将每个指针指向数组 arr 中的相应元素,并使用指针数组来访问数组元素的地址和值。

指针数组在某些情况下非常有用,特别是在需要引用多个变量或数据结构的场景中。它允许你维护一个指向多个不同数据的指针集合。

八.类型定义

在 Go 语言中,有两种方式可以创建新的类型或类型别名:类型定义(Type Definition)和类型别名(Type Alias)。这两者有不同的语法和用途。

类型定义(Type Definition)

类型定义是通过 type 关键字来创建一个全新的数据类型。这个新类型具有与其基础类型相同的底层结构,但它们在类型系统中被视为不同的类型。类型定义通常用于在编写代码时提供更多的语义信息,以增加代码的可读性。

type Celsius float64 // 创建新的类型 Celsius,底层类型是 float64
type ID int         // 创建新的类型 ID,底层类型是 int

使用类型定义后,CelsiusID 将被视为不同于 float64int 的类型,尽管它们具有相同的底层结构。

类型别名(Type Alias)

类型别名是通过 type 关键字创建的一个现有类型的别名。类型别名不会创建新的类型,而只是为现有类型提供一个替代名称。类型别名通常用于提供向后兼容性,以便可以逐渐将新名称引入代码。

type MyString = string // 创建类型别名 MyString,它与 string 类型是同一个类型

使用类型别名后,MyString 将被视为 string 的别名,它们是相同的类型。

区别:

  • 类型定义创建一个新类型,使其在类型系统中被视为不同的类型。
  • 类型别名只是为现有类型提供了一个替代名称,没有创建新的类型。

在选择类型定义和类型别名时,需要根据代码的需求和语义来进行选择。通常,类型定义用于提供更多的语义信息,而类型别名用于兼容性和简化代码。

7.2nil介绍

在Go语言中,nil 是一个预定义的标识符,用于表示指针、函数、接口、映射、切片、通道(channel)以及聚合类型(如数组、结构体等)的零值(zero value),但这些聚合类型的零值并不是 nil(它们可能是零长度的切片、空结构体等)。这里我们主要关注指针、接口、映射、切片和通道的 nil 值。

指针(Pointers):
在Go中,当你声明一个指针变量但没有初始化它时,它的默认值是 nil。这表示指针没有指向任何有效的内存地址。

go
var ptr *int // ptr 是一个 int 类型的指针,默认值为 nil

接口(Interfaces):
接口类型的变量可以持有一个实现了该接口的具体类型的值,或者它的值可以是 nil,表示没有引用任何值。

go
var intf interface{} // intf 是一个空接口变量,默认值为 nil

映射(Maps):
映射是Go中的关联数组,允许你使用键来查找值。当映射被声明但没有初始化时,它的默认值是 nil。一个 nil 的映射不能进行任何操作(如读取、写入或迭代),必须先通过 make 函数进行初始化。

go
var m map[string]int // m 是一个 string 到 int 的映射,默认值为 nil  
m = make(map[string]int) // 初始化 m

切片(Slices):
切片是对数组的抽象,提供了动态大小的、灵活的、可变的序列类型。与映射类似,当切片被声明但没有初始化时,它的默认值是 nil。一个 nil 切片没有底层数组,长度为0,容量为0。

go
var s []int // s 是一个 int 类型的切片,默认值为 nil  
s = make([]int, 0, 5) // 初始化 s,长度为0,容量为5

通道(Channels):
通道是Go中用于在goroutine之间进行通信的管道。与映射和切片类似,当通道被声明但没有初始化时,它的默认值是 nil。一个 nil 通道不能进行任何操作(如发送、接收或关闭),必须先通过 make 函数进行初始化。

go
var ch chan int // ch 是一个 int 类型的通道,默认值为 nil  
ch = make(chan int) // 初始化 ch

在Go中,检查一个指针、接口、映射、切片或通道是否为 nil 是常见的做法,以确保在尝试访问它们之前它们是有效的。这有助于避免运行时错误,如空指针引用或尝试在 nil 映射上进行操作。

go的结构体默认值不是nil,而是具体字段的默认值,pointer默认是nil

var ps []Person
if ps == nil {
	fmt.Println("this is a nil")
}


//这边是make初始化了所以不是nil
people := make([]Person, 3)

if people == nil {
	fmt.Println("this is aaaaa nil")
}

8.1.Golang结构体

Go 语言是一门支持面向对象编程(Object-Oriented Programming,简称 OOP)的编程语言,但它采用了一种不同于传统的面向对象编程语言的方式。Go 更强调简洁性和清晰性,而不是复杂的继承层次和类型层次。

Go 语言中的面向对象编程特性包括以下要点:

go语言类型要求很严格,自定义的类型在运算时候也需要同一种类型才可以

  1. 结构体(Struct):Go 中的结构体类似于其他编程语言中的类,它可以包含数据字段和方法。结构体是自定义数据类型,可以用于建模对象。

  2. 方法(Method):在 Go 中,可以为结构体定义方法。方法是与结构体关联的函数,它们可以在结构体上调用,以实现特定的行为。

  3. 接口(Interface):接口是一种定义了一组方法签名的抽象类型。类型可以实现接口,只要它们提供了接口所定义的所有方法。这允许多态性和代码重用。

  4. 组合和嵌入(Composition and Embedding):Go 语言通过结构体的嵌套和组合实现了对象复用。你可以在一个结构体中嵌入其他结构体,以复用其字段和方法。

  5. 封装(Encapsulation):Go 语言支持访问控制,可以使用大小写来定义是否导出一个标识符。导出的标识符可以在包外部访问,从而实现了封装。

虽然 Go 语言支持面向对象编程的概念,但它不像某些其他语言(如Java和C++)那样强调继承层次和类的概念。Go 更多地依赖于接口和组合来实现代码的可复用性和灵活性,鼓励简单和直接的设计。这种设计哲学使得 Go 语言在构建高性能、可维护的系统时非常有用。

下面是一个简单的 Go 语言结构体示例,展示了如何定义结构体、创建结构体实例以及访问结构体字段和方法:

package main

import "fmt"

// 定义一个结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// 为结构体定义一个方法
func (p Person) FullName() string {
    return p.FirstName + " " + p.LastName
}

func main() {
    // 创建结构体实例
    person1 := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 访问结构体字段
    fmt.Println("First Name:", person1.FirstName)
    fmt.Println("Last Name:", person1.LastName)
    fmt.Println("Age:", person1.Age)

    // 调用结构体方法
    fullName := person1.FullName()
    fmt.Println("Full Name:", fullName)

    // 修改结构体字段的值
    person1.Age = 31
    fmt.Println("Updated Age:", person1.Age)
}

在上面的示例中,我们首先定义了一个名为 Person 的结构体,该结构体具有三个字段:FirstNameLastNameAge。然后,我们为 Person 结构体定义了一个方法 FullName,该方法返回完整的姓名。

main 函数中,我们创建了一个名为 person1Person 结构体实例,并演示了如何访问结构体的字段和调用结构体的方法。还演示了如何修改结构体字段的值。

这个示例只是一个简单的结构体用法示例,结构体在 Go 语言中常用于建模和组织数据,用于构建复杂的数据类型和对象。
在 Go 语言中,匿名结构体是一种没有类型名称的结构体,通常用于创建临时的、一次性的数据结构,而不需要定义一个具体的结构体类型。匿名结构体的语法非常简单,可以在需要的地方直接声明和初始化。

8.2.Golang匿名结构体

下面是一个匿名结构体的示例,最后的打括号用于初始化,展示了如何创建和使用匿名结构体:

package main

import "fmt"

func main() {
    // 创建一个匿名结构体并初始化
    person := struct {
        FirstName string
        LastName  string
        Age       int
    }{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 访问匿名结构体字段
    fmt.Println("First Name:", person.FirstName)
    fmt.Println("Last Name:", person.LastName)
    fmt.Println("Age:", person.Age)

    // 修改匿名结构体字段的值
    person.Age = 31
    fmt.Println("Updated Age:", person.Age)
}

在上面的示例中,我们创建了一个匿名结构体并初始化了它。匿名结构体的定义和初始化与命名结构体相似,只是没有类型名称。然后,我们访问和修改了匿名结构体的字段。

匿名结构体在某些情况下非常方便,特别是在需要创建临时数据结构或进行快速的数据组合时。然而,由于它们没有类型名称,因此只能在局部范围内使用,通常不用于公共接口或全局变量。

8.3.Golang结构体的初始化

在 Go 语言中,结构体的初始化可以通过多种方式进行,具体的方式取决于你的需求和代码设计。以下是一些常见的结构体初始化方式:

  1. 字面量初始化:可以使用结构体字面量(类似于JSON)来初始化结构体。在这种方式下,你需要提供结构体字段的值,并按照字段的定义顺序或字段名来赋值。
// 定义一个结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// 字面量初始化
person := Person{
    FirstName: "John",
    LastName:  "Doe",
    Age:       30,
}
  1. 使用零值初始化:当你声明一个结构体变量而不显式初始化时,结构体的零值将自动赋给它。对于数值类型,零值是0;对于字符串类型,零值是空字符串;对于布尔类型,零值是false
var person Person // 结构体零值初始化
  1. 使用new函数初始化:可以使用new函数来分配一个零值结构体,并返回一个指向结构体的指针。
personPtr := new(Person) // 使用new函数初始化,并得到指针
  1. 使用&符号初始化:你可以直接使用&符号创建一个结构体指针,然后为该指针的字段赋值。
personPtr := &Person{
    FirstName: "Jane",
    LastName:  "Smith",
    Age:       25,
}
  1. 使用结构体构造函数:你可以定义一个构造函数,用于创建和初始化结构体实例。构造函数通常返回一个结构体实例。
func NewPerson(firstName string, lastName string, age int) *Person {
    return &Person{
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
    }
}

person := NewPerson("Alice", "Johnson", 28) // 使用构造函数初始化

你可以按照结构体字段的定义顺序来顺序赋值。这种方式适用于匿名结构体或者当你希望初始化时省略字段名的情况。以下是一个示例:

// 定义一个结构体
type Point struct {
    X int
    Y int
}

func main() {
    // 顺序赋值
    p := Point{10, 20}

    fmt.Println("X:", p.X)
    fmt.Println("Y:", p.Y)
}

在上面的示例中,我们创建了一个 Point 结构体的实例 p,并按照字段定义的顺序分别为 XY 字段赋值。

虽然这种方式可以省略字段名,但需要确保按照正确的顺序给字段赋值,否则可能导致字段值混淆。因此,使用字段名明确赋值通常更容易理解和维护。
在 Go 中,你可以选择性地初始化结构体的部分成员,而不是为每个字段都提供值。这样做的关键是使用结构体字面量,并只为你想要初始化的字段提供值。其他字段将被自动初始化为它们的零值。

以下是一个示例,演示如何部分初始化结构体:

package main

import "fmt"

// 定义一个结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 部分成员初始化
    person := Person{
        FirstName: "John",
        Age:       30,
    }

    // 访问结构体字段
    fmt.Println("First Name:", person.FirstName)
    fmt.Println("Last Name:", person.LastName) // 未初始化字段,输出空字符串
    fmt.Println("Age:", person.Age)

    // 修改结构体字段的值
    person.Age = 31
    fmt.Println("Updated Age:", person.Age)
}

在上面的示例中,我们只为 FirstNameAge 字段提供了值,而 LastName 字段将被自动初始化为空字符串,因为它没有显式赋值。这使得你可以选择性地初始化结构体的字段,根据需要来赋值。未初始化的字段将采用它们的零值。

这种方式使得在创建结构体实例时更加灵活,你只需为必要的字段提供值,而不必关心其他字段。

8.4.Json转结构体

转换json的参数都是[]byte类型,转换为json字符串之后也是比特数组,所以要转string
如果一个切片中包含多个map键值对,输出时候使用fmt.printf(“%+v”,varName)的方式可以输出对应的key和value,不然只会输出value,结构体输出也是这样。
你可以使用 Go 语言的结构体来表示上述 JSON 数据。首先,需要定义一个 Go 结构体,以匹配 JSON 中的字段结构。以下是一个示例:

package main

import (
    "fmt"
    "encoding/json"
)

// 定义一个结构体用于匹配 JSON 数据的结构
type Result struct {
    Code   string `json:"code"`
    Result struct {
        Total int `json:"total"`
        Page  int `json:"page"`
        Row   int `json:"row"`
        Data  []struct {
            ID      int     `json:"id"`
            Mode    string  `json:"mode"`
            DT      int     `json:"dt"`
            Agent   int     `json:"agent"`
            Used    string  `json:"used"`
            UsedYOY string  `json:"used_yoy,omitempty"`
            UsedMOM string  `json:"used_mom,omitempty"`
            Used1   string  `json:"used_1"`
            Used2   string  `json:"used_2"`
            Used3   string  `json:"used_3"`
            Used4   string  `json:"used_4"`
            From    string  `json:"from"`
            To      string  `json:"to"`
            Name    string  `json:"name"`
            Stations string `json:"stations"`
        } `json:"data"`
    } `json:"result"`
}

func main() {
    // JSON 数据字符串
    jsonData := `{"code":"00000","result":{"total":106,"page":1,"row":10,"data":[{"id":97370,"mode":"y","dt":2023,"agent":294,"used":"32.7300000000","used_yoy":null,"used_mom":null,"used_1":"11.8500000000","used_2":"10.3000000000","used_3":"0.0000000000","used_4":"10.5900000000","from":"2023-08-22 13:00:00","to":"2023-09-17 15:00:00","name":"B08宿舍","stations":"697"}]}}`

    // 解析 JSON 数据到结构体
    var result Result
    err := json.Unmarshal([]byte(jsonData), &result)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // 访问解析后的数据
    fmt.Println("Code:", result.Code)
    fmt.Println("Total:", result.Result.Total)
    fmt.Println("First Data ID:", result.Result.Data[0].ID)
}

在上面的示例中,我们首先定义了一个名为 Result 的结构体,该结构体的字段与 JSON 数据中的字段相匹配。然后,我们使用 json.Unmarshal 函数将 JSON 数据解析到这个结构体中,以便后续访问和操作。你可以根据需要访问解析后的结构体字段。

8.5.结构体指针和new关键字

结构体指针是指向结构体实例的内存地址的指针。它们允许你在程序中直接操作结构体的内部数据,而无需进行复制,这对于大型结构体或需要在多个地方引用相同数据的情况非常有用。以下是有关结构体指针的介绍:

  1. 创建结构体指针:你可以使用&运算符来创建结构体指针。例如,如果有一个结构体类型Person,你可以创建一个指向Person类型的指针如下:

    var personPtr *Person
    
  2. 访问结构体字段:使用指针可以通过->操作符(类似于C/C++)或.操作符来访问结构体的字段。例如:

    // 通过指针访问字段
    personPtr := &Person{FirstName: "John", LastName: "Doe"}
    fmt.Println("First Name:", personPtr.FirstName)
    fmt.Println("Last Name:", personPtr.LastName)
    
  3. 传递结构体指针:当你传递一个结构体指针作为函数参数时,函数可以修改原始结构体的值。这是因为函数接收到的是指向结构体的地址,而不是结构体的副本。

    func modifyPerson(person *Person) {
        person.FirstName = "Jane"
    }
    
    person := &Person{FirstName: "John"}
    modifyPerson(person)
    fmt.Println("First Name after modification:", person.FirstName)
    
  4. 避免不必要的指针使用:尽管结构体指针很有用,但要谨慎使用。只有在需要直接修改结构体的情况下才使用指针。在其他情况下,使用结构体值会更安全,因为它们避免了意外的副作用。

  5. 初始化结构体指针:你可以使用new函数初始化一个结构体指针,并分配内存。

    personPtr := new(Person)
    personPtr.FirstName = "John"
    personPtr.LastName = "Doe"
    

总之,结构体指针是 Go 语言中一种有用的工具,用于操作结构体的数据,特别是在需要修改结构体的情况下。但要小心使用,以避免潜在的指针错误。
使用 new 操作符可以创建一个指向结构体的指针,并分配内存空间。这是在 Go 中创建结构体实例的一种常见方式,特别是在需要将结构体传递给函数并在函数内部修改其值时非常有用。以下是一个示例:

package main

import "fmt"

// 定义一个结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 使用 new 创建结构体的指针,并分配内存
    personPtr := new(Person)

    // 使用指针访问结构体字段,并为字段赋值
    personPtr.FirstName = "John"
    personPtr.LastName = "Doe"
    personPtr.Age = 30

    // 访问和打印结构体的值
    fmt.Println("First Name:", personPtr.FirstName)
    fmt.Println("Last Name:", personPtr.LastName)
    fmt.Println("Age:", personPtr.Age)
}

在上述示例中,我们首先使用 new 操作符创建了一个指向 Person 结构体的指针 personPtr,并为其分配了内存。然后,我们通过该指针访问结构体字段,并为字段分别赋值。

这种方式适用于需要动态分配内存并获得指向结构体的指针的情况,特别是在函数中创建结构体实例并返回指针时非常有用。通过指针,你可以直接修改结构体的字段值。
在 Go 语言中,new 是一个用于创建指向新分配的零值的指针的内建函数。它主要用于动态分配内存,并返回一个指向分配的内存地址的指针。下面是 new 关键字的使用示例:

package main

import "fmt"

func main() {
    // 使用 new 创建一个整数的指针
    numPtr := new(int)
    
    // new 返回一个指向零值整数的指针
    // 打印该指针的值将会是零值,即0
    fmt.Println("Value of numPtr:", *numPtr)

    // 修改通过指针访问的值
    *numPtr = 42
    fmt.Println("New value of numPtr:", *numPtr)
}

在上述示例中,我们首先使用 new 函数创建了一个 int 类型的指针 numPtr,它指向一个新分配的整数的零值。然后,我们通过 *numPtr 访问该指针所指向的整数,并对其进行修改。

请注意,new 函数返回的是指向新分配的零值的指针,因此在创建后,该指针所指向的值将是类型的零值(对于 int 类型,零值是0)。

new 关键字通常用于创建指向结构体、数组、切片等数据类型的指针,并分配所需的内存。这使得在需要时可以在堆上动态分配内存,以便在多个地方共享相同的数据。

8.6.结构体的函数传递参数

结构体可以作为函数的参数传递,这样函数可以访问和修改结构体的字段值。这是在 Go 中常见的一种方式,用于将数据传递给函数并对其进行处理。以下是结构体作为函数参数的几种情况:

  1. 传递结构体值

    package main
    
    import "fmt"
    
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }
    
    func printPerson(p Person) {
        fmt.Println("First Name:", p.FirstName)
        fmt.Println("Last Name:", p.LastName)
        fmt.Println("Age:", p.Age)
    }
    
    func main() {
        person := Person{
            FirstName: "John",
            LastName:  "Doe",
            Age:       30,
        }
        printPerson(person)
    }
    

    在这个例子中,printPerson 函数接收一个 Person 结构体值作为参数,然后打印结构体的字段值。

  2. 传递结构体指针

    package main
    
    import "fmt"
    
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }
    
    func modifyPerson(p *Person) {
        p.Age = 31
    }
    
    func main() {
        person := &Person{
            FirstName: "John",
            LastName:  "Doe",
            Age:       30,
        }
        modifyPerson(person)
        fmt.Println("Updated Age:", person.Age)
    }
    

    在这个例子中,modifyPerson 函数接收一个 Person 结构体指针作为参数,并通过指针修改了结构体的字段值。这使得在函数内部对结构体进行修改后,原始结构体也会受到影响。

  3. 传递结构体的切片或数组

    package main
    
    import "fmt"
    
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }
    
    func printPeople(people []Person) {
        for _, p := range people {
            fmt.Println("First Name:", p.FirstName)
            fmt.Println("Last Name:", p.LastName)
            fmt.Println("Age:", p.Age)
        }
    }
    
    func main() {
        people := []Person{
            {FirstName: "John", LastName: "Doe", Age: 30},
            {FirstName: "Jane", LastName: "Smith", Age: 25},
        }
        printPeople(people)
    }
    

    在这个例子中,printPeople 函数接收一个 Person 结构体的切片作为参数,并遍历打印每个结构体的字段值。这种方式适用于处理多个结构体的情况。

总之,结构体可以作为函数的参数传递,无论是作为值还是指针,都可以根据需要选择来访问和修改结构体的字段。这使得在 Go 中能够方便地处理和操作复杂的数据结构。

九.方法

在许多面向对象编程语言中,类和方法是面向对象编程的核心概念。虽然 Go 语言中没有类的概念,但它的方法具有一些与类和方法相关的相似点,这些相似点包括:

  1. 封装:类和方法都支持封装,即将数据和方法组合在一起,并控制对它们的访问。在 Go 中,可以通过将字段和方法定义在同一个结构体中来实现封装。

  2. 数据和行为的关联:类和方法都允许将数据(字段)与特定行为(方法)关联起来。在 Go 中,这可以通过将方法定义在特定类型的接收者上实现。

  3. 方法调用:与类的实例方法类似,Go 中的方法可以通过实例(值或指针)来调用。这使得可以在特定类型的实例上执行操作。

  4. 代码组织:方法允许将相关的操作组织在一起,从而提高了代码的可读性和维护性。虽然 Go 中没有类的层次结构,但可以将类型相关的方法定义在同一个文件或包中。

  5. 可扩展性:类和方法都支持扩展,可以添加新的方法来扩展现有类型的行为。在 Go 中,可以为现有类型添加新的方法,无需修改原始类型的定义。

然而,也有一些重要的区别:

  • Go 中没有类的概念,因此没有类层次结构、继承或类的实例化。
  • Go 的方法是与类型关联的,而不是与实例关联的。这意味着方法在类型级别定义,而不是在实例级别。
  • Go 中的方法可以用于内置类型,不仅限于用户自定义类型。

总之,虽然 Go 中没有传统的类和类的实例化,但通过方法,你可以实现面向对象编程的一些关键概念,如封装、数据和行为的关联以及代码组织。这种方法在 Go 中通常被称为基于组合的面向对象编程。
在 Go 语言中,方法是与特定类型关联的函数。这些函数被称为方法,它们可以在类型的实例上调用。方法使得可以在类型上定义行为,类似于面向对象编程中的方法。

以下是关于 Go 方法的介绍和示例:

9.1.方法定义

在 Go 中,方法的定义与函数类似,但在函数名前加上了接收者(receiver)参数,接收者可以是任何命名类型,通常是一个自定义类型的值或指针。方法的定义如下:

func (receiver Type) methodName(parameters) returnType {
    // 方法的实现
}
  • receiver 是方法所属的类型。
  • methodName 是方法的名称。
  • parameters 是方法的参数列表。
  • returnType 是方法的返回类型。
  1. 值接收者和指针接收者

    方法可以有两种类型的接收者:值接收者和指针接收者。值接收者用于在方法内部操作值的副本,而指针接收者用于在方法内部修改原始值。选择哪种接收者取决于是否需要修改原始值。地址换地是省空间的,一般都是地址传递,指针在结构体上有优化,指针指向结构体的时候可以通过结构体对象直接访问内部数据,不用*先解出来

    // 值接收者
    func (p Person) SayHello() {
        fmt.Println("Hello, I'm", p.FirstName)
    }
    
    // 指针接收者
    func (p *Person) SetAge(age int) {
        p.Age = age
    }
    

9.2.方法调用

方法可以通过实例(值或指针)来调用。当调用方法时,方法的接收者会自动传递到方法内部。

person := Person{FirstName: "John", LastName: "Doe", Age: 30}

// 调用值接收者方法
person.SayHello()

// 调用指针接收者方法
person.SetAge(31)

在上述示例中,我们首先创建了一个 Person 结构体的实例 person,然后调用了两个不同类型的方法。

  1. 方法集

    方法集是类型的一部分,它定义了可以在该类型上调用的方法。Go 中有两种方法集:值方法集和指针方法集。对于值接收者的方法,可以在值和指针上调用;而对于指针接收者的方法,只能在指针上调用。

    var p1 Person
    p2 := &Person{}
    
    p1.SayHello() // 可以调用,因为值方法集包括值和指针
    p2.SayHello() // 可以调用,因为值方法集包括值和指针
    
    p1.SetAge(32) // 不能调用,因为指针方法集只包括指针
    p2.SetAge(33) // 可以调用,因为指针方法集包括指针
    

9.3 注意事项

这些是关于 Go 方法的基本概念和用法。方法使得可以将特定行为与自定义类型相关联,提高了代码的组织性和可读性。
在使用 Go 语言中的方法时,有一些注意事项和最佳实践,以确保代码的可读性和可维护性:

  1. 接收者的命名:通常,接收者参数的命名使用类型名称的首字母缩写,例如,如果类型是 Person,则接收者可以命名为 pperson。遵循一致的命名约定可以提高代码的可读性。

  2. 值接收者 vs. 指针接收者:选择使用值接收者还是指针接收者取决于你是否需要在方法内部修改原始值。如果需要修改原始值,则使用指针接收者。否则,使用值接收者以避免无意中修改值的副本。

  3. 避免不必要的指针接收者:不是所有的方法都需要指针接收者。只有在需要修改接收者状态或者接收者是大型数据结构时才使用指针接收者。否则,使用值接收者可以更安全和清晰。

  4. 方法的一致性:如果一个类型有多个方法,确保它们在方法名称、参数数量和类型上保持一致性。这使得类型的行为更加一致和可预测。

  5. 避免方法冲突:当一个类型实现多个接口时,确保没有方法签名冲突。方法签名包括方法的名称、参数数量和参数类型。避免冲突可以保持代码的一致性。

  6. 方法文档:为方法编写清晰的文档注释,以便其他开发者能够理解方法的用途和行为。文档应包括方法的参数说明、返回值说明和用法示例。

  7. 方法的导出性:只有以大写字母开头的方法才会被导出(可从其他包中访问)。小写字母开头的方法仅在同一包内可见。

  8. 方法集:了解方法集的概念,包括值方法集和指针方法集。理解方法集有助于理解在哪些情况下可以调用方法。

  9. 接口的实现:当实现接口时,确保类型的方法与接口定义的方法完全匹配。接口实现应该是隐式的,即类型满足接口的所有方法。

  10. 方法嵌套:在结构体中嵌套类型时,注意内嵌类型的方法是否会与外部类型的方法产生冲突。使用方法重命名或组合来解决冲突。

这些是在使用 Go 语言中的方法时的一些注意事项。良好的方法设计和一致的编程习惯可以提高代码的质量和可维护性。

1、方法的recever type并非一定是struct类型,type的类型定义别名,slice、 map 、channel、func类型等都可以
2、struct结合他的方法等价于面向对象的类,只不过struct可以和他的方法分开,并非一定要属于同一个文件,但必须属于同一个包
3、方法接收有两种类型:(T Type)和(T *Type)他们之间有区别
4、方法就是函数,所以go中没有重载的概念,也就是说同一个类型的所有方法名必须唯一
5、如果receiver是一个指针类型,就会自动解除引用就是原来要 (*指针变量 = 赋值 现在直接 指针变量 = 赋值)
6、方法和type是分开的,意味着示例的行为和数据存储是分开的,通过receiver建立关联关系

十.接口

在Go语言中,接口(interface)是一种类型,它定义了一组方法的集合。一个类型如果拥有接口中定义的所有方法,则称该类型实现了该接口。然而,在Go中,并没有显式的语句来声明一个类型实现了某个接口,这是隐式的。

当一个类型定义了接口中所有方法的实现时,它就被认为实现了该接口。Go编译器会在编译时检查这一点。

下面是一个简单的例子,演示了如何在Go中实现接口:

go
package main  
  
import "fmt"  
  
// 定义一个接口  
type Speaker interface {  
    Speak() string  
}  
  
// 定义一个结构体  
type Dog struct {  
    Name string  
}  
  
// 为Dog类型实现Speak方法  
func (d Dog) Speak() string {  
    return d.Name + " says woof!"  
}  
  
// 定义一个函数,它接受一个实现了Speaker接口的参数  
func Greet(s Speaker) {  
    fmt.Println(s.Speak())  
}  
  
func main() {  
    myDog := Dog{Name: "Buddy"}  
    Greet(myDog) // 这里可以传入myDog,因为Dog类型实现了Speaker接口  
}

在上面的例子中,Speaker 是一个接口,它定义了一个名为 Speak 的方法。Dog 是一个结构体,它有一个 Speak 方法,其签名与 Speaker 接口中的 Speak 方法相同。因此,Dog 类型隐式地实现了 Speaker 接口。

当我们调用 Greet(myDog) 时,因为 myDog 的类型是 Dog,而 Dog 实现了 Speaker 接口,所以这是合法的调用。

请注意,Go语言中不需要(也不支持)使用类似 implements 的关键字来显式声明一个类型实现了某个接口。接口的实现是隐式的,只要类型的方法集合是接口方法集合的超集即可。

Go语言中的接口实现是隐式的。这意味着类型无需显式声明它们实现了某个接口,只要它们满足接口中定义的方法签名,它们就被视为实现了该接口。这是与某些其他编程语言不同的地方,其中接口实现通常需要显式声明。

在Go中,你可以定义一个新的类型,并在该类型上实现接口中指定的方法,而不需要在类型声明中明确指出它实现了哪些接口。这种隐式接口实现的方式使得代码更加灵活,你可以在不修改原始类型定义的情况下为类型添加新的接口实现,这也促使了更好的代码组织和可扩展性。

在Go语言中,要实现一个接口,你需要创建一个具有与接口中定义的所有方法相匹配的方法的类型。然后,你可以创建该类型的值,并将其赋给接口类型的变量。以下是实现和调用接口的基本步骤:

  1. 定义接口:首先,你需要定义一个接口,指定接口中的方法签名。
type Writer interface {
    Write([]byte) (int, error)
}
  1. 创建一个类型并实现接口方法:接下来,创建一个自定义类型,并为其添加接口中指定的方法。确保方法的签名与接口方法的签名完全匹配。
type MyWriter struct{}

func (mw MyWriter) Write(data []byte) (int, error) {
    // 实现 Write 方法的具体逻辑
    // 返回写入的字节数和可能的错误
    return len(data), nil
}
  1. 创建接口类型的变量并赋值:现在,你可以创建接口类型的变量,并将实现了接口的类型的值赋给该变量。
var writer Writer
writer = MyWriter{}
  1. 调用接口方法:通过接口变量,你可以调用接口中定义的方法。
data := []byte("Hello, World!")
n, err := writer.Write(data)
if err != nil {
    fmt.Println("Error:", err)
} else {
    fmt.Printf("Wrote %d bytes\n", n)
}

这样,你就可以实现接口并通过接口调用方法。在运行时,实际调用的是 MyWriter 结构体中的 Write 方法,因为它实现了 Writer 接口的方法。

通过这种方式,你可以将不同类型的对象赋给相同的接口变量,从而实现多态,使代码更加灵活和可扩展。
多个类型可以实现同一个接口
多个接口可以让一个类型实现

接口案例

package main

import "fmt"

// 结构定义
type Duck interface {
	// Gaga 方法的申请
	Gaga()
	Walk()
	Swimming()
}

// 具体实现
type pskDuck struct {

	/*可达鸭有没有腿无所谓,照样可以实现走路的方法*/
	//legs int
}

func (pd *pskDuck) Gaga() {
	fmt.Println("嘎嘎嘎嘎")

}
func (pd *pskDuck) Walk() {
	fmt.Println("走路喽")

}
func (pd *pskDuck) Swimming() {
	fmt.Println("游泳喽")

}

func main() {

	//	接口练习,理解鸭子类型,php,python
	//	go语言中,处处都是interface,到处都是鸭子类型
	//	当看到一只鸟,走起来像鸭子,游泳起来像鸭子,叫起来像鸭子,那么这只鸟就是鸭子
	//动词、方法、具备某些方法:鸭子类型强调事物的外部行为,暴露出来的方法,而不是内部的结构

	//重点,声明一个Duck类型变量,将可达鸭复制给他
	//interface是结构的定义也就是方法集,只有定义结构体,将方法绑定了,然后就可以以下赋值使用了,结构体方法的绑定相当于Java中的implement
	var duck Duck = &pskDuck{}
	duck.Walk()
	duck.Gaga()
	duck.Swimming()
}

10.1.接口嵌套组合

当涉及到多个特性的动物或对象时,可以使用接口嵌套来模拟这种复杂性。在这个示例中,我们将创建一个接口,表示既能飞又能游泳的动物,然后实现一个飞鱼类型,该类型同时满足这两个接口。接口是可以复用的

package main

import (
	"fmt"
)

// 定义飞行能力接口
type Flying interface {
	Fly()
}

// 定义游泳能力接口
type Swimming interface {
	Swim()
}

// 定义既能飞又能游泳的接口
type FlyingAndSwimming interface {
	Flying
	Swimming
}

// 飞鱼类型实现Flying和Swimming接口
type FlyingFish struct {
	Name string
}

func (ff FlyingFish) Fly() {
	fmt.Printf("%s can fly!\n", ff.Name)
}

func (ff FlyingFish) Swim() {
	fmt.Printf("%s can swim!\n", ff.Name)
}

func main() {
	flyingFish := FlyingFish{Name: "Flying Fish"}
	
	// 飞鱼既能飞又能游泳,满足FlyingAndSwimming接口
	var fas FlyingAndSwimming = flyingFish

	// 调用接口方法
	fas.Fly()
	fas.Swim()
}

在上述示例中,我们定义了三个接口:FlyingSwimmingFlyingAndSwimming。然后,我们创建了一个飞鱼类型 FlyingFish,该类型实现了 Fly()Swim() 方法。最后,我们将 FlyingFish 类型的实例分配给 FlyingAndSwimming 接口,并调用了两个接口的方法,以展示飞鱼既能飞又能游泳。这样,我们通过接口嵌套来表示了复杂的行为特性。
在Go语言中,不需要显式声明类型实现接口。类型只需要实现接口中定义的所有方法,它就自动满足该接口,无需使用显式的 implements 或 implements 关键字。所以**,当你将 flyingFish 分配给 FlyingAndSwimming 接口时,它实际上是在实现该接口。

具体来说,FlyingFish 类型实现了 Fly() 和 Swim() 方法,这两个方法分别属于 Flying 和 Swimming 接口的方法集。因此,FlyingFish 类型自动满足了 Flying 和 Swimming 接口的要求,并且也满足了 FlyingAndSwimming 接口,因为它包含了 Flying 和 Swimming 这两个接口。

FlyingFish 类型上定义了一个方法 Fly(),这个方法的签名与 Flying 接口中的 Fly() 方法签名相匹配。因此,这个方法的定义实际上是 Flying 接口的一个实现。

当你在 FlyingFish 类型上定义了这个方法后,Go 语言会自动识别并关联 FlyingFish 类型与 Flying 接口之间的关系。这意味着 FlyingFish 类型满足了 Flying 接口的要求,因为它实现了 Fly() 方法。因此,你可以将 FlyingFish 类型的实例分配给 Flying 接口的变量,然后调用 Fly() 方法,这就是接口的一种典型用法。

所以,这一步不仅是在类型中定义了一个方法,还是实现了接口中的方法,使得该类型满足了接口的要求。这是 Go 语言中的接口实现机制。

package main

import "fmt"

//	定义接口

type MyWriter interface {

	// Write 定义一个写的方法
	Write(string) error
}
type MyCloser interface {

	// Close 定义一个关闭文件的方法
	Close() error
}

type writerCloser struct {
	MyWriter //interface也是一种类型,放入写文件的匿名实现。我想放一个写数据库的实现
}

// 写文件结构体
type fileWriter struct {
	filePath string
}

// 写数据库结构体
type databaseWriter struct {
	host string
}

func (fw *fileWriter) Write(s string) error {
	fmt.Println("写文件->", s)
	return nil
}

func (da *databaseWriter) Write(s string) error {
	fmt.Println("写数据库->", s)
	return nil
}

//	定时实现类,实现类不关心,只关心方法有没有,实现重写

func main() {

	//	结构体里面可以放interface,因为interface也是一种类型,具体实现

	//实例化
	var mw MyWriter = &writerCloser{
		//添加具体实现进来
		&databaseWriter{},
	}

	mw.Write("hello")

}

10.2.接口的断言

package main

import "fmt"

func add(a, b interface{}) interface{} {

	switch a.(type) {
	case int:
		ai, _ := a.(int)
		bi, _ := b.(int)
		return ai + bi

	case float64:
		ai, _ := a.(float64)
		bi, _ := b.(float64)
		return ai + bi
	default:
		return "什么都不是"
	}

}

func main() {
	//这里有个问题,如下,switch进入int float直接转换为默认值0了,所以还是用范形
	var a int = 1
	var b float64 = 1.1
	re := add(a, b)
	fmt.Println(re)
}

10.3.Golang的OCP原则

OCP(Open-Closed Principle)是面向对象编程中的一个重要设计原则,它强调软件实体(通常是类、模块或函数)应该对扩展开放,对修改关闭。这意味着你可以通过添加新的代码来扩展现有的功能,而不需要修改现有的代码。这有助于降低代码的耦合性,并使代码更容易维护和扩展。

在Go语言中,OCP原则同样适用,尽管Go没有类和继承的概念,但仍然可以使用以下方法来遵守OCP原则:

  1. 接口:在Go中,您可以定义接口来描述类型的行为。当您编写函数或方法时,尽量使用接口而不是具体的类型。这样,您可以通过实现新的接口来添加新的功能,而不需要修改现有的函数或方法。
type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

在这个例子中,如果您想要添加新的形状,只需实现Shape接口,而不需要修改现有的Area函数。

  1. 组合:Go通过组合而不是继承来实现代码复用。您可以使用嵌入(Embedding)来组合不同类型的结构体,从而扩展其功能。
type Animal struct {
    Name string
}

func (a Animal) Speak() {
    fmt.Println(a.Name + " makes a sound")
}

type Dog struct {
    Animal
}

func (d Dog) Speak() {
    fmt.Println(d.Name + " barks")
}

在这个例子中,Dog类型通过嵌入Animal类型来扩展其功能,而不需要修改Animal类型。

总之,尽管Go不使用传统的类和继承模型,但您仍然可以通过接口和组合来遵守OCP原则,使您的代码更容易扩展和维护。

10.4.Golang的OOP原则

Go语言在设计上并不支持传统的面向对象编程(OOP)原则,如类和继承。它采用了一种更为简洁和灵活的方式来实现面向对象编程,主要依赖于结构体、接口、和组合,而不是类和继承。虽然Go不是一个典型的OOP语言,但仍然可以应用一些OOP原则和概念。以下是一些在Go中使用的OOP原则和相关概念:

  1. 结构体(Structs):在Go中,结构体用于定义数据结构,类似于其他OOP语言中的类。您可以在结构体中定义字段和方法。
type Person struct {
    Name string
    Age  int
}

func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s, and I am %d years old.\n", p.Name, p.Age)
}
  1. 方法(Methods):Go允许在结构体上定义方法。这些方法可以被结构体的实例调用,类似于其他语言中的类方法。

  2. 接口(Interfaces):接口在Go中扮演了重要角色,它们描述了类型的行为。一个类型只要实现了接口中定义的方法,就被视为实现了该接口。

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}
  1. 组合(Composition):Go通过组合而不是继承来实现代码复用。您可以嵌入一个结构体类型来获得其字段和方法,从而实现代码的复用。
type Animal struct {
    Name string
}

func (a Animal) Speak() {
    fmt.Println(a.Name + " makes a sound")
}

type Dog struct {
    Animal
}

func (d Dog) Bark() {
    fmt.Println(d.Name + " barks")
}
  1. 封装(Encapsulation):Go支持访问控制,您可以使用大写字母开头的字段和方法来实现对外部包的封装。小写字母开头的字段和方法只能在同一个包内部访问。

虽然Go的面向对象编程方式与传统的OOP语言(如Java和C++)不同,但它提供了一种灵活且简洁的方式来组织和抽象代码,使得代码易于理解、测试和维护。在Go中,您可以利用结构体、方法、接口和组合来实现OOP的核心概念,同时避免了一些与传统OOP相关的复杂性。这种设计哲学有助于编写高效、可维护的代码。
Go语言支持结构体包含多个其他结构体对象,这被称为嵌入(embedding)或组合。通过将一个结构体嵌套到另一个结构体中,您可以在一个结构体类型中包含另一个结构体类型的字段。这种方式可以用来创建复杂的数据结构,实现对象组合,以及代码重用。

以下是一个示例:

package main

import (
    "fmt"
)

// 定义一个吉他结构体
type Guitar struct {
    Brand  string
    Model  string
    Strings int
}

// 定义一个音乐家结构体,包含吉他
type Musician struct {
    Name    string
    Age     int
    Guitar  Guitar
}

func main() {
    // 创建一个吉他对象
    myGuitar := Guitar{
        Brand:  "Fender",
        Model:  "Stratocaster",
        Strings: 6,
    }

    // 创建一个音乐家对象,包含吉他对象
    john := Musician{
        Name:    "John",
        Age:     30,
        Guitar:  myGuitar,
    }

    // 访问音乐家的吉他信息
    fmt.Printf("%s plays a %s %s with %d strings.\n", john.Name, john.Guitar.Brand, john.Guitar.Model, john.Guitar.Strings)
}

在这个示例中,音乐家结构体包含了一个吉他结构体对象作为其字段之一。这种方式允许您创建包含其他结构体对象的复合数据类型。

十一.Golang的工厂函数

Go语言中没有像一些其他编程语言中常见的构造方法(constructor)的概念。在Go中,对象的创建和初始化通常是通过一个普通的函数完成的,这个函数返回一个指向新创建对象的指针。

一种常见的模式是使用工厂函数(factory function)来创建和初始化对象。这个工厂函数会返回一个新对象的指针,并设置对象的初始状态。以下是一个简单的示例:

package main

import "fmt"

// 定义一个结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// 工厂函数,用于创建和初始化Person对象
func NewPerson(firstName, lastName string, age int) *Person {
    p := Person{
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
    }
    return &p
}

func main() {
    // 使用工厂函数创建Person对象
    person := NewPerson("John", "Doe", 30)
    
    // 访问对象的字段
    fmt.Printf("First Name: %s\n", person.FirstName)
    fmt.Printf("Last Name: %s\n", person.LastName)
    fmt.Printf("Age: %d\n", person.Age)
}

在上面的示例中,NewPerson 函数用于创建和初始化 Person 对象,并返回一个指向该对象的指针。

虽然Go没有构造方法的概念,但工厂函数是一种常见的模式,用于创建和初始化对象。这种模式允许更灵活地进行对象的创建和初始化,而不受构造方法的限制。

11.1.结构体字面量创建对象

在Go语言中,你可以通过以下方式来创建对象:

  1. 使用结构体字面量:

    • 你可以通过结构体字面量来直接创建一个结构体对象。这种方式不需要使用构造方法或工厂函数。
    • 示例:
    package main
    
    import "fmt"
    
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }
    
    func main() {
        person := Person{
            FirstName: "John",
            LastName:  "Doe",
            Age:       30,
        }
        fmt.Printf("First Name: %s\n", person.FirstName)
        fmt.Printf("Last Name: %s\n", person.LastName)
        fmt.Printf("Age: %d\n", person.Age)
    }
    

11.2.使用 new 关键字创建对象:

  • 你可以使用 new 关键字来创建一个指向新对象的指针。这个新对象的字段将被初始化为零值。
  • 示例:
package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    personPtr := new(Person)
    fmt.Printf("First Name: %s\n", personPtr.FirstName) // 输出空字符串
    fmt.Printf("Last Name: %s\n", personPtr.LastName)   // 输出空字符串
    fmt.Printf("Age: %d\n", personPtr.Age)             // 输出0
}

这些方法允许你创建Go中的对象,你可以选择适合你的需求的方式。注意,在Go中,结构体字段的零值是它们的默认值,因此无需显式初始化所有字段。
在Go中,如果您要创建一个指向结构体的指针,您可以使用 new 关键字或直接声明一个结构体指针变量并分配给它一个新的结构体。两者之间的区别是 new 返回的是指向结构体的指针,而直接声明结构体指针变量需要分配一个结构体并返回指针。以下是两种方法的示例:

  1. 使用 new 创建指向结构体的指针:
var InsertLogData *entity.InsertLogData
InsertLogData = new(entity.InsertLogData)

或者可以将声明和分配合并为一行:

InsertLogData := new(entity.InsertLogData)
  1. 直接声明结构体指针变量并分配结构体:
var InsertLogData entity.InsertLogData

上述两种方法都可以创建一个 InsertLogData 结构体的指针,您可以根据需要选择其中一种方法。如果使用第一种方法,您需要显式分配内存,而第二种方法在声明结构体时已经分配了内存。

11.3interface{}万能结构体

在Go语言中,interface{}是一个空接口,它可以表示任何类型的值。当你有一个interface{}类型的变量并且想要根据它的实际类型进行不同的处理时,你可以使用类型断言或者类型选择(type switch)。

类型选择(type switch)是Go中处理interface{}类型变量的一种优雅方式。它允许你根据变量的实际类型来执行不同的代码块。下面是一个使用类型选择(type switch)的示例:

go
package main  
  
import (  
	"fmt"  
)  
  
func main() {  
	var x interface{}  
  
	// 给 x 赋不同的值来演示类型选择  
	x = 42  
	// x = "hello"  
	// x = 3.14  
  
	switch v := x.(type) {  
	case int:  
		fmt.Printf("x is an int: %d\n", v)  
	case string:  
		fmt.Printf("x is a string: %s\n", v)  
	case float64:  
		fmt.Printf("x is a float64: %f\n", v)  
	default:  
		fmt.Printf("x is of a different type\n")  
	}  
}

在上面的示例中,switch语句中的v := x.(type)是一个类型选择表达式。它首先会尝试将x断言为int类型,如果成功,则执行相应的case代码块。如果断言失败,它会继续尝试下一个case,依此类推。如果所有case都不匹配,则会执行default代码块。

请注意,在类型选择中,变量v会被赋予x的实际类型的值,这样你就可以在case代码块中直接使用v了。

类型断言的另一种形式(不使用switch)是这样的:

go
if v, ok := x.(int); ok {  
	fmt.Printf("x is an int: %d\n", v)  
} else if v, ok := x.(string); ok {  
	fmt.Printf("x is a string: %s\n", v)  
} // ... 其他类型检查  
  

// 如果x不是上述任何一种类型,则不会执行任何操作
这种方法使用了两个值的断言(x.(int)),它返回被断言的类型的值和一个布尔值,该布尔值表示断言是否成功。如果断言成功,你可以使用返回的值v;如果断言失败,ok将为false,并且v将是该类型的零值(在这种情况下是int类型的零值0)。然而,这种方法在处理多个类型时不如类型选择(type switch)那么简洁。

十二.Golang包

在Go编程语言中,包(package)是一种用于组织和管理代码的基本单元。包包含了一组相关的Go源代码文件,这些文件可以一起被编译成一个可执行程序或库。包的概念有以下几个关键特点:

  1. 封装:包提供了一种封装代码的方式,允许将相关功能或数据结构组织在一起,并限制外部代码对内部的访问。Go使用大小写来控制标识符的可见性,小写字母开头的标识符只能在包内部访问,而大写字母开头的标识符可以在包外部访问。

  2. 重用:包可以被其他程序或包重用,这有助于促进代码重用和模块化开发。通过导入其他包,你可以轻松地使用其功能而不必重新实现。

  3. 组织:包允许将代码组织成层次结构,有助于维护大型项目。你可以创建自己的包,并将相关的功能和数据结构放在不同的包中,以便更好地组织和管理代码。

  4. 可扩展性:包的设计使得你可以轻松地添加新功能或更改现有功能,而不必影响其他部分的代码。这有助于提高代码的可维护性和可扩展性。

在Go中,标准库本身就是由多个包组成的,例如fmt包用于格式化输入和输出,os包用于操作操作系统功能,net包用于网络编程等等。此外,你也可以创建自己的包来组织和封装你的代码,以便在不同项目中重用。

要创建一个包,你需要在Go源代码文件的顶部使用package关键字指定包的名称,然后将相关的函数、变量和类型定义放在该文件中。然后,你可以使用import语句在其他源代码文件中导入该包,以便在其他地方使用包的功能。包是Go编程中的关键概念,有助于组织、封装和重用代码,从而提高开发效率和代码质量。
在使用包(package)的过程中,有一些注意事项和最佳实践可以帮助你编写更高质量的Go代码,确保包的正确使用和可维护性。以下是一些包注意事项:

  1. 包的命名:包的命名应该具有描述性,并且遵循Go的命名规范。包名通常应该是小写字母,短而有意义。避免使用缩写和不清晰的名称。

  2. 可见性:在包内部,标识符的可见性受大小写规则的限制。以大写字母开头的标识符是可导出的,可以在包外部访问。小写字母开头的标识符是私有的,只能在包内部访问。使用可见性规则来隐藏内部实现细节,提供稳定的外部API。

  3. 文档注释:为包和包内的函数、变量等添加文档注释。文档注释应该清晰地描述功能、用法和示例,以便其他开发者能够理解如何正确使用包。

  4. 循环依赖:避免包之间的循环依赖。循环依赖会导致编译错误,并使代码更难维护。尽量保持包之间的依赖关系清晰,并使用接口来减少依赖关系。

  5. 包的粒度:划分包时,考虑包的粒度。包可以包含多个相关功能,但不要使包过于庞大。合理的包粒度有助于代码的组织和维护。

  6. 版本管理:如果你计划将包发布为开源项目或与其他人共享,考虑使用版本管理工具,如Go Modules,来管理包的版本和依赖关系。

  7. 测试:编写单元测试和集成测试以确保包的正确性。Go提供了一个内置的测试框架,使测试编写变得简单而方便。测试是确保包质量的重要手段。

  8. 性能和效率:在设计包时,要考虑性能和效率。避免不必要的内存分配和计算,尽量减少包的复杂度,以提高代码的执行效率。

  9. 错误处理:良好的错误处理是包的重要组成部分。包应该返回明确的错误信息,并且允许调用方处理错误情况。

  10. 更新和维护:定期更新和维护包,确保它与Go的新版本和依赖项保持兼容。如果不再维护一个包,最好通知用户,或者将包的维护权移交给其他人。

遵循这些注意事项和最佳实践可以帮助你编写可维护、高质量的Go包,并使你的代码更易于理解和共享。

12.1.包管理工具 go module

Go Modules 是 Go 1.11 版本引入的一种依赖管理工具和模块版本管理系统,用于更方便地管理和构建 Go 项目的依赖关系。它解决了旧的 GOPATH 依赖管理方式的一些问题,使 Go 语言项目更加可维护、可移植和可分享。以下是 Go Modules 的一些关键特点和介绍:

  1. 模块(Module):Go Modules 将项目称为模块,每个模块都有一个唯一的模块路径,通常是项目的版本控制仓库地址(例如 GitHub 地址)或自定义路径。模块包含一个或多个 Go 包,并有一个 go.mod 文件来管理模块的依赖和版本信息。

  2. 依赖管理:Go Modules 通过 go.mod 文件来定义和管理项目的依赖关系。这些依赖可以是其他模块,可以指定版本或版本范围,也可以是本地的包。

  3. 版本管理:Go Modules 支持语义版本控制(Semantic Versioning),允许你指定项目依赖的具体版本或版本范围。这有助于确保项目的稳定性,并允许你轻松升级或降级依赖。

  4. 自动化:Go Modules 提供自动化的依赖解析和下载功能。当你导入一个新的依赖包时,Go Modules 会自动下载并添加到 go.mod 文件中,无需手动管理 GOPATH。

  5. Vendor 目录:Go Modules 支持 Vendor 目录,可以使用 go mod vendor 命令将依赖包复制到项目的 Vendor 目录中,以确保项目在不同环境中具有相同的依赖版本。

  6. 可移植性:Go Modules 支持模块路径,这使得项目更容易在不同的版本控制系统和托管平台上共享和移植。

  7. 兼容性:Go Modules 与旧的 GOPATH 方式兼容,这意味着你可以在旧的 GOPATH 项目中使用 Go Modules,或将旧项目迁移到 Go Modules。

  8. 在线和离线支持:Go Modules 支持在线下载依赖,但也支持在没有网络连接时使用已下载的缓存依赖。

  9. 私有仓库支持:Go Modules 支持从私有版本控制仓库(如 GitLab 或 Bitbucket)下载依赖,并提供了身份验证和凭证管理功能。

Go Modules 提供了一种现代化的方式来管理 Go 项目的依赖关系,解决了以前 GOPATH 的一些痛点,并提供了更多的灵活性和可维护性。它是 Go 语言社区的标准做法,如果你在编写新的 Go 项目或升级旧项目,强烈建议使用 Go Modules 来管理依赖。

使用 Go Modules(go mod)管理你的 Go 项目的依赖关系非常简单。以下是一些常见的 go mod 命令和用法,以及它们的说明:

  1. 初始化一个新模块

    在你的项目目录中,运行以下命令以初始化一个新的 Go 模块:

    go mod init <module_name>
    

    这会创建一个 go.mod 文件,其中包含了你的模块信息。

  2. 添加依赖

    使用 go get 命令来添加依赖包。例如,要添加一个名为 “github.com/example/package” 的依赖:

    go get github.com/example/package
    

    这会下载依赖包并将其添加到你的 go.mod 文件中。

  3. 更新依赖

    若要更新依赖包到其最新版本,可以运行:

    go get -u github.com/example/package
    

    这会更新 go.mod 文件中的依赖版本信息。

  4. 查看依赖

    若要查看当前项目的依赖关系,可以运行:

    go list -m all
    

    这会列出当前项目的所有依赖模块及其版本。

  5. 删除依赖

    若要删除一个依赖包,可以运行:

    go get -u github.com/example/package@none
    

    这会将该依赖包标记为 “none” 并从 go.mod 文件中移除。

  6. 下载依赖

    如果你在一个新的环境中,或者项目中没有 go.sum 文件,你可以使用以下命令来下载项目的依赖:

    go mod download
    

    这会根据 go.modgo.sum 文件下载所有依赖。

  7. Vendor 目录

    如果你想使用 Vendor 目录来管理依赖包,可以运行以下命令将依赖包复制到 Vendor 目录中:

    go mod vendor
    

    这将把依赖包复制到项目的 vendor 子目录中。

  8. 清理不使用的依赖

go mod tidy 命令主要用于整理项目的依赖关系,并确保 go.modgo.sum 文件中的依赖列表与你的代码中实际导入的依赖一致。它会删除不再需要的依赖项,但不会主动卸载那些可能仍然在代码中被引用的依赖。所以,你的理解是正确的,go mod tidy 的主要目的是确保依赖列表的一致性,而不是主动卸载不再使用的依赖。如果某个依赖在你的代码中不再被引用,但仍然存在于 go.modgo.sum 中,go mod tidy 可以帮助你将其移除。

如果你想主动删除不再使用的依赖,可以使用 go get -u 命令来指定依赖的特定版本,将其更新为 none,然后运行 go mod tidy。例如:

go get -u github.com/example/old-package@none
go mod tidy

这样,old-package 将被标记为 “none” 并从 go.mod 文件中移除。

go mod tidy

这会删除不再需要的依赖项。

  1. 查看模块信息

    若要查看模块信息,包括模块名称、版本等,可以运行:

    go list -m
    

    这会显示当前模块的信息。

  2. 更改模块路径

    如果你需要更改模块的路径,可以手动编辑 go.mod 文件,并使用 go mod edit 命令来修改模块路径:

    go mod edit -module <new_module_path>
    

这些是一些常见的 go mod 命令和用法,可以帮助你管理和维护你的 Go 项目的依赖关系。使用 Go Modules 可以使依赖管理更加简单和可控,同时提供了版本管理和依赖隔离的好处。

12.1.包调用方法

在 Go 中,你可以通过包名来调用包文件下的方法和函数。这是 Go 语言中的一种常见的方式来组织和访问代码。相当于Java的static方法可以被类调用
假设你有一个名为 mypackage 的包,其中包含一个名为 MyFunction 的函数。要调用这个函数,你可以按照以下步骤进行操作:

  1. 导入包:在你的代码文件中使用 import 语句导入 mypackage 包。

    import "mypackage"
    
  2. 调用函数:然后,你可以使用 mypackage 包名来调用 MyFunction 函数。

    mypackage.MyFunction()
    

这就是通过包名来调用包文件下的方法和函数的方式。在 Go 中,包名通常是小写字母,且要与包文件的目录名一致。要注意,只有被导出的函数(即首字母大写的函数名)可以在包外部访问,非导出的函数只能在包内部使用。所以,确保你要调用的函数在包文件中是可导出的,以便在其他地方使用。

十三.Golang并发编程之协程

在Go编程语言中,协程(goroutine)是一种轻量级的并发执行单元,它由Go运行时(goroutine scheduler)管理,可以在同一个进程中并发运行成千上万个协程。协程是Go语言的一个核心特性,它们提供了一种方便的方式来实现并发编程,使得编写并行代码变得更加简单和高效。

在生产环境中,当使用过多的协程导致CPU性能爆满时,可以通过以下几种方法来控制协程的数量:

协程池(Goroutine Pool):
创建一个固定大小的协程池,用于管理和复用协程。当需要执行新的任务时,从协程池中取出一个空闲的协程来执行。如果协程池已满,则等待有协程空闲或根据策略拒绝新的任务。可以使用sync.Pool或者第三方库如golang.org/x/sync/semaphore来实现协程池。

信号量(Semaphore):
使用信号量来限制同时运行的协程数量。在启动协程之前,先尝试从信号量中获取一个许可(token)。如果信号量中还有许可,则获取成功并启动协程;否则,等待其他协程释放许可或根据策略处理。使用golang.org/x/sync/semaphore包中的Semaphore类型可以方便地实现信号量。

动态调整:
根据系统的负载和性能指标动态地调整协程的数量。可以使用监控工具来实时检测系统的CPU使用率、内存占用、响应时间等指标,并根据这些指标来动态地增加或减少协程的数量。这需要编写相应的逻辑来判断何时增加或减少协程,并确保调整的平滑性和稳定性。

限制任务队列:
如果任务是通过任务队列来分发的,可以限制任务队列的长度来控制并发数量。当任务队列的长度达到某个阈值时,拒绝新的任务进入队列,或者根据策略进行排队等待。这样可以确保不会有过多的任务同时被处理,从而控制协程的数量。

优化代码和算法:
除了控制协程数量外,还可以从代码和算法层面进行优化,减少不必要的计算和内存分配,提高程序的执行效率。例如,避免不必要的循环嵌套、减少内存分配和回收次数、优化数据结构和算法等。

使用调度器:
使用Go语言的调度器(scheduler)来管理协程的执行。Go语言的调度器会自动根据系统的CPU核心数和负载情况来调度协程的执行,确保系统的负载均衡和高效运行。虽然你不能直接控制调度器的行为,但可以通过合理的设计和使用并发原语来影响调度器的决策。

监控和告警:
在生产环境中,建立监控和告警机制来实时监测系统的性能指标和协程数量。当发现协程数量过多或系统负载过高时,及时发出告警并采取相应的措施,如调整协程数量、优化代码等。

需要注意的是,控制协程数量是一个平衡的过程,需要根据具体的业务场景和系统环境进行调整。过多的协程可能会导致系统资源耗尽和性能下降,而过少的协程则可能无法充分利用系统资源。因此,需要根据实际情况进行权衡和选择。

以下是关于Go协程的一些重要概念和特点:

  1. 轻量级:协程是非常轻量级的,一个普通的操作系统线程可以容纳成千上万个协程。这使得协程的创建和销毁非常快速,几乎可以忽略不计的开销。

  2. 并发:协程使得在Go程序中实现并发非常容易。你可以使用go关键字启动一个新的协程,它将在后台并发执行,不会阻塞主线程的执行。

    go someFunction() // 启动一个新的协程并发执行 someFunction
    
  3. 通信通过共享内存:在Go中,协程之间的通信通常通过共享内存的方式实现,但通过使用通道(channel)来保证安全的数据传递。通道是一种用于在协程之间传递数据的机制,它提供了同步的方式来避免竞态条件和数据竞争问题。

  4. 易于使用:Go的协程模型非常容易使用,不需要手动管理线程和锁。Go运行时负责协程的调度和管理,这使得并发编程相对容易且安全。

  5. 可休眠:协程可以通过time.Sleep或通道等方式进行休眠和唤醒,这有助于实现定时操作和协程之间的协同工作。

  6. 无需手动内存管理:Go的垃圾回收器负责管理内存,开发者无需手动释放内存,这降低了并发编程中内存管理方面的复杂性。

  7. 高并发性能:由于协程的轻量级特性以及Go运行时的优化,Go语言在处理高并发任务时表现出色,适用于构建高性能的服务器和并发应用程序。

协程是Go语言的一个重要特性,它们使得并发编程变得更加容易和安全。通过简化并发编程的复杂性,Go语言鼓励开发者编写高效且可维护的并发代码。在Go中,你可以使用协程来处理并行任务、网络通信、I/O操作等各种并发任务。

在你的代码中,你试图使用多个协程模拟抢票的场景,但有一个问题,即你传递的 tocket 变量是按值传递的,这意味着每个协程都操作了它自己的副本,而不是共享同一个计数器。为了模拟多协程抢票,你需要使用Go的并发原语来确保多个协程安全地访问和修改共享状态。

在Go语言中,缓冲管道(buffered channel)和普通管道(unbuffered channel,也称为无缓冲管道)的选择取决于你的并发编程需求。

缓冲管道(buffered channel):

批量处理与异步填充:当你希望发送方能在接收方尚未消费完之前继续发送数据时,可以使用缓冲管道。例如,你可以创建一个固定容量的缓冲管道,用于暂存一段时间内产生的事件或数据,消费者goroutine随后处理这些积累的数据。
解耦并发任务:缓冲管道可以作为一个中间层,解耦生产者和消费者的速度差异,允许生产者更快地生成数据而不需要等待消费者实时处理。
控制流量:缓冲管道的大小可以用来限制流入下游系统的数据速率,起到平滑流量的效果。一旦channel填满,生产者goroutine就会阻塞,从而防止下游系统过载。
避免死锁:在某些情况下,当两个或多个goroutine相互等待对方释放资源时,可能会发生死锁。使用缓冲管道可以减少这种情况的发生,因为即使一个goroutine阻塞在发送或接收操作上,其他goroutine也可以继续执行。
普通管道(unbuffered channel):

简单的数据传递和同步:当你只需要在goroutine之间进行简单的数据传递和同步时,可以使用普通管道。由于普通管道没有缓冲区,因此发送和接收操作是阻塞的,这可以确保数据的顺序性和一致性。
强制同步:在某些情况下,你可能希望强制goroutine之间进行同步操作,以确保数据的一致性和正确性。使用普通管道可以实现这种强制同步,因为发送和接收操作都会阻塞,直到另一方准备好为止。
总结来说,选择缓冲管道还是普通管道主要取决于你的具体需求。如果你需要批量处理、解耦并发任务、控制流量或避免死锁等特性,那么缓冲管道可能是一个更好的选择。而如果你只需要简单的数据传递和同步操作,那么普通管道可能就足够了。

13.1.多协程模拟抢票

使用协程来模拟56个人抢100张票,并输出最后的剩余票数。在这个示例中,我们将使用互斥锁来确保多个协程安全地访问和修改共享的票数变量。

package main

import (
	"fmt"
	"sync"
)

var (
	tickets      = 100
	ticketsMutex sync.Mutex
	wg           sync.WaitGroup
)

func main() {
	// 启动56个协程模拟抢票
	for i := 0; i < 56; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			grabTicket()
		}()
	}

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

	// 输出剩余票数
	fmt.Println("剩余票数:", tickets)
}

func grabTicket() {
	ticketsMutex.Lock()
	defer ticketsMutex.Unlock()

	if tickets > 0 {
		tickets--
		fmt.Printf("抢到一张票,剩余 %d 张票\n", tickets)
	} else {
		fmt.Println("票已抢光")
	}
}

在这个示例中,我们使用了 ticketsMutex 互斥锁来保护共享的 tickets 变量,确保每次只有一个协程可以修改它。每个协程模拟抢票,如果还有票可供抢,就减少票数并输出抢到的结果,如果票已抢光,就输出 “票已抢光”。

通过这种方式,多个协程可以安全地并发抢票,最后输出剩余票数。请注意,实际中可能需要更复杂的逻辑来处理竞争条件和错误情况。

13.1.1通道语法

语法糖(Syntactic Sugar),也称为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

从面向过程到面向对象也是一种语法糖。例如,C语言可以通过其指针、类型转换和结构体实现面向对象的编程风格,但C++更进一步推广了这种风格,使其更加易用。到了C#,面向对象的编程风格得到了充分的发挥。

在编程语言中,常见的语法糖包括迭代器、Lambda表达式、自动类型推导、操作符重载和属性访问器等。这些语法糖通过简化和优化代码的编写方式,用更简练的言语表达较复杂的含义,提高了代码的可读性和可维护性,使编程更加高效。

需要注意的是,“语法糖”这个词并非贬义词,它给编程带来了方便,是一种便捷的写法,编译器会帮程序员做转换,而且在性能上也不会带来损失。

package main

import "fmt"

func main() {

	//var msg chan string
	//这样会死锁堵塞,
	msg := make(chan string, 0)
	//因为变量塞进去了没有别的协程读,就堵塞了,不会进行到下一步
	msg <- "hello world"

	data := msg

	fmt.Println(data)
}

package main

import (
	"fmt"
	"time"
)

func main() {

	//var msg chan string
	//这样会死锁堵塞,
	//msg := make(chan string, 0)
	因为变量塞进去了没有别的协程读,就堵塞了,不会进行到下一步
	//msg <- "hello world"

	//data := msg
	//
	//fmt.Println(data)

	//解决办法
	//go 有一种head-before机制
	msg := make(chan string, 0)
	go func(msg chan string) {

		data := <-msg
		fmt.Println(data)
	}(msg)

	//因为变量塞进去了没有别的协程读,就堵塞了,不会进行到下一步
	msg <- "hello world"
	//waitgroup如果少了done调用,容易出现deadlock,无缓冲的channel也容易出现死锁,需要单独起一个协程预先消费
	time.Sleep(time.Second * 10)

}

在Go语言中,通道的创建、发送和接收操作都有特定的语法。以下是通道的基本语法:

  1. 创建通道

    • 使用make函数创建通道,指定通道可以传递的数据类型和可选的缓冲容量。通道的类型是chan加上数据类型。
    • 示例:ch := make(chan int) 创建一个传递整数的无缓冲通道。
  2. 发送数据到通道

    • 使用<-运算符将数据发送到通道。
    • 示例:ch <- 42 将整数42发送到通道ch中。
  3. 接收数据从通道

    • 使用<-运算符从通道接收数据,并将其赋值给一个变量。
    • 示例:x := <-ch 从通道ch中接收数据,并将其存储在变量x中。
  4. 关闭通道

    • 使用close函数关闭通道,以表示不再向通道发送数据。
    • 示例:close(ch) 关闭通道ch
  5. 通道的选择语句

    • 使用select语句来在多个通道之间选择操作,可以等待多个通道中的任何一个完成操作。
    • 示例:
      select {
      case x := <-ch1:
          // 从 ch1 接收数据
      case ch2 <- 42:
          // 向 ch2 发送数据
      case <-ch3:
          // 接收数据并忽略
      }
      
  6. 无缓冲通道

    • 无缓冲通道在创建时指定容量为0,发送操作和接收操作是同步的。
    • 示例:ch := make(chan int) 创建一个无缓冲通道。
  7. 缓冲通道

    • 缓冲通道在创建时指定一个正整数的容量,允许在通道满或空时引入异步性。
    • 示例:ch := make(chan int, 10) 创建一个容量为10的缓冲通道。
  8. 通道的类型

    • 通道的类型包括发送操作符<-和接收操作符<-,以及通道中传递的数据类型。例如,chan int 表示一个传递整数的通道。

请注意,通道的发送和接收操作是阻塞的,除非有其他协程准备好接收或发送数据。通道是Go语言中非常重要的并发编程工具,用于协程之间的数据同步和通信。

13.1.2缓冲通道和无缓冲通道

在Go语言中,通道可以分为两种主要类型:无缓冲通道(unbuffered channel)和缓冲通道(buffered channel)。这两种通道类型在用途和行为上有重要的区别:

  1. 无缓冲通道(Unbuffered Channel)

    • 无缓冲通道不保存任何数据,它的容量为0。
    • 发送到无缓冲通道的数据会立即被接收者接收,发送和接收操作是同步的。
    • 当发送者尝试发送数据到无缓冲通道时,会阻塞直到有其他协程准备好接收数据。
    • 当接收者尝试从无缓冲通道接收数据时,会阻塞直到有其他协程准备好发送数据。
    • 无缓冲通道用于协程之间的数据同步和通信,它们强制发送和接收协程同步等待对方。
  2. 缓冲通道(Buffered Channel)

    • 缓冲通道具有指定的容量,大于0。
    • 发送到缓冲通道的数据会被存储在通道中,直到通道满时才会阻塞发送操作。
    • 接收者可以在通道非空时接收数据,否则会阻塞。
    • 缓冲通道允许在发送和接收之间引入异步性,发送者和接收者之间的时间解耦。
    • 缓冲通道通常用于限制通信的速度或在生产者和消费者之间平衡负载。

下面是示例代码,演示了无缓冲通道和缓冲通道的不同行为:

package main

import "fmt"

func main() {
	// 无缓冲通道
	unbufferedChan := make(chan int)
	go func() {
		unbufferedChan <- 42 // 阻塞,直到另一个协程接收数据
	}()
	result := <-unbufferedChan // 阻塞,直到数据被发送
	fmt.Println("Unbuffered Channel Result:", result)

	// 缓冲通道
	bufferedChan := make(chan int, 3) // 缓冲容量为3
	bufferedChan <- 1
	bufferedChan <- 2
	bufferedChan <- 3
	// bufferedChan <- 4 // 阻塞,通道已满
	fmt.Println("Buffered Channel Result:", <-bufferedChan)
}

在这个示例中,无缓冲通道和缓冲通道的行为进行了比较。无缓冲通道要求发送和接收操作同步进行,而缓冲通道允许发送者和接收者异步工作,只有当通道满或空时才会阻塞。

你可以根据具体的需求选择使用无缓冲通道或缓冲通道,以满足并发程序的要求。无缓冲通道通常用于同步和控制协程之间的执行,而缓冲通道则适用于平衡生产者和消费者之间的速度,或者在协程之间传递一定量的数据。

13.1.3堵塞和加锁的概念

堵塞和加锁是相关但不完全相同的概念。

  1. 堵塞(Blocking)

    • 堵塞指的是程序的某个部分或操作因等待某种条件的满足而停止执行,直到条件满足为止。
    • 在并发编程中,当一个协程试图执行某个操作(如发送数据到通道或接收数据),但条件不满足时,它可能会被堵塞,即卡住不继续执行,直到条件满足。
    • 通常,堵塞可以通过等待条件的满足、定时超时或其他方式来解除。
  2. 加锁(Locking)

    • 加锁是一种机制,用于在多个协程之间控制对共享资源的访问,以避免竞态条件和数据竞争。
    • 通常,加锁会使用互斥锁(Mutex)或其他同步原语来确保在任何给定时间只有一个协程能够访问共享资源。其他协程在尝试访问资源时会被阻塞,直到锁被释放。
    • 加锁的目的是确保数据的安全性和一致性,以防止多个协程同时修改共享数据。

虽然堵塞和加锁都涉及阻止某些操作的进行,但堵塞通常与等待某些条件有关,而加锁是一种更通用的同步机制,用于管理共享资源的访问。在并发编程中,加锁通常用于解决竞态条件和数据竞争,以确保多个协程之间的协作和数据一致性。

13.2WaitGroup介绍(保证全部协程一起执行完毕)

没有WaitGroup时候,当main主协程结束之后,其他协程不管有没有执行完都一起结束,而使用WaitGroup时候,他相当于一个瓶子,开启协程瓶子里面添加石头,协程结束瓶子去掉石头,当瓶子为空也就是全部协程处理完毕时候结束程序。
在Go编程语言中,“WaitGroup”(等待组)是一个用于协调多个Goroutine(Go语言中的轻量级线程)的同步机制。WaitGroup用于等待一组Goroutine完成其工作,然后再继续执行主线程或其他任务。通常,它用于确保所有Goroutine都已完成,然后再进行下一步操作。

WaitGroup属于标准库sync包,主要包含三个方法:

  1. Add(delta int):用于向WaitGroup添加等待的计数。每次调用Add方法会增加等待的计数,通常在启动新的Goroutine之前使用。delta参数指定要添加的计数值,可以是正数也可以是负数。

  2. Done():用于通知WaitGroup一个Goroutine已完成其工作。每次调用Done方法会减少等待的计数。通常,在Goroutine的工作完成后调用此方法。

  3. Wait():用于阻塞当前Goroutine,直到等待的计数归零。一旦计数为零,Wait方法将返回,允许程序继续执行。

以下是一个简单的示例,演示了如何在Go中使用WaitGroup:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 在函数退出时通知WaitGroup工作已完成
    fmt.Printf("Worker %d starting\n", id)
    // 模拟工作
    // ...
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // 启动一个Goroutine前增加等待计数
        go worker(i, &wg)
    }

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

    fmt.Println("All workers have finished")
}

在这个示例中,我们创建了三个Goroutine并使用Add方法增加等待计数,然后在每个Goroutine完成工作时调用Done方法。最后,我们使用Wait方法来等待所有的Goroutine完成。这确保了在"所有工人都完成"之前,main函数不会继续执行。

13.3并发编程runtime包下的API

runtime 包是Go语言标准库中的一个重要包,它提供了与Go程序的运行时环境和控制相关的功能。这个包包含了许多底层的函数和方法,用于管理Goroutines(Go语言的轻量级线程)、内存分配、垃圾回收和程序的底层执行。

以下是一些常见的runtime包中的功能和函数:

  1. Goroutine 控制:

    • go 函数:用于启动新的Goroutines。
    • Gosched 函数:用于手动将当前Goroutine切换给其他等待执行的Goroutine。
    • NumGoroutine 函数:返回当前运行的Goroutine数量。
  2. 内存分配和垃圾回收:

    • GC 函数:手动触发垃圾回收。
    • MemStats 结构体:包含了运行时的内存统计信息,可以通过ReadMemStats函数获取。
    • SetFinalizer 函数:用于注册一个对象的终结器函数,以在垃圾回收时执行特定操作。
  3. 堆栈控制:

    • GODEBUG 环境变量:用于调整Goroutine调度和堆栈跟踪的调试参数。
    • Stack 函数:用于获取Goroutine的堆栈信息。
  4. 程序控制:

    • LockOSThreadUnlockOSThread 函数:用于将Goroutine绑定到当前OS线程,以控制其执行位置。
    • Goexit 函数:用于终止当前Goroutine的执行。
  5. 调试和追踪:

    • CgoCheckUnknownFptr 变量:用于控制Cgo调用中未知函数指针的检查。
    • CgoEnabled 变量:用于控制是否启用Cgo调用。

runtime 包通常用于处理与并发编程、性能优化和低级系统交互相关的任务。它提供了一些高级的功能,但也需要谨慎使用,因为它们可能会影响程序的行为和性能。一般来说,大多数Go程序不需要直接使用 runtime 包,因为Go的并发模型和垃圾回收已经在底层进行了良好的管理。
理解 runtime 包的每个方法的使用范例需要具体的情境和需求。这里我将为你提供一些常见方法的简单示例,以便更好地理解它们的功能。

  1. go 函数 - 启动新的Goroutines:
package main

import (
	"fmt"
	"runtime"
)

func main() {
	runtime.GOMAXPROCS(2) // 设置同时运行的CPU核心数量
	for i := 0; i < 5; i++ {
		go func(n int) {
			fmt.Println("Goroutine", n)
		}(i)
	}
	// 等待所有Goroutines完成
	var input string
	fmt.Scanln(&input)
}
  1. GC 函数 - 手动触发垃圾回收:
package main

import (
	"fmt"
	"runtime"
)

func main() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("Memory Alloc = %v MiB\n", m.Alloc/1024/1024)
	runtime.GC() // 手动触发垃圾回收
	runtime.ReadMemStats(&m)
	fmt.Printf("Memory Alloc = %v MiB\n", m.Alloc/1024/1024)
}
  1. SetFinalizer 函数 - 注册终结器函数:
package main

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

type MyStruct struct {
	Data []byte
}

func main() {
	obj := &MyStruct{Data: make([]byte, 100)}
	runtime.SetFinalizer(obj, func(obj *MyStruct) {
		fmt.Println("Finalizing MyStruct")
	})
	obj = nil // 此时 MyStruct 对象将被垃圾回收
	// 等待一段时间以触发垃圾回收
	time.Sleep(time.Second)
}

这些示例演示了如何使用 runtime 包的一些常见方法,但请注意,这只是基本用法。实际使用中,需要根据具体的需求和情境来合理使用这些方法。此外,某些方法(如手动触发垃圾回收)通常在特殊情况下才会使用,而不是在通常的应用程序中。
继续讨论 runtime 包的方法的使用范例:

  1. Stack 函数 - 获取Goroutine的堆栈信息:
package main

import (
	"fmt"
	"runtime"
)

func printStack() {
	buf := make([]byte, 1024)
	n := runtime.Stack(buf, false)
	fmt.Printf("Stack trace:\n%s\n", buf[:n])
}

func main() {
	go printStack()
	// 做一些工作...
	var input string
	fmt.Scanln(&input)
}

在这个示例中,我们通过 runtime.Stack 函数获取了当前Goroutine的堆栈信息,然后打印出来。这在调试和诊断时非常有用。

  1. LockOSThreadUnlockOSThread 函数 - 控制Goroutine绑定到OS线程:
package main

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

func worker() {
	runtime.LockOSThread()   // 将当前Goroutine绑定到OS线程
	defer runtime.UnlockOSThread()
	// 执行一些需要线程控制的任务
	fmt.Println("Worker is running on OS thread:", runtime.GoroutineID())
	time.Sleep(time.Second)
}

func main() {
	go worker()
	time.Sleep(time.Second * 2)
}

在此示例中,LockOSThreadUnlockOSThread 函数用于控制Goroutine是否绑定到OS线程。这对于需要控制Goroutine的执行位置和线程上下文的特殊情况很有用。

  1. GODEBUG 环境变量 - 调整Goroutine调度和堆栈跟踪的调试参数:
GODEBUG=gctrace=1,schedtrace=1000 go run yourprogram.go

这个示例展示了如何使用 GODEBUG 环境变量来启用垃圾回收跟踪和调度跟踪,以诊断程序的性能问题。根据具体的需求,你可以在不同的调试场景下设置不同的 GODEBUG 值。

这些示例涵盖了一些 runtime 包方法的常见用法。请根据你的具体需求和项目要求,合理使用这些方法以优化和管理Go程序。这些方法通常用于处理并发、性能优化和底层系统交互等任务。

13.3.1.runtime.gosched()

runtime.gosched() 是 Go 编程语言中的一个函数,它的作用是主动让出当前 goroutine 的执行权限,让其他 goroutine 有机会执行。在 Go 中,调度器会负责管理 goroutine 的执行,但有时候你可能希望显式地让出执行权限,以避免某个 goroutine 长时间占用 CPU,或者为了更好地平衡并发执行。

通常情况下,Go 的调度器会自动进行调度,而不需要显式调用 runtime.gosched()。但在某些特殊情况下,你可能会需要使用它,例如在一个长时间运行的计算任务中,你可以周期性地调用 runtime.gosched() 来确保其他 goroutine 有机会执行,以避免某个 goroutine 长时间独占 CPU 资源。

这个函数的用法比较少见,一般情况下不需要显式调用它,因为 Go 的调度器通常会很好地管理 goroutine 的执行。只有在特殊需求下才会使用它。如果你不确定是否需要使用 runtime.gosched(),最好先考虑其他并发控制机制,例如使用互斥锁或通道来协调 goroutine 之间的执行。
runtime.gosched() 通常在以下情况下使用:

  1. 长时间运行的计算任务中的协作: 如果你有一个长时间运行的计算任务,为了避免它占用太多 CPU 时间,你可以在适当的时候调用 runtime.gosched(),以确保其他 goroutine 有机会执行。这可以帮助你平衡并发执行,防止某个 goroutine 长时间独占 CPU。

    func longRunningTask() {
        // 长时间运行的计算任务
        for {
            // 做一些计算工作
            // ...
    
            // 让出执行权限给其他 goroutine
            runtime.Gosched()
        }
    }
    
  2. 实验性代码和调试: 在某些情况下,你可能需要调试或测试一些涉及并发的代码,使用 runtime.gosched() 可能有助于产生一些特定的并发条件以进行调试或测试。

  3. 特殊需求的并发控制: 在某些情况下,你可能需要在自己的代码中实现特殊的并发控制策略,而 runtime.gosched() 可能是其中的一部分。

需要注意的是,在大多数情况下,不需要显式地调用 runtime.gosched(),因为 Go 的调度器会自动管理 goroutine 的执行。而且,滥用 runtime.gosched() 可能会导致性能问题,因此应该谨慎使用,只在确实需要时使用。在一般情况下,使用 Go 提供的并发控制机制,如互斥锁、通道等,通常更加安全和高效。

13.3.2.goexit()

“goexit” 通常是指 Go 编程语言(通常称为 Golang)中的一个函数或命令。在 Go 中,“goexit” 是用于终止 Goroutine 的函数,Goroutine 是 Go 中的并发执行单元。

以下是 “goexit” 在 Go 中的用法的简要解释:

  1. Goroutine:Goroutine 是 Go 中的轻量级线程,允许并发执行代码。它们是 Go 并发模型的重要组成部分。

  2. goexit 函数:“goexit” 函数是 Go 的运行时包(runtime)中的内部函数,用于终止 Goroutine。通常情况下,程序员不会直接在代码中使用它,而是在 Goroutine 完成执行时隐式调用它。

  3. 隐式使用:当 Goroutine 完成工作或到达执行的末尾时,“goexit” 函数会自动调用。这允许清理与 Goroutine 相关的资源,并使 Goroutine 优雅地退出。

以下是一个带有 Goroutine 的简单 Go 程序示例:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {
        fmt.Println("Hello from goroutine!")
        time.Sleep(time.Second)
    }
}

func main() {
    go sayHello() // 启动一个新的 Goroutine
    time.Sleep(3 * time.Second)
    fmt.Println("主函数退出。")
}

在此示例中,使用 go sayHello() 创建了一个新的 Goroutine,在 sayHello 函数内部,当它完成执行时,Goroutine 将隐式调用 “goexit”。

请注意,“goexit” 不是您通常会直接在 Go 程序中使用的函数或命令。它是 Go 运行时的一部分,并由 Go 调度程序管理。

13.3.3.untime.GOMAXPROCS(控制cpu数量高科技真的快)

runtime.GOMAXPROCS() 是 Go 编程语言中的一个函数,用于设置可同时执行的操作系统线程数(也称为操作系统线程的最大数量)。这个函数可以用来控制 Go 程序中的并发性。

在 Go 中,每个 Goroutine 都可以在操作系统的线程上运行。runtime.GOMAXPROCS() 允许您显式地设置可以同时运行 Goroutine 的线程数量。默认情况下,Go 会根据计算机的 CPU 核心数量自动设置这个值,以充分利用多核 CPU。

函数签名如下:

func GOMAXPROCS(n int) int
  • n 参数是一个整数,表示要设置的最大操作系统线程数量。
  • 函数返回之前的最大操作系统线程数量。

这个函数在需要微调并发性能的情况下非常有用。通过增加线程数量,可以让程序更好地利用多核 CPU,从而加速并发执行。但是,过多的线程数量可能会导致竞争和资源争夺,因此需要谨慎使用。

以下是一个示例,演示如何使用 runtime.GOMAXPROCS() 设置最大操作系统线程数:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// 获取当前的最大操作系统线程数
	oldMaxProcs := runtime.GOMAXPROCS(0)
	fmt.Printf("原始最大操作系统线程数:%d\n", oldMaxProcs)

	// 设置新的最大操作系统线程数(这里设置为2)
	newMaxProcs := runtime.GOMAXPROCS(2)
	fmt.Printf("新的最大操作系统线程数:%d\n", newMaxProcs)

	// 获取当前的最大操作系统线程数
	currentMaxProcs := runtime.GOMAXPROCS(0)
	fmt.Printf("当前最大操作系统线程数:%d\n", currentMaxProcs)
}

请注意,runtime.GOMAXPROCS() 的具体效果取决于您的硬件和操作系统,并不是所有情况下都能明显提高性能。通常情况下,Go 的调度器会自动管理线程,而不需要手动设置。但在某些特定情况下,手动调整线程数可能有助于优化程序的性能。

13.4.协程同步(处理线程安全问题)

13.4.1sync.Mutex方法

sync.Mutex 是 Go 编程语言标准库 sync 包中的一种同步原语,用于实现互斥锁(Mutex),以保护共享资源免受多个 Goroutine 的并发访问,保证单个协程的顺序处理。Mutex 是一种常用的同步机制,用于确保在任何给定时刻只有一个 Goroutine 能够访问临界区(共享资源)。

以下是 sync.Mutex 的基本用法:

  1. 创建 Mutex:

    var mu sync.Mutex
    
  2. 锁定(Lock):使用 mu.Lock() 来锁定 Mutex,这将阻止其他 Goroutine 进入临界区。

    mu.Lock()
    // 访问或修改共享资源的代码
    mu.Unlock() // 解锁 Mutex
    
  3. 解锁(Unlock):使用 mu.Unlock() 来解锁 Mutex,允许其他 Goroutine 进入临界区。

下面是一个示例,演示了如何使用 sync.Mutex 来保护一个共享的计数器:

package main

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

func main() {
	var mu sync.Mutex
	var count int

	// 启动多个 Goroutine 来增加计数器的值
	for i := 0; i < 5; i++ {
		go func() {
			mu.Lock()         // 锁定 Mutex
			defer mu.Unlock() // 在函数退出时解锁 Mutex

			count++           // 访问共享资源
			fmt.Println(count)
		}()
	}

	// 等待一段时间以确保所有 Goroutine 完成
	time.Sleep(time.Second)
}

在这个示例中,我们创建了一个 Mutex (mu) 来保护 count 变量。多个 Goroutine 并发地尝试访问和修改 count 变量,但由于 Mutex 的保护,只有一个 Goroutine 能够进入临界区,从而避免了竞态条件。

请注意,使用 Mutex 来保护共享资源是一种确保线程安全的方法,但要小心避免死锁(多个 Goroutine 互相等待锁)和过多的锁竞争(性能问题)。因此,使用 Mutex 需要谨慎考虑和设计。

在Go语言中,标准库中的sync包提供了并发原语,但直接的“加读锁”并不是通过单一的原语来实现的。通常,我们会使用sync.RWMutex(读写互斥锁)来实现对共享资源的读写访问控制,其中多个读操作可以同时进行,但写操作会阻塞其他读和写操作。

以下是一个使用sync.RWMutex的示例,该示例展示了如何在并发环境中安全地读写共享数据:

go
package main  
  
import (  
	"fmt"  
	"sync"  
	"time"  
)  
  
// SharedData 是一个包含互斥锁和共享数据的结构体  
type SharedData struct {  
	mu     sync.RWMutex  
	values map[string]int  
}  
  
// Set 是一个写入方法,它使用Lock来确保写操作的原子性  
func (sd *SharedData) Set(key string, value int) {  
	sd.mu.Lock() // 加写锁  
	defer sd.mu.Unlock() // 释放写锁(使用defer确保即使在函数返回前也会解锁)  
	sd.values[key] = value  
}  
  
// Get 是一个读取方法,它使用RLock来允许多个读操作并发进行  
func (sd *SharedData) Get(key string) (int, bool) {  
	sd.mu.RLock() // 加读锁  
	defer sd.mu.RUnlock() // 释放读锁(同样使用defer)  
	value, ok := sd.values[key]  
	return value, ok  
}  
  
func main() {  
	// 初始化共享数据和goroutines的等待组  
	data := &SharedData{values: make(map[string]int)}  
	var wg sync.WaitGroup  
  
	// 启动多个写goroutine  
	for i := 0; i < 5; i++ {  
		wg.Add(1)  
		go func(id int) {  
			defer wg.Done()  
			data.Set(fmt.Sprintf("key%d", id), id*10)  
		}(i)  
	}  
  
	// 等待写操作完成  
	wg.Wait()  
  
	// 启动多个读goroutine  
	for i := 0; i < 10; i++ {  
		wg.Add(1)  
		go func(id int) {  
			defer wg.Done()  
			value, ok := data.Get(fmt.Sprintf("key%d", id%5)) // 读取key0到key4  
			if ok {  
				fmt.Printf("Goroutine %d read value: %d\n", id, value)  
			}  
		}(i)  
	}  
  
	// 等待读操作完成(只是为了演示,实际上读操作可能会在其他地方完成)  
	wg.Wait()  
  
	// 如果需要,可以在这里打印最终的数据状态,但通常这不是必要的  
	// fmt.Println(data.values)  
  
	// 模拟程序运行一段时间以确保所有goroutine都有机会执行  
	time.Sleep(time.Second)  
}
在上面的示例中,我们定义了一个SharedData结构体,它包含一个sync.RWMutex和一个map来存储键值对。Set方法用于设置键值对,并在操作前加写锁,操作后释放写锁。Get方法用于获取键对应的值,并在操作前加读锁,操作后释放读锁。

通过sync.WaitGroup,我们可以等待所有的写操作完成后才开始读操作(尽管在实际应用中,读写操作可能是交错的)。time.Sleep用于模拟程序运行一段时间,以确保所有goroutine都有机会执行,但这并不是并发控制的一部分,而只是为了演示。

请注意,虽然读锁允许多个读操作同时进行,但写操作会阻塞所有其他读和写操作,直到写操作完成并释放写锁为止。这是读写锁的一个基本特性,用于确保数据的一致性。

13.4.2channel的遍历

在 Go 编程语言中,可以使用 for 循环来遍历通道(channel)中的元素。遍历通道通常需要结合使用 range 关键字来实现。以下是如何遍历通道的一般方法:

package main

import (
	"fmt"
)

func main() {
	// 创建一个通道
	ch := make(chan int)

	// 启动一个 Goroutine 向通道发送数据
	go func() {
		for i := 1; i <= 5; i++ {
			ch <- i // 发送数据到通道
		}
		close(ch) // 关闭通道
	}()

	// 遍历通道中的数据
	for value := range ch {
		fmt.Println(value)
	}
}

在这个示例中,我们首先创建了一个整数类型的通道 ch,然后启动了一个单独的 Goroutine 来向通道发送数据。在主 Goroutine 中,我们使用 for value := range ch 的形式来遍历通道中的数据。这种方式会一直循环,直到通道被关闭。当通道被关闭后,range 循环会自动退出。

需要注意的是,当通道被关闭后,仍然可以从通道中读取剩余的数据,直到通道中的所有数据都被读取完为止。关闭通道是为了告知接收方已经没有更多的数据需要接收。

如果不关闭通道,遍历通道时可能会导致程序死锁,因为 range 循环会一直等待新数据的到来,而发送数据的 Goroutine 不会结束。

另外,还可以使用通道的 select 语句来遍历通道并同时处理其他操作,这对于多个通道的并发操作非常有用。例如:

for {
    select {
    case value, ok := <-ch:
        if !ok {
            // 通道已关闭
            return
        }
        fmt.Println(value)
    // 可以添加其他 case 语句来处理其他操作
    }
}

要使用 for 循环来遍历通道,可以结合 select 语句和通道的关闭状态来实现。以下是一个使用 for 循环遍历通道的示例:

package main

import (
	"fmt"
)

func main() {
	// 创建一个通道
	ch := make(chan int)

	// 启动一个 Goroutine 向通道发送数据
	go func() {
		for i := 1; i <= 5; i++ {
			ch <- i // 发送数据到通道
		}
		close(ch) // 关闭通道
	}()

	// 使用 for 循环遍历通道
	for {
		value, ok := <-ch
		if !ok {
			// 通道已关闭
			break
		}
		fmt.Println(value)
	}
}

在这个示例中,我们创建了一个通道 ch,启动了一个 Goroutine 来向通道发送数据,然后使用无限循环 for 来不断尝试读取通道中的数据。当通道被关闭时,读取操作将返回零值和 falseok 变量为 false),这时我们退出循环。

这种方式可以用来遍历通道,即使通道在未来可能会有更多的数据被发送。但请注意,这种方法需要小心处理,以确保不会永久阻塞程序。通常情况下,使用 range 关键字来遍历通道更为安全和便捷,因为它会在通道关闭后自动退出循环。只有在特殊情况下才需要使用无限循环来遍历通道。

这种方式允许你同时处理多个通道的数据或执行其他操作,以响应不同的事件。

13.4.3.协程死锁

协程(Goroutine)死锁是指在多个协程之间发生了互相等待的情况,导致程序无法继续执行下去。这种情况通常发生在并发程序中,其中一个或多个协程试图获取锁或等待某个资源,但这些资源却不可用或被其他协程占用,从而导致所有涉及的协程都无法继续执行。

以下是一些可能导致协程死锁的常见情况和原因:

  1. 相互等待锁:两个或多个协程试图获取彼此持有的锁,导致互相等待。例如,协程 A 持有锁 L1,协程 B 持有锁 L2,并且它们都试图获取对方持有的锁。

  2. 资源耗尽:如果程序中的协程不释放资源(如通道或文件句柄),那么其他协程可能会因为无法获取这些资源而陷入死锁状态。

  3. 无限等待:协程可能陷入无限等待某个事件的情况,例如等待通道关闭但通道不会关闭,或者等待特定条件的变化但条件不会改变。

下面是一个示例,演示了两个协程之间的死锁情况:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	// 协程1:向通道发送数据
	wg.Add(1)
	go func() {
		defer wg.Done()
		ch <- 42
	}()

	// 协程2:从通道接收数据
	wg.Add(1)
	go func() {
		defer wg.Done()
		value := <-ch
		fmt.Println(value)
	}()

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

	// 此时会发生死锁,因为协程2试图从通道接收数据,但协程1没有释放通道
}

在这个示例中,协程1向通道发送数据,而协程2尝试从通道接收数据。由于协程1没有完成发送操作,协程2会一直等待,而协程1也无法完成发送操作,因为没有其他协程来接收数据。这就导致了死锁情况。

要避免协程死锁,通常需要仔细规划并发程序的逻辑,确保协程能够正常地获取和释放资源,以及合理处理竞态条件。使用 select 语句、互斥锁(Mutex)等同步机制可以帮助解决协程死锁的问题。

13.4.4.select switch操作

在 Go 编程语言中,select 语句用于处理多个通道操作,它可以让程序在多个通道之间进行非阻塞选择操作。类似于 switch 语句,但专用于通道操作。以下是 select 语句的基本用法:

select {
case value1 := <-channel1:
    // 从channel1接收到数据时执行的操作
case value2 := <-channel2:
    // 从channel2接收到数据时执行的操作
case channel3 <- data:
    // 向channel3发送数据时执行的操作
default:
    // 如果没有通道操作就绪时执行的默认操作
}
  • select 语句会等待其中的任何一个通道操作就绪。一旦有通道操作就绪,就会执行对应的操作。
  • case 语句中可以是通道接收操作、通道发送操作,或其他通用的表达式。
  • 如果有多个 case 语句就绪,Go 会随机选择一个执行(如果你需要确定的顺序,可以使用互斥锁等同步机制来实现)。
  • 如果没有任何 case 语句就绪,那么就会执行 default 分支(如果存在的话)。

下面是一个示例,演示了 select 语句的用法:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- 42
	}()

	go func() {
		time.Sleep(1 * time.Second)
		ch2 <- 23
	}()

	select {
	case value := <-ch1:
		fmt.Println("Received from ch1:", value)
	case value := <-ch2:
		fmt.Println("Received from ch2:", value)
	default:
		fmt.Println("No channel operation ready")
	}
}

在这个示例中,我们有两个协程分别向 ch1ch2 通道发送数据。select 语句会等待第一个通道操作就绪,然后执行对应的操作。因为 ch2 的数据先就绪,所以会执行 ch2 分支。

select 语句通常用于处理多个并发操作,例如等待多个通道中的数据、实现超时操作等。它是 Go 中处理并发编程中的重要工具之一。

13.4.5.Timer使用

在 Go 编程语言中,time.Timer 是标准库 time 包中的一种定时器,用于在指定的时间段之后触发一个事件。time.Timer 可以用于执行一次性的延时操作,也可以用于在定时间隔内周期性地触发事件。

以下是 time.Timer 的基本用法:

  1. 创建 Timer:使用 time.NewTimer(d) 来创建一个新的定时器,其中 d 是一个 time.Duration 表示的时间段,指定了多长时间后触发事件。创建后的定时器是未激活的,需要通过 <-timer.C 来等待定时器的到期。

  2. 等待定时器到期:通过 <-timer.C 可以等待定时器的到期。这是一个阻塞操作,直到定时器到期,它会返回一个用于触发定时器的通道(timer.C)的值。

  3. 重置定时器:如果需要重新使用定时器,可以使用 timer.Reset(d) 方法将其重置为一个新的时间段 d。这将取消先前的到期操作,并重新计时。

  4. 停止定时器:如果不再需要定时器,可以使用 timer.Stop() 方法停止它,以释放资源。注意,停止后的定时器不能再次启动。

下面是一个示例,演示了如何使用 time.Timer 来实现延时操作:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个定时器,设置触发时间为2秒后
	timer := time.NewTimer(2 * time.Second)

	fmt.Println("Waiting for the timer to expire...")
	<-timer.C // 等待定时器到期

	fmt.Println("Timer expired!")
}

在这个示例中,我们创建了一个定时器 timer,设置了它的触发时间为2秒后,然后使用 <-timer.C 等待定时器的到期。一旦定时器到期,程序将打印 “Timer expired!”。

需要注意的是,如果只需要进行一次性的延时操作,建议使用 time.After 函数,而不是显式创建一个 time.Timertime.After 返回一个通道,在指定时间段后会自动关闭,从而触发事件,更加方便。而 time.Timer 更适合需要手动控制定时器的情况,例如需要在定时器到期前取消或重新计时。

13.4.6.Ticker使用

time.Ticker 是 Go 标准库中的一个类型,用于定期触发事件,类似于周期性的定时器。Ticker 可以用于反复触发一个事件,例如在每隔一段固定的时间执行某个任务。下面是 time.Ticker 的基本用法:

  1. 创建 Ticker:使用 time.NewTicker(d) 来创建一个新的 Ticker,其中 d 是一个 time.Duration 表示的时间段,指定了触发事件的时间间隔。

  2. 定期触发事件Ticker 返回一个通道 (ticker.C),在每个时间间隔结束时都会向该通道发送一个时间值。您可以使用 for range 循环来监听这个通道,以执行定期的操作。

  3. 停止 Ticker:如果不再需要 Ticker,可以使用 ticker.Stop() 方法来停止它,以释放资源。停止后的 Ticker 不能再次启动。

下面是一个示例,演示了如何使用 time.Ticker 来定期触发事件:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个每秒触发一次的 Ticker
	ticker := time.NewTicker(1 * time.Second)

	// 启动一个 Goroutine 来监听 Ticker
	go func() {
		for {
			select {
			case <-ticker.C:
				fmt.Println("Ticker triggered at", time.Now())
				// 在这里执行定期的操作
			}
		}
	}()

	// 等待一段时间,以便观察 Ticker 触发
	time.Sleep(5 * time.Second)

	// 停止 Ticker
	ticker.Stop()
	fmt.Println("Ticker stopped.")
}

在这个示例中,我们创建了一个每秒触发一次的 Ticker,并在一个单独的 Goroutine 中监听 Ticker。每次 Ticker 触发时,它会向通道 ticker.C 发送一个时间值,然后我们在 select 语句中监听并执行定期的操作。

需要注意的是,一旦不再需要 Ticker,我们使用 ticker.Stop() 来停止它,以释放资源。在示例中,我们等待了一段时间,然后停止了 Ticker,以演示如何停止定期触发。如果不停止 Ticker,它将一直运行下去,可能导致资源泄漏。
time.Ticker 是 Go 标准库中的一个类型,用于定期触发事件,类似于周期性的定时器。Ticker 可以用于反复触发一个事件,例如在每隔一段固定的时间执行某个任务。下面是 time.Ticker 的基本用法:

  1. 创建 Ticker:使用 time.NewTicker(d) 来创建一个新的 Ticker,其中 d 是一个 time.Duration 表示的时间段,指定了触发事件的时间间隔。

  2. 定期触发事件Ticker 返回一个通道 (ticker.C),在每个时间间隔结束时都会向该通道发送一个时间值。您可以使用 for range 循环来监听这个通道,以执行定期的操作。

  3. 停止 Ticker:如果不再需要 Ticker,可以使用 ticker.Stop() 方法来停止它,以释放资源。停止后的 Ticker 不能再次启动。

下面是一个示例,演示了如何使用 time.Ticker 来定期触发事件:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个每秒触发一次的 Ticker
	ticker := time.NewTicker(1 * time.Second)

	// 启动一个 Goroutine 来监听 Ticker
	go func() {
		for {
			select {
			case <-ticker.C:
				fmt.Println("Ticker triggered at", time.Now())
				// 在这里执行定期的操作
			}
		}
	}()

	// 等待一段时间,以便观察 Ticker 触发
	time.Sleep(5 * time.Second)

	// 停止 Ticker
	ticker.Stop()
	fmt.Println("Ticker stopped.")
}

在这个示例中,我们创建了一个每秒触发一次的 Ticker,并在一个单独的 Goroutine 中监听 Ticker。每次 Ticker 触发时,它会向通道 ticker.C 发送一个时间值,然后我们在 select 语句中监听并执行定期的操作。

需要注意的是,一旦不再需要 Ticker,我们使用 ticker.Stop() 来停止它,以释放资源。在示例中,我们等待了一段时间,然后停止了 Ticker,以演示如何停止定期触发。如果不停止 Ticker,它将一直运行下去,可能导致资源泄漏。

13.4.7.原子变量的引入(处理数据同步)

在 Go 编程语言中,可以使用标准库的 sync/atomic 包来操作原子变量,以确保多个 Goroutine 对变量的读写操作是原子的,不会发生竞态条件。这个包提供了一组原子操作函数,可用于操作整数类型和指针类型的变量。

以下是一些常用的原子操作函数:

  1. Add:用于原子地将一个整数与另一个整数相加,并将结果存储在变量中。例如,atomic.AddInt32atomic.AddInt64 分别用于 int32int64 类型。

  2. Load:用于原子地加载变量的值,返回当前值。例如,atomic.LoadInt32atomic.LoadInt64 用于 int32int64 类型。

  3. Store:用于原子地存储一个值到变量中。例如,atomic.StoreInt32atomic.StoreInt64 用于 int32int64 类型。

  4. Swap:用于原子地交换变量的值,并返回变量之前的值。例如,atomic.SwapInt32atomic.SwapInt64 用于 int32int64 类型。

  5. CompareAndSwap:用于原子地比较变量的值与旧值,如果相等,则将新值存储到变量中。例如,atomic.CompareAndSwapInt32atomic.CompareAndSwapInt64 用于 int32int64 类型。

这些原子操作函数允许你执行操作而不用担心并发竞态条件,因为它们确保了原子性。这在多个 Goroutine 同时访问共享变量时非常有用,可以避免数据竞争问题。

以下是一个简单的示例,演示如何使用 sync/atomic 包来操作原子变量:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var counter int32
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			atomic.AddInt32(&counter, 1) // 原子地增加计数器的值
			wg.Done()
		}()
	}

	wg.Wait()

	fmt.Println("Final counter value:", atomic.LoadInt32(&counter)) // 原子地加载计数器的值
}

在这个示例中,我们使用 atomic.AddInt32 来原子地增加 counter 变量的值,并使用 atomic.LoadInt32 来原子地加载 counter 的值。这样,我们可以确保计数器的操作是线程安全的。
在并发编程中,“交换”(Swap)和"加载"(Load)是两个关键的原子操作概念,它们通常用于处理共享数据的读写和同步。这些操作是为了确保多个并发线程或 Goroutine 可以安全地访问和修改共享数据而设计的。

  1. 加载(Load):加载操作是用于读取共享数据的原子操作。它通常用于获取共享变量的当前值,而不会对变量进行修改。在 Go 中,atomic.LoadXXX 函数(如 atomic.LoadInt32)用于原子地加载共享变量的值,以确保在读取时不会被其他并发操作干扰。

  2. 交换(Swap):交换操作是用于原子地读取和写入共享变量的原子操作。它通常用于获取共享变量的当前值并将新值写入变量。在 Go 中,atomic.SwapXXX 函数(如 atomic.SwapInt32)用于原子地交换共享变量的值,并返回交换前的旧值。

  3. 对于 “交换” 操作,我可以进一步解释一下。

在 Go 的 sync/atomic 包中,atomic.SwapXXX 函数用于原子地交换共享变量的值,并返回交换前的旧值。这个操作适用于需要原子地读取当前值并设置新值的情况。让我用一个更具体的示例来说明这个概念:

假设有一个整数类型的共享变量 x,我们想要原子地读取其当前值,并将其设置为一个新的值。我们可以使用 atomic.SwapInt32 来执行这个操作。示例代码如下:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var x int32 = 42

	// 原子地交换共享变量的值,并返回旧值
	oldValue := atomic.SwapInt32(&x, 100)
	fmt.Println("Old value:", oldValue) // 打印旧值
	fmt.Println("New value:", x)        // 打印新值
}

在这个示例中,我们首先创建了一个整数变量 x,并将其初始化为 42。然后,我们使用 atomic.SwapInt32 来原子地交换 x 的值,将其设置为 100,并获取了交换前的旧值。最后,我们打印了旧值和新值。

关键点是 atomic.SwapInt32 函数执行这个操作是原子的,这意味着在执行交换操作期间,不会有其他并发操作来干扰或修改共享变量 x 的值。因此,oldValue 变量将包含交换前的值,而 x 变量将包含新的值。这确保了在多个 Goroutine 并发访问 x 变量时的数据一致性和可靠性。

这些操作是原子的,这意味着它们在执行时不会被其他并发操作中断,从而避免了竞态条件和数据竞争。在多线程或多 Goroutine 的并发环境中,使用加载和交换操作可以确保共享数据的一致性和可靠性,而无需显式地使用锁来保护。

以下是一个示例,演示了如何在 Go 中使用加载和交换操作:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var sharedValue int32 = 42

	// 原子地加载共享变量的值
	value := atomic.LoadInt32(&sharedValue)
	fmt.Println("Loaded value:", value)

	// 原子地交换共享变量的值,并返回旧值
	oldValue := atomic.SwapInt32(&sharedValue, 100)
	fmt.Println("Old value:", oldValue)
	fmt.Println("New value:", sharedValue)
}

在这个示例中,我们使用 atomic.LoadInt32 来加载共享变量的值,然后使用 atomic.SwapInt32 来交换共享变量的值。这些操作确保了对共享变量的读取和修改都是原子的,不会受到其他并发操作的影响。
在 Go 中,比较交换(Compare and Swap,简称 CAS)是通过 sync/atomic 包中的 CompareAndSwap 函数来实现的。CAS 操作允许你原子地比较共享变量的当前值与预期值,如果它们匹配,就将变量设置为新值。这个操作通常用于解决并发环境下的竞态条件问题。

以下是使用 sync/atomic 包中的 CompareAndSwap 函数的示例:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var value int32 = 42

	// 预期值为42,如果当前值等于42,则将其原子地设置为100
	expected := int32(42)
	newValue := int32(100)
	success := atomic.CompareAndSwapInt32(&value, expected, newValue)

	if success {
		fmt.Println("CAS succeeded")
	} else {
		fmt.Println("CAS failed")
	}

	fmt.Println("New value:", value)
}

在这个示例中,我们首先创建了一个整数变量 value,然后使用 atomic.CompareAndSwapInt32 函数来比较 value 的当前值是否等于预期值(expected,这里是 42)。如果当前值等于预期值,函数将原子地将变量的值设置为新值(newValue,这里是 100),并返回 true 表示 CAS 操作成功。否则,返回 false 表示操作失败。

CAS 操作是一种非常有用的原子操作,可以用于构建高效的并发算法和数据结构,例如无锁队列、自旋锁、并发计数器等。它允许在无需使用显式锁的情况下,安全地更新共享变量,从而减少了竞态条件的发生。CAS 是并发编程中的重要工具,用于解决多个 Goroutine 同时访问共享数据时的同步问题。
在 Go 的 sync/atomic 包中,存储操作是通过 Store 函数来实现的。存储操作是一种原子操作,用于原子地设置共享变量的新值,而不需要获取变量的当前值。这个操作通常用于只关心设置新值的情况,而不需要读取或比较当前值。

以下是使用 sync/atomic 包中的 Store 函数的示例:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var value int32

	// 原子地设置共享变量的新值
	newValue := int32(100)
	atomic.StoreInt32(&value, newValue)

	fmt.Println("New value:", value)
}

在这个示例中,我们创建了一个整数变量 value,然后使用 atomic.StoreInt32 函数来原子地将 value 的值设置为新值(newValue,这里是 100)。

存储操作是一种非常有用的原子操作,通常用于需要原子性地更改共享变量的值,而不需要比较或读取当前值的情况。这对于一些并发算法和数据结构来说非常有用,例如原子地更新计数器或标记某些状态的变量。与 CAS 操作不同,存储操作不需要提供预期值,因为它总是设置变量的新值。
在 Go 的 sync/atomic 包中,提供了一系列原子操作的方法,用于在并发编程中执行原子操作。以下是常见的原子操作函数和它们的签名:

  1. Add:原子地将指定的值添加到整数变量,并返回新的值。

    • func AddInt32(addr *int32, delta int32) (new int32)
    • func AddInt64(addr *int64, delta int64) (new int64)
    • func AddUint32(addr *uint32, delta uint32) (new uint32)
    • func AddUint64(addr *uint64, delta uint64) (new uint64)
  2. CompareAndSwap:原子地比较指定的值和共享变量的当前值,如果相等则将共享变量设置为新的值,并返回是否操作成功。

    • func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
    • func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
    • func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
    • func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
    • func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
  3. Load:原子地加载共享变量的当前值。

    • func LoadInt32(addr *int32) (val int32)
    • func LoadInt64(addr *int64) (val int64)
    • func LoadUint32(addr *uint32) (val uint32)
    • func LoadUint64(addr *uint64) (val uint64)
    • func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
  4. Store:原子地设置共享变量的新值。

    • func StoreInt32(addr *int32, val int32)
    • func StoreInt64(addr *int64, val int64)
    • func StoreUint32(addr *uint32, val uint32)
    • func StoreUint64(addr *uint64, val uint64)
    • func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
  5. Swap:原子地交换共享变量的值,并返回交换前的旧值。

    • func SwapInt32(addr *int32, new int32) (old int32)
    • func SwapInt64(addr *int64, new int64) (old int64)
    • func SwapUint32(addr *uint32, new uint32) (old uint32)
    • func SwapUint64(addr *uint64, new uint64) (old uint64)
    • func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

这些原子操作函数提供了一种安全的方式来处理共享变量,确保在多个 Goroutine 之间进行并发访问时不会发生竞态条件或数据竞争。使用这些原子操作函数,你可以构建高效且可靠的并发算法和数据结构。请根据你的需求选择适当的原子操作函数来处理共享数据。

13.4.8.context介绍

在Go语言中,context是一个用于在函数之间传递请求范围的值、取消信号和截止时间等的包。它提供了一种机制,使得你可以控制函数的执行,特别是涉及并发操作或可能需要取消长时间运行的操作时。

以下是context包的一些主要特点和用途:

传递请求范围的数据:通过使用context,你可以将请求特定的数据(如身份验证信息、用户ID等)从一个函数传递到另一个函数,而无需将这些数据作为每个函数的参数显式传递。
取消操作:context可以提供一个取消信号,允许你取消正在进行的操作。这对于处理可能需要中断的长时间运行任务非常有用,例如HTTP请求、数据库查询或文件I/O操作。
设置超时和截止时间:通过context,你可以为操作设置一个超时时间或截止时间。如果在这个时间内操作未完成,context将发出取消信号。
控制并发:在Go的并发编程中,context可以帮助管理和协调多个goroutine的执行。你可以使用context来通知所有相关的goroutine停止它们的工作,以避免资源泄漏和不必要的计算。
context包中主要有几个关键类型:

context.Context:这是一个接口,定义了几个方法,包括Deadline()、Done()、Err()和Value()。这些方法允许你查询操作的截止时间、检测取消信号、获取取消错误以及获取存储在context中的值。
context.Background():这个函数返回一个空的Context,通常用作整个Context树的根节点。它不包含任何值,也没有取消信号,主要用于主程序、初始化以及测试,有时也用作顶层函数的Context参数。
context.TODO():当你不确定应该使用哪种Context,或者当前函数还不支持Context,但未来可能会添加时,可以使用这个函数。它返回一个不做任何事情的Context,仅用作占位符,以避免在未来重构时需要大量修改代码。
context.WithCancel(parent):这个函数接受一个父Context作为参数,并返回一个带有取消功能的子Context。当你调用返回的取消函数时,子Context及其所有派生的Context都会被取消。
context.WithDeadline(parent, deadline) 和 context.WithTimeout(parent, timeout):这两个函数允许你设置一个截止时间或超时时间。当达到指定的时间时,相关的Context将被取消。这对于需要限制执行时间的操作非常有用。
context.WithValue(parent, key, value):这个函数允许你将一个值与Context相关联。这可以用于在函数之间传递请求特定的数据。但请注意,不要过度使用这个功能,以避免滥用上下文来传递可选的参数。
总的来说,context包是Go语言中处理并发和取消操作的重要工具。它提供了一种灵活且强大的机制来管理和协调goroutine的执行,以及传递请求范围的数据。

package main

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

var wg sync.WaitGroup

// 共享变量,尽量不是用共享变量,使用消息
//var stop bool
//var stop = make(chan struct{})

// func cpuInfo(stop <-chan struct{}) {
func cpuInfo(ctx context.Context) {
	// 这里能请求到请求的id,withValue存放一些与业务无关的参数
	defer wg.Done()
	for {

		select {
		case <-ctx.Done():
			fmt.Println("退出cpu监控")
			//	记住return
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("cpu info")
		}

		//相当于是一个机关
		//if stop {
		//	break
		//}

	}

}

func main() {

	//	context详解
	/*
			为什么要使用context?
			示例:有一个goroutine监控cpu信息,
			context	提供了三种函数,withCancel,withTimeout,withValue,
		如果你的goruntine,函数中,希望被控制,超时,传递值,但是我不希望影响我原来的接口信息的时候,函数参数中第一个参数就尽量带上
	*/
	//var stop = make(chan struct{})
	//	渐进式的方式
	wg.Add(1)
	//这是父的ctx
	//ctx, cancel := context.WithCancel(context.Background())
	可以传递,同时生效cancel,主动取消
	//ctx2, _ := context.WithCancel(ctx)
	//go cpuInfo(ctx2)
	defer wg.Done()
	//time.Sleep(6 * time.Second)
	//stop <- struct{}{}
	//cancel()
	//主动超时
	//ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)
	//cpuInfo(ctx)

	//3.withDeadLine 	在时间点上cancel
	//4.withValue  	在时间点上cancel
	ctx := context.WithValue(ctx, "mykey", "myvalue")
	go cpuInfo(ctx)
	wg.Wait()
	fmt.Println("监控完成!")

}

十四.OS文件操作

14.1.文件操作

Go 编程语言提供了丰富的操作系统操作功能,这些功能可以让你执行文件操作、路径操作、环境变量设置等等。以下是一些常见的 Go 中的操作系统操作功能和它们的简要介绍:

  1. 文件操作:

    • os.Open:用于打开文件以进行读取或写入操作。
    • os.Create:创建一个新文件。
    • os.Remove:删除文件。
    • os.Rename:重命名文件。
  2. 目录操作:

    • os.Mkdir:创建新目录。
    • os.MkdirAll:递归地创建目录,包括所有必要的父级目录。
    • os.Remove:删除目录。
    • os.RemoveAll:递归地删除目录及其所有内容。
  3. 文件信息:

    • os.Stat:获取文件或目录的详细信息,如大小、修改时间等。
  4. 路径操作:

    • path/filepath 包提供了用于处理文件路径的函数,包括路径拼接、路径分割等功能。
  5. 环境变量:

    • os.Getenv:获取环境变量的值。
    • os.Setenv:设置环境变量的值。
    • os.Unsetenv:删除环境变量。
  6. 执行外部命令:

    • os/exec 包允许你执行外部命令并与其交互。
  7. 工作目录:

    • os.Getwd:获取当前工作目录。
    • os.Chdir:更改当前工作目录。
  8. 标准输入/输出:

    • os.Stdinos.Stdoutos.Stderr:表示标准输入、标准输出和标准错误流。

这些是 Go 语言中一些常见的操作系统操作功能,它们使得在文件系统、环境变量和进程控制方面进行编程变得非常方便。你可以根据具体的需求使用这些函数和包来执行操作系统相关的任务。
以下是一些 Go 语言中常见的操作系统操作的示例代码:

  1. 文件操作:

    // 打开文件并读取内容
    file, err := os.Open("example.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    
    // 创建新文件并写入内容
    newFile, err := os.Create("newfile.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer newFile.Close()
    
    _, err = newFile.WriteString("Hello, World!")
    if err != nil {
        log.Fatal(err)
    }
    
  2. 目录操作:

    // 创建新目录
    err := os.Mkdir("newdir", 0755)
    if err != nil {
        log.Fatal(err)
    }
    
    // 删除目录
    err = os.Remove("newdir")
    if err != nil {
        log.Fatal(err)
    }
    
  3. 文件信息:

    // 获取文件信息
    fileInfo, err := os.Stat("example.txt")
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Println("文件名:", fileInfo.Name())
    fmt.Println("大小 (字节):", fileInfo.Size())
    fmt.Println("修改时间:", fileInfo.ModTime())
    
  4. 环境变量:

    // 获取环境变量的值
    value := os.Getenv("PATH")
    fmt.Println("PATH 环境变量:", value)
    
    // 设置环境变量的值
    err := os.Setenv("MY_VARIABLE", "my_value")
    if err != nil {
        log.Fatal(err)
    }
    
  5. 执行外部命令:

    // 执行外部命令
    cmd := exec.Command("ls", "-l")
    output, err := cmd.CombinedOutput()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
    
  6. 工作目录:

    // 获取当前工作目录
    wd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("当前工作目录:", wd)
    
    // 更改当前工作目录
    err = os.Chdir("/path/to/new/directory")
    if err != nil {
        log.Fatal(err)
    }
    

这些示例涵盖了 Go 语言中常见的操作系统操作。你可以根据具体需求自由组合和修改这些示例代码。请确保在实际应用中添加适当的错误处理和错误检查。
os.OpenFile 函数用于打开或创建文件,并提供了许多参数来控制文件的打开方式、权限等。以下是 os.OpenFile 函数的参数说明:

func OpenFile(name string, flag int, perm FileMode) (*File, error)
  • name:表示要打开或创建的文件的名称(包括路径)。可以是相对路径或绝对路径。

  • flag:是一个整数标志,用于指定文件的打开模式。可以使用位掩码来组合多个标志。常见的标志包括:

    • os.O_RDONLY:只读模式。
    • os.O_WRONLY:只写模式。
    • os.O_RDWR:读写模式。
    • os.O_CREATE:如果文件不存在,则创建它。
    • os.O_APPEND:在文件末尾追加数据而不覆盖。
    • os.O_TRUNC:如果文件存在,将其截断为空文件。
    • 等等。
  • perm:是一个文件权限模式,通常用八进制表示。它用于确定文件被创建时的权限。例如,0644 表示文件所有者具有读写权限,其他用户只有读权限。

该函数返回两个值:

  • *File:表示成功打开或创建的文件的句柄,可以用于进行读写操作。
  • error:如果打开或创建文件时出现错误,将返回一个非空的错误。

示例使用:

file, err := os.OpenFile("example.txt", os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

这个示例打开或创建一个名为 “example.txt” 的文件,以读写模式打开,如果文件不存在则创建,文件权限设置为 0644
在 Go 语言中,你可以使用 os 包来执行与进程相关的操作,包括以下内容:

  1. 获取进程信息:

    • os.Getpid():获取当前进程的 PID(进程标识符)。
    • os.Getppid():获取父进程的 PID。
  2. 环境变量:

    • os.Getenv(name string):获取指定名称的环境变量的值。
    • os.Setenv(name, value string):设置指定名称的环境变量的值。
    • os.Unsetenv(name string):删除指定名称的环境变量。
  3. 退出程序:

    • os.Exit(code int):以指定的退出码退出程序。通常,0 表示成功,非零值表示错误。
  4. 命令行参数:

    • os.Args:一个字符串切片,包含命令行参数,其中 os.Args[0] 是程序的名称。
  5. 执行外部命令:

    • os/exec 包提供了执行外部命令的功能。你可以使用 exec.Command 创建一个外部命令,然后执行它。
  6. 工作目录:

    • os.Getwd():获取当前工作目录。
    • os.Chdir(dir string):更改当前工作目录为指定的目录。
  7. 信号处理:

    • os.Signalos/signal 包用于处理进程接收到的操作系统信号。这可以用于在运行时处理中断、终止等信号。

以下是一些示例代码,演示了上述操作的使用:

package main

import (
    "fmt"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
)

func main() {
    // 获取进程的 PID 和父进程的 PID
    fmt.Printf("PID: %d\n", os.Getpid())
    fmt.Printf("Parent PID: %d\n", os.Getppid())

    // 获取环境变量的值
    path := os.Getenv("PATH")
    fmt.Printf("PATH: %s\n", path)

    // 执行外部命令
    cmd := exec.Command("ls", "-l")
    output, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(output))

    // 获取命令行参数
    fmt.Println("命令行参数:", os.Args)

    // 获取当前工作目录
    wd, _ := os.Getwd()
    fmt.Println("当前工作目录:", wd)

    // 处理操作系统信号(例如,Ctrl+C)
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        sig := <-sigCh
        fmt.Printf("接收到信号: %s\n", sig)
        os.Exit(1)
    }()

    // 退出程序
    os.Exit(0)
}

这些示例演示了如何在 Go 中执行各种与进程相关的操作,包括获取进程信息、处理环境变量、执行外部命令、处理信号等。根据你的实际需求,你可以使用这些操作来管理和控制程序的行为。

14.2.IO相关操作

Go语言中的I/O(输入/输出)操作是通过标准库中的包来实现的,这些包提供了丰富的功能和性能优势。以下是一些常用的Go语言I/O包和其介绍:

  1. fmt包: fmt 包提供了格式化输入和输出的功能,通常用于控制台输入和输出。它包括 PrintlnPrintfScan 等函数。

  2. os包: os 包提供了与操作系统交互的功能,包括文件操作。你可以使用它来创建、打开、读取、写入和删除文件,以及访问环境变量等。

  3. io包: io 包定义了接口,用于通用的输入和输出操作。这包括 ReaderWriter 接口,允许你将不同类型的数据(例如文件、网络连接、内存缓冲区)与统一的接口进行交互。

  4. bufio包: bufio 包提供了缓冲区读写,可以大大提高读取和写入数据的性能。它包括 ScannerWriter 类型,用于更高效地处理输入和输出。

  5. ioutil包: ioutil 包提供了一些便捷的I/O函数,如文件的复制、读取整个文件到内存等。

  6. filepath包: filepath 包用于处理文件路径,提供了跨平台的路径操作功能,使你可以轻松地处理文件和目录的路径。

  7. encoding包: Go语言的 encoding 包提供了各种数据格式的编码和解码功能,如JSON、XML、CSV等。这对于与外部系统进行数据交换非常有用。

  8. net包: net 包用于网络编程,包括TCP、UDP、HTTP等协议的支持,以及套接字操作。

  9. archive包: archive 包用于处理归档文件,如ZIP和TAR格式,允许你压缩和解压文件。

  10. os/exec包: os/exec 包用于执行外部命令,这对于与操作系统进行交互非常有用。

这些是Go语言中常见的I/O相关包,它们提供了丰富的功能,可以满足各种输入和输出需求。你可以根据具体的任务选择合适的包来处理I/O操作。
当涉及到不同类型的I/O操作时,下面是一些Go语言的示例代码,涵盖了常见的场景。这些示例涵盖了文件I/O、网络I/O、标准输入/输出以及其他常见的操作。

  1. 文件读取和写入:
package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	// 写入文件
	data := []byte("Hello, Go I/O!")
	err := ioutil.WriteFile("example.txt", data, 0644)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 读取文件
	fileData, err := ioutil.ReadFile("example.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(fileData))
}
  1. 使用bufio进行文件读取和写入:
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	// 写入文件
	file, err := os.Create("example.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()
	writer := bufio.NewWriter(file)
	writer.WriteString("Hello, Go I/O with bufio!")
	writer.Flush()

	// 读取文件
	file, err = os.Open("example.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}
  1. 标准输入和输出:
package main

import (
	"fmt"
	"os"
)

func main() {
	// 从标准输入读取数据
	var input string
	fmt.Print("请输入文本: ")
	_, err := fmt.Scanln(&input)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("你输入的是:", input)

	// 向标准输出写入数据
	fmt.Fprintln(os.Stdout, "这是标准输出示例")
}
  1. 网络I/O:
package main

import (
	"fmt"
	"net"
)

func main() {
	// 启动一个简单的TCP服务器
	listener, err := net.Listen("tcp", "localhost:8080")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer listener.Close()

	fmt.Println("等待客户端连接...")
	conn, err := listener.Accept()
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()

	fmt.Fprintln(conn, "欢迎连接到服务器!")
}

这些示例展示了Go语言中不同类型的I/O操作,包括文件读写、标准输入输出以及网络通信。你可以根据需要进一步扩展和修改这些示例来满足特定的用例。
在Go语言中,io.Readerio.Writer 接口是用于进行通用输入和输出操作的核心接口。它们允许你从不同的数据源读取数据(输入操作)或将数据写入不同的目标(输出操作),而无需关心底层数据源或目标的具体类型。以下是它们的使用示例:

使用 io.Reader 读取数据:

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	// 创建一个字符串作为输入源
	input := strings.NewReader("Hello, Reader!")

	// 创建一个缓冲区来存储读取的数据
	buffer := make([]byte, 10)

	for {
		n, err := input.Read(buffer)
		if err == io.EOF {
			break // 到达文件末尾时退出循环
		}
		if err != nil {
			fmt.Println(err)
			break
		}
		fmt.Printf("读取了 %d 字节数据: %s\n", n, buffer[:n])
	}
}

在上面的示例中,我们使用了 strings.NewReader 创建了一个实现了 io.Reader 接口的字符串输入源,并使用循环从中读取数据直到文件末尾。

使用 io.Writer 写入数据:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 创建或打开一个文件用于写入数据
	file, err := os.Create("output.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()

	// 创建一个实现了 io.Writer 接口的文件写入器
	writer := file

	// 写入数据到文件
	data := []byte("Hello, Writer!")
	n, err := writer.Write(data)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("写入了 %d 字节数据\n", n)
}

在上面的示例中,我们使用 os.Create 创建了一个文件,并将其转换为实现了 io.Writer 接口的文件写入器。然后,我们使用 Write 方法将数据写入文件。

这些示例展示了如何使用 io.Readerio.Writer 接口来进行通用的输入和输出操作。这些接口的强大之处在于它们可以与各种不同类型的数据源和目标进行交互,使你能够编写通用的I/O代码。
在Go语言中,io.Pipe 类型提供了一种在内存中创建管道(pipe)的机制,用于在两个goroutine之间进行通信。管道允许一个goroutine将数据写入管道的一端,而另一个goroutine可以从另一端读取这些数据,从而实现数据的传输。

以下是一个使用io.Pipe的简单示例:

package main

import (
	"fmt"
	"io"
)

func main() {
	// 创建一个管道
	reader, writer := io.Pipe()

	// 启动一个goroutine向管道写入数据
	go func() {
		defer writer.Close() // 关闭写入端以指示数据写入完成
		data := []byte("Hello, Pipe!")
		_, err := writer.Write(data)
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 从管道中读取数据
	buffer := make([]byte, 20)
	n, err := reader.Read(buffer)
	if err != nil && err != io.EOF {
		fmt.Println(err)
		return
	}

	fmt.Printf("读取了 %d 字节数据: %s\n", n, buffer[:n])
}

在上面的示例中,我们使用io.Pipe创建了一个管道,然后启动了一个goroutine,向管道写入数据。同时,主goroutine从管道读取数据。io.Pipe返回的readerwriter实现了io.Readerio.Writer接口,因此可以直接用于读取和写入数据。

需要注意的是,使用io.Pipe时要小心管理管道的关闭,以确保读取端在读取完所有数据后可以正确关闭,否则可能会导致死锁。在示例中,我们使用defer writer.Close()来关闭写入端,以便通知读取端数据写入完成。

管道通常用于在goroutines之间传递数据,例如在并发编程中,可以用于将数据从一个goroutine传递给另一个goroutine,以实现协程之间的协作。

14.3.ioutil使用

ioutil 是Go语言标准库中的一个工具包,它提供了一些实用的I/O(输入/输出)相关功能,旨在简化文件和目录操作。以下是一些ioutil包中常用的功能和函数:

  1. 文件读写:

    • ioutil.ReadFile(filename string) ([]byte, error): 读取指定文件的内容并以字节切片的形式返回。
    • ioutil.WriteFile(filename string, data []byte, perm os.FileMode) error: 将字节切片写入指定文件,并设置文件权限。
  2. 目录操作:

    • ioutil.ReadDir(dirname string) ([]os.DirEntry, error): 读取指定目录的文件和子目录的信息,返回一个os.DirEntry切片。
    • ioutil.ReadDirSorted(dirname string) ([]os.DirEntry, error): 读取指定目录的文件和子目录的信息,并按名称排序后返回。
  3. 临时文件和目录:

    • ioutil.TempDir(dir, prefix string) (string, error): 在指定目录下创建一个临时目录,返回目录的路径。
    • ioutil.TempFile(dir, prefix string) (*os.File, error): 在指定目录下创建一个临时文件,返回文件的指针。
  4. 拷贝文件:

    • ioutil.Copy(dst io.Writer, src io.Reader) (written int64, err error): 从源io.Reader中复制数据到目标io.Writer中,返回拷贝的字节数。
  5. 删除文件和目录:

    • ioutil.Remove(filename string) error: 删除指定的文件或空目录。
    • ioutil.RemoveAll(path string) error: 递归删除指定路径下的所有文件和目录。
  6. 其他功能:

    • ioutil.NopCloser(r io.Reader) io.ReadCloser: 将io.Reader包装成io.ReadCloser,通常用于将数据源适配为满足io.ReadCloser接口的情况。

ioutil包提供了许多便捷的函数,使文件和目录操作更加方便和简洁。然而,需要注意的是,在处理大文件或需要更高级别控制的情况下,可能需要使用更底层的文件和目录操作函数,以更精细地控制内存和性能。

14.4.bufio(提高读写效率)

bufio 是Go语言标准库中的一个包,提供了缓冲读写功能,用于提高I/O操作的性能。它包装了io.Readerio.Writer接口,并添加了缓冲功能,以减少频繁的系统调用,从而提高数据的读取和写入效率。bufio包中最常用的类型是bufio.Readerbufio.Writer

以下是 bufio 包的一些常见功能和类型:

  1. bufio.Reader:

    • bufio.NewReader(r io.Reader) *Reader:创建一个新的bufio.Reader,用于包装给定的io.Reader
    • Reader.Read(p []byte) (n int, err error):从包装的io.Reader中读取数据到字节切片p,返回读取的字节数和可能的错误。
    • Reader.ReadString(delim byte) (string, error):读取直到遇到指定分隔符delim的字符串。
    • Reader.ReadLine() (line []byte, isPrefix bool, err error):逐行读取数据,返回一行数据的字节切片,是否为行前缀,以及可能的错误。
  2. bufio.Writer:

    • bufio.NewWriter(w io.Writer) *Writer:创建一个新的bufio.Writer,用于包装给定的io.Writer
    • Writer.Write(p []byte) (n int, err error):将字节切片p写入包装的io.Writer,返回写入的字节数和可能的错误。
    • Writer.WriteString(s string) (n int, err error):将字符串写入包装的io.Writer,返回写入的字节数和可能的错误。
    • Writer.Flush() error:将缓冲区中的数据刷新到底层的io.Writer

使用 bufio 包可以显著提高对文件、网络连接等的读写性能,特别是在大量小数据块的场景下。通过使用缓冲,减少了对底层资源的频繁访问,从而提高了效率。

以下是一个示例,演示了如何使用bufio.Readerbufio.Writer从文件中读取内容并写入到另一个文件中:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	// 打开输入文件和输出文件
	inputFile, err := os.Open("input.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer inputFile.Close()

	outputFile, err := os.Create("output.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer outputFile.Close()

	// 创建 bufio.Reader 和 bufio.Writer
	reader := bufio.NewReader(inputFile)
	writer := bufio.NewWriter(outputFile)

	// 从输入文件读取内容并写入到输出文件
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			break // 文件读取完毕或发生错误
		}
		writer.WriteString(line)
	}

	// 刷新缓冲区,确保数据写入到文件
	writer.Flush()
}

在此示例中,我们使用了bufio.Reader从输入文件逐行读取内容,并使用bufio.Writer将内容逐行写入到输出文件。这种方式不仅提高了性能,还允许我们逐行处理文件而无需担心内存溢出问题。
以下是一个更详细的示例,演示了如何使用 bufio 包来从标准输入读取文本行并将其写入到文件中。这个示例涵盖了创建 bufio.Readerbufio.Writer,以及如何处理输入和输出。

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	// 打开一个文件用于写入数据
	outputFile, err := os.Create("output.txt")
	if err != nil {
		fmt.Println("无法创建文件:", err)
		return
	}
	defer outputFile.Close()

	// 创建 bufio.Writer
	writer := bufio.NewWriter(outputFile)

	// 从标准输入读取文本行,并写入文件
	fmt.Println("请输入文本行(Ctrl+D结束输入):")
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line := scanner.Text()
		_, err := writer.WriteString(line + "\n")
		if err != nil {
			fmt.Println("写入文件错误:", err)
			return
		}
	}

	// 检查扫描错误
	if err := scanner.Err(); err != nil {
		fmt.Println("扫描输入错误:", err)
		return
	}

	// 刷新缓冲区,确保数据写入到文件
	err = writer.Flush()
	if err != nil {
		fmt.Println("刷新缓冲区错误:", err)
		return
	}

	fmt.Println("文本已成功写入到 output.txt 文件。")
}

在这个示例中,我们首先创建了一个 output.txt 文件用于写入数据,并创建了一个 bufio.Writer 对象,用于缓冲写入操作。然后,我们使用 bufio.Scanner 从标准输入读取文本行。循环读取每一行文本,将其写入 bufio.Writer 缓冲区,最后使用 Flush 方法将缓冲区的内容刷新到文件中。

这个示例允许用户从标准输入逐行输入文本,直到输入结束(通过按下Ctrl+D或Ctrl+Z,具体取决于操作系统)。读取的文本行会逐行写入到 output.txt 文件中。

通过使用 bufio 包,可以有效地处理输入和输出,提高了性能并减少了对底层资源的频繁访问。

14.5.Scanner

bufio.Scanner 是 Go 语言标准库中的一个类型,用于从各种输入源(例如文件、标准输入、字符串等)逐行或逐个词(单词)地读取数据。bufio.Scanner 提供了一种方便的方式来处理文本数据,特别适用于逐行或逐词解析文本内容。以下是关于 bufio.Scanner 的一些重要信息和使用示例:

主要功能:

  1. 逐行读取文本: bufio.Scanner 可以逐行读取文本,每次读取一行,并自动识别换行符(包括 \n\r\n)。

  2. 自定义分隔符: 你可以自定义分隔符,以便逐个词(单词)读取文本内容,而不仅仅是逐行。

  3. 错误处理: bufio.Scanner 会自动处理输入中的错误,如读取错误、文件结束等,并将错误信息报告给你。

  4. 输入源灵活: 你可以使用 bufio.Scanner 处理各种输入源,包括文件、标准输入、字符串、网络连接等。

基本用法示例:

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	// 示例 1: 从字符串读取数据
	input := "Hello\nWorld\n"
	scanner := bufio.NewScanner(strings.NewReader(input))
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}

	// 示例 2: 从文件读取数据
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()
	scanner = bufio.NewScanner(file)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}

	// 示例 3: 自定义分隔符(逐个词读取)
	input = "apple orange banana"
	scanner = bufio.NewScanner(strings.NewReader(input))
	scanner.Split(bufio.ScanWords) // 使用空格作为词的分隔符
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("扫描错误:", err)
	}
}

在这些示例中,我们首先创建了 bufio.Scanner 对象,然后使用 Scan 方法来逐行或逐个词读取输入内容。你可以根据需要自定义分隔符,以便更精确地处理文本数据。示例中的错误处理部分演示了如何检测和处理可能发生的错误。

bufio.Scanner 是处理文本数据时的一个强大工具,它可以简化文本处理任务,特别是逐行处理日志文件、配置文件、CSV文件等情况。

十五.log标准库

在Go语言中,log 标准库提供了用于记录日志信息的功能,允许你在程序中添加日志记录,以便在诊断和调试时查看程序的运行状态。log 标准库位于 log 包中,以下是关于 log 包的详细介绍:

15.1.基本日志功能:

  1. 日志输出: log 包提供了输出日志信息的函数,包括 PrintPrintfPrintln,它们分别用于输出不带格式化的字符串、格式化的字符串和带有换行符的字符串。

  2. 日志级别: log 包没有原生支持日志级别,但你可以通过自定义前缀或结合标准库中的 flag 包来实现日志级别的控制。

  3. 输出位置: 默认情况下,日志信息会输出到标准错误(stderr),但你可以使用 log.SetOutput 方法将日志信息输出到其他地方,例如文件。

  4. 日期和时间戳: log 包会自动为日志信息添加日期和时间戳。

高级日志功能:

  1. 自定义日志格式: 你可以通过实现自定义的 log.Formatter 接口来定义日志的格式,以满足特定需求。

  2. 日志文件分割: 通过结合 log 包和 time 包,你可以编写代码以在达到特定条件时切分日志文件,例如按日期或文件大小。

  3. 日志记录到多个输出源: 你可以将日志信息同时记录到多个输出源,例如同时输出到控制台和文件。

  4. 日志滚动策略: 你可以实现自定义的日志滚动策略,以定期清理旧的日志文件。

下面是一个简单的示例,演示如何在Go程序中使用 log 包记录日志:

package main

import (
	"log"
	"os"
)

func main() {
	// 设置日志输出到文件
	file, err := os.Create("app.log")
	if err != nil {
		log.Fatal("无法创建日志文件:", err)
	}
	defer file.Close()

	// 设置日志输出位置
	log.SetOutput(file)

	// 记录日志信息
	log.Println("这是一条普通日志信息")
	log.Printf("这是一条格式化日志信息:%d\n", 42)
	log.Fatal("这是一条致命错误日志信息")
}

在这个示例中,我们首先使用 os.Create 创建了一个名为 app.log 的日志文件,并将日志输出位置设置为该文件。然后,我们使用 log 包的函数记录了不同类型的日志信息,包括普通信息、格式化信息和致命错误信息。

请注意,log.Fatal 函数会在记录致命错误后立即终止程序执行。你可以根据需要使用其他日志函数,如 log.Paniclog.Panicf 来处理不同类型的错误情况。

总的来说,log 包是Go语言标准库中的一个简单但功能强大的日志记录工具,适用于基本的日志需求。如果需要更复杂的日志记录功能,可以考虑使用第三方日志库,如 logruszap 等,它们提供了更多的自定义和配置选项。
fatal里面方法含有os.Exit()所以不会执行defer,panic会。
log.New 是 Go 语言标准库中的一个函数,用于创建一个自定义配置的日志记录器(logger)。它的作用是根据给定的输出目标(io.Writer 接口)、前缀字符串和日志选项来创建一个新的日志记录器实例,以便你可以使用它来记录日志消息。

例如,你可以使用以下方式创建一个新的日志记录器:

import (
    "log"
    "os"
)

logger := log.New(os.Stdout, "MyApp: ", log.Ldate|log.Ltime)

上面的代码创建了一个日志记录器,将日志消息输出到标准输出(os.Stdout),并在每条日志消息前添加了 "MyApp: " 前缀,并且还使用了日期和时间作为前缀选项。接下来,你可以使用这个 logger 记录日志消息,例如:

logger.Println("This is a log message.")

这将在标准输出上打印类似以下内容的日志消息:

MyApp: 2023/09/24 15:30:00 This is a log message.

log.New 允许你根据自己的需求创建具有不同配置的日志记录器,以便更好地控制日志的格式和输出位置。

15.2.builtin库

Go 语言中有一些内建函数(built-in functions),它们是语言本身提供的函数,可以直接在代码中使用,而无需导入任何包。这些内建函数提供了一些基本的功能,用于操作数据、控制流程和进行其他常见的操作。以下是一些常见的内建函数以及它们的简要介绍:

  1. len():用于获取数组、切片、字符串、映射(map)等数据结构的长度。
mySlice := []int{1, 2, 3, 4, 5}
length := len(mySlice) // 获取切片的长度,结果为 5
  1. make():用于创建切片、映射map和通道(channel)等数据结构,并初始化其内部数据结构。
mySlice := make([]int, 5) // 创建一个包含 5 个整数的切片
  1. append():用于向切片追加元素。
mySlice := []int{1, 2, 3}
mySlice = append(mySlice, 4) // 将 4 追加到切片中
  1. copy():用于复制切片的内容到另一个切片。
source := []int{1, 2, 3}
destination := make([]int, len(source))
copy(destination, source) // 复制 source 切片到 destination 切片
  1. new():用于创建新的变量并返回指向该变量的指针。可以分配任意类型并且是返回指针
    new分配空间会被清零,make分配后会进行初始化
myIntPointer := new(int) // 创建一个新的整数变量,并返回指向该变量的指针
  1. panic()recover():用于处理运行时错误和异常。
func divide(a, b int) int {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    if b == 0 {
        panic("Division by zero")
    }
    return a / b
}

这些是 Go 语言中一些常见的内建函数,它们用于执行各种基本操作,使开发者能够更方便地处理数据和控制程序流程。当然,Go 语言还提供了更多的内建函数,你可以在官方文档中找到完整的列表和详细的说明。

15.3.byte库

bytes 包是 Go 语言标准库中的一个重要包,它提供了用于操作字节切片([]byte)的函数和类型。bytes 包的目标是提供高效、可变和安全的字节序列操作,使开发者能够更方便地处理二进制数据或文本数据。

以下是一些 bytes 包中常用的功能和类型:

  1. Buffer 类型bytes.Buffer 是一个动态字节缓冲区,可以用于在内存中构建和操作字节切片。它支持向缓冲区追加、读取、写入等操作。
import "bytes"

var buffer bytes.Buffer
buffer.WriteString("Hello, ")
buffer.WriteString("World!")
result := buffer.String() // 将缓冲区内容转为字符串
  1. 字节切片操作bytes 包提供了许多用于操作字节切片的函数,如拼接、分割、查找、替换等。
import "bytes"

data := []byte("Hello, World!")
prefix := []byte("Hello")

// 检查前缀
hasPrefix := bytes.HasPrefix(data, prefix)

// 替换
newData := bytes.Replace(data, []byte("Hello"), []byte("Hi"), -1)
  1. ReaderWriter 接口bytes.Readerbytes.Writer 类型实现了 io.Readerio.Writer 接口,允许将字节数据与标准库中的其他数据流进行交互。
import (
    "bytes"
    "io"
)

data := []byte("Hello, World!")
reader := bytes.NewReader(data)

buffer := make([]byte, 5)
_, err := reader.Read(buffer) // 从字节数组中读取数据到缓冲区
  1. 其他函数bytes 包还提供了其他一些有用的函数,如比较字节切片、将字节切片转为大写或小写等。
import "bytes"

s1 := []byte("Hello")
s2 := []byte("HELLO")
equal := bytes.Equal(s1, s2) // 比较两个字节切片是否相等

upper := bytes.ToUpper(s1) // 将字节切片转为大写
lower := bytes.ToLower(s2) // 将字节切片转为小写

15.4.buffer介绍

“缓冲区”(Buffer)是计算机科学中常用的概念,通常用于存储临时数据,以便在数据的生产者和消费者之间进行数据传输或协调。缓冲区通常用于优化数据读写性能,特别是在处理输入/输出或网络通信时。

在 Go 语言中,bytes.Bufferbufio 包中的 Buffer 类型是常见的缓冲区实现。

  1. bytes.Bufferbytes.Buffer 类型是 Go 标准库中的一个类型,用于在内存中创建和操作字节切片([]byte)的缓冲区。它实现了 io.Readerio.Writerio.ByteReaderio.ByteWriter 接口,因此可以像文件或其他 I/O 设备一样使用。你可以使用 bytes.Buffer 来构建和操作字节数据,然后将其写入文件、网络连接或其他地方。
import (
    "bytes"
    "fmt"
)

var buffer bytes.Buffer
buffer.WriteString("Hello, ")
buffer.WriteString("World!")
fmt.Println(buffer.String()) // 将缓冲区内容转为字符串
  1. bufio 包中的 Buffer 类型bufio 包中也有 Buffer 类型,用于创建和操作字节和文本的缓冲区。它实现了 io.Readerio.Writerio.ReaderFromio.WriterTo 等接口。bufio.Buffer 可以用于高效地读取和写入文件,它可以减少频繁的系统调用,提高性能。
import (
    "bufio"
    "fmt"
    "os"
)

file, err := os.Open("example.txt")
if err != nil {
    fmt.Println("文件打开失败:", err)
    return
}
以下是 Go 语言中 `bytes.Buffer` 类型的一些常用操作方法。`bytes.Buffer` 用于在内存中创建和操作字节切片(`[]byte`)的缓冲区,可以执行许多操作,如写入、读取、追加、截取等。下面是一些示例操作方法:

```go
import (
    "bytes"
    "fmt"
)

// 创建一个新的 Buffer
var buffer bytes.Buffer

// 写入数据到缓冲区
buffer.WriteString("Hello, ")
buffer.WriteString("World!")

// 将缓冲区内容转为字符串
content := buffer.String()
fmt.Println(content) // 输出: Hello, World!

// 从缓冲区读取数据
data := make([]byte, 5)
n, err := buffer.Read(data) // 读取 5 个字节到 data
if err != nil {
    fmt.Println("读取失败:", err)
} else {
    fmt.Println("读取成功:", string(data[:n])) // 输出: Hello,
}

// 清空缓冲区
buffer.Reset()

// 追加数据到缓冲区
buffer.Write([]byte("This is a test."))

// 获取缓冲区的长度
length := buffer.Len()

// 截取部分缓冲区内容
substring := buffer.Bytes()[5:9] // 获取 "is a" 部分

// 检查缓冲区是否为空
isEmpty := buffer.Len() == 0

// 读取缓冲区中的下一个字节
nextByte, err := buffer.ReadByte()

// 从缓冲区读取直到遇到指定分隔符
line, err := buffer.ReadString('\n')

// 获取缓冲区剩余的未读数据
remainingData := buffer.Bytes()

// 将缓冲区内容写入到 io.Writer 接口
destination := &bytes.Buffer{}
buffer.WriteTo(destination)

// 其他操作还包括 Replace、Truncate、Grow 等方法

bytes.Buffer 提供了许多方法来进行各种操作,这些方法使你能够有效地构建、修改和处理字节数据。具体操作方法的选择取决于你的需求,你可以根据需要使用这些方法来操作缓冲区中的数据。

15.5.errors介绍

在 Go 语言中,errors 包提供了一种简单的错误处理机制,它定义了一个 error 接口和一个用于创建错误值的函数。这个包使得在编写 Go 代码时能够更加规范地处理错误情况。

errors 包的核心部分是 error 接口,它只有一个方法:

type error interface {
    Error() string
}

这个接口包含了一个名为 Error 的方法,该方法返回一个字符串,表示错误的描述信息。

使用 errors 包,你可以创建自定义的错误值,通常是通过调用 errors.New 函数来创建的,该函数接受一个字符串参数,用于指定错误的描述信息。例如:

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("发生错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}

在上面的示例中,errors.New 函数创建了一个包含错误描述信息的错误值,并将其返回。在 divide 函数中,当除数为零时,它返回一个包含错误信息的错误值。

通常,函数会返回一个结果和一个错误值。如果操作成功,错误值为 nil;如果发生错误,错误值会包含错误的描述信息。在调用函数后,你可以检查错误值来确定操作是否成功,并根据情况采取适当的措施。

此外,Go 还提供了更高级的错误处理机制,如使用 deferpanic 来处理错误,以及使用自定义的错误类型来提供更多信息。但是,errors 包中的 error 接口和 errors.New 函数是处理错误的基础,可以在许多情况下满足需求。

在 Go 语言中,你可以通过创建自定义类型来表示自定义的异常(错误),并满足 error 接口的要求。这使得你能够在代码中使用更具描述性的错误类型,以便更好地了解错误的性质和原因。以下是创建自定义异常的一般步骤:

  1. 创建自定义错误类型:你可以定义一个新的类型来表示特定的错误。通常,这个类型是一个结构体,包含了描述错误的字段。确保这个类型实现了 error 接口的 Error 方法。
package main

import "fmt"

// 自定义错误类型
type MyError struct {
    Message string
    Code    int
}

// 实现 error 接口的 Error 方法
func (e *MyError) Error() string {
    return fmt.Sprintf("错误代码 %d: %s", e.Code, e.Message)
}

15.5.1自定义异常

  1. 使用自定义错误类型:在你的代码中,可以使用自定义错误类型来表示特定的错误情况,并创建错误值。
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &MyError{Message: "除数不能为零", Code: 1001}
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("发生错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}

在上面的示例中,MyError 类型表示特定的自定义错误,它包含了错误的描述信息和错误代码。当 divide 函数中出现除以零的情况时,它返回一个包含自定义错误的错误值。

这样做的好处是你可以根据错误类型和错误信息更好地理解错误的性质和原因,从而更容易调试和处理错误。当然,你可以根据需要扩展自定义错误类型,以包含更多的错误相关信息。在实际应用中,通常会为不同的错误情况创建不同的自定义错误类型,以便更精确地处理不同类型的错误。

15.6.sort包

sort 包是 Go 语言标准库中的一个包,它提供了用于对切片进行排序的函数和类型。这个包实现了常见的排序算法,使开发者可以轻松地对切片中的元素进行排序,无论是整数、浮点数、字符串还是自定义类型。

以下是 sort 包中的一些重要类型和函数:

  1. Interface 接口sort.Interface 是排序的核心接口,它定义了排序算法所需的方法。要对切片进行排序,你需要实现这个接口的三个方法:Len()Less(i, j int) boolSwap(i, j int)
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}
  1. IntsFloat64sStrings 函数:这些函数用于对切片中的整数、浮点数和字符串进行排序。它们接受实现了 sort.Interface 接口的切片作为参数,并将其原地排序。
import (
    "fmt"
    "sort"
)

numbers := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
sort.Ints(numbers)
fmt.Println(numbers) // 输出: [1 1 2 3 3 4 5 5 5 6 9]
  1. Sort 函数sort.Sort 函数用于对实现了 sort.Interface 接口的切片进行排序。这个函数接受一个接口类型作为参数,所以你可以用它来排序任何类型的切片。
import (
    "fmt"
    "sort"
)

names := []string{"Alice", "Eve", "Bob", "Charlie"}
sort.Sort(sort.StringSlice(names))
fmt.Println(names) // 输出: [Alice Bob Charlie Eve]
  1. 自定义类型的排序:你可以为自定义类型实现 sort.Interface 接口的方法,从而使你的自定义类型可以被 sort.Sort 函数排序。
type Person struct {
    Name string
    Age  int
}

type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

people := []Person{
    {"Alice", 25},
    {"Bob", 30},
    {"Eve", 22},
}

sort.Sort(ByAge(people))
fmt.Println(people) // 根据年龄排序

sort 包提供了一种简单但强大的排序机制,可用于各种类型的切片。它的性能很好,通常可以满足绝大多数排序需求。不需要编写自定义排序算法,只需实现 sort.Interface 接口的方法,就可以轻松地对切片进行排序。
以下是 Go 语言标准库中 sort 包中的主要函数和方法:

  1. sort.Interface 接口:虽然不是一个函数,但这个接口是 sort 包中的核心。它定义了排序算法所需的方法。要对切片进行排序,你需要实现以下三个方法:

    • Len() int:返回切片的长度。
    • Less(i, j int) bool:比较索引 ij 处的元素,返回 true 如果 i 位置的元素应该排在 j 位置的元素之前。
    • Swap(i, j int):交换索引 ij 处的元素。
  2. Ints 函数sort.Ints(slice []int) 用于对整数切片进行升序排序。

  3. Float64s 函数sort.Float64s(slice []float64) 用于对浮点数切片进行升序排序。

  4. Strings 函数sort.Strings(slice []string) 用于对字符串切片进行升序排序。

  5. Sort 函数sort.Sort(data sort.Interface) 用于对实现了 sort.Interface 接口的切片进行排序。这个函数是一个通用的排序函数,可以用于不同类型的切片。

  6. Slice 函数sort.Slice(slice interface{}, less func(i, j int) bool) 允许你使用自定义的比较函数对切片进行排序。less 函数接受两个索引 ij,并返回一个布尔值,表示 i 处的元素是否应排在 j 处的元素之前。

  7. SliceStable 函数sort.SliceStable(slice interface{}, less func(i, j int) bool) 类似于 Slice 函数,但它保持相等元素的原始顺序。

  8. Search 函数sort.Search(n int, f func(int) bool) int 用于在升序序列中搜索满足条件的索引 i,使得 f(i) == true,如果没有找到,则返回序列长度 n。这个函数可以用于查找某个值在已排序切片中的位置。

  9. IsSorted 函数sort.IsSorted(data sort.Interface) bool 用于检查切片是否已经按升序排序。

  10. Float64SliceIntSlice 类型:这些类型是 []float64[]int 的别名,并为它们提供了一些附加方法,如 SortSearch

这些函数和方法一起提供了对切片进行排序和搜索的强大工具,使你能够轻松地处理不同类型的数据并满足各种排序需求。

15.7.time包

time 包是 Go 语言标准库中的一个重要包,它提供了用于处理时间和日期的函数、类型和常量。time 包使得在 Go 程序中操作时间、计时和日期非常方便。

以下是 time 包中的一些常见功能和类型:

  1. Time 类型time.Time 是 Go 中表示时间的基本类型。它包含了日期和时间信息,以及与时区相关的信息。你可以使用 time.Now() 函数获取当前的时间。
import (
    "fmt"
    "time"
)

currentTime := time.Now()
fmt.Println(currentTime)
  1. 时间的格式化和解析time 包允许你将时间转换为字符串,或者从字符串解析出时间。你可以使用 Time 类型的 Format 方法来进行格式化,或使用 time.Parse 函数进行解析。
import (
    "fmt"
    "time"
)

currentTime := time.Now()
formattedTime := currentTime.Format("2006-01-02 15:04:05")
fmt.Println(formattedTime)

parsedTime, err := time.Parse("2006-01-02", "2023-09-24")
if err != nil {
    fmt.Println("解析失败:", err)
} else {
    fmt.Println(parsedTime)
}
  1. 时间的运算和比较time 包允许你对时间进行加减操作,以及进行比较。你可以计算时间间隔,或者比较两个时间的先后顺序。
import (
    "fmt"
    "time"
)

currentTime := time.Now()
oneHourLater := currentTime.Add(time.Hour)
timeDiff := oneHourLater.Sub(currentTime)

if oneHourLater.After(currentTime) {
    fmt.Println("oneHourLater 在 currentTime 之后")
}
  1. 定时器和延迟执行time 包允许你创建定时器,定时执行函数,或者实现延迟执行。
import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("开始...")
    time.Sleep(2 * time.Second) // 等待 2 秒钟
    fmt.Println("结束.")

    timer := time.NewTimer(1 * time.Second)
    <-timer.C // 等待定时器到期
    fmt.Println("定时器到期了.")

    time.AfterFunc(2*time.Second, func() {
        fmt.Println("2 秒后执行的函数.")
    })

    select {}
}
  1. 时区处理time 包允许你在不同的时区之间转换时间,并考虑夏令时等因素。
import (
    "fmt"
    "time"
)

location, err := time.LoadLocation("America/New_York")
if err != nil {
    fmt.Println("加载时区失败:", err)
} else {
    currentTime := time.Now().In(location)
    fmt.Println("纽约时间

## 15.7.json包
`encoding/json` 包是 Go 语言标准库中的一个包,用于处理 JSON(JavaScript Object Notation)格式的数据。JSON 是一种常用的数据交换格式,它通常用于在不同系统之间传递结构化数据。

`encoding/json` 包提供了一系列函数和类型,允许你在 Go 中进行 JSON 数据的编码(将 Go 数据结构转换为 JSON 字符串)和解码(将 JSON 字符串转换为 Go 数据结构)操作。以下是一些 `encoding/json` 包中的重要功能和类型:

1. **`Marshal``Unmarshal` 函数**`json.Marshal` 用于将 Go 数据结构编码为 JSON 格式的字符串,而 `json.Unmarshal` 用于将 JSON 字符串解码为 Go 数据结构。

```go
import (
    "encoding/json"
    "fmt"
)

// 结构体表示一个示例对象
type Person struct {
    Name  string
    Age   int
    Email string
}

func main() {
    person := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}

    // 编码为 JSON 字符串
    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("编码失败:", err)
        return
    }
    fmt.Println(string(jsonData))

    // 解码 JSON 字符串
    var decodedPerson Person
    err = json.Unmarshal(jsonData, &decodedPerson)
    if err != nil {
        fmt.Println("解码失败:", err)
        return
    }
    fmt.Println(decodedPerson)
}
  1. EncoderDecoder 类型json.Encoderjson.Decoder 类型允许你进行流式的 JSON 编码和解码。这对于处理大型 JSON 数据或从网络流中读取/写入 JSON 数据非常有用。
import (
    "encoding/json"
    "fmt"
    "os"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    // 创建文件用于写入 JSON 数据
    file, err := os.Create("person.json")
    if err != nil {
        fmt.Println("创建文件失败:", err)
        return
    }
    defer file.Close()

    // 创建 JSON 编码器
    encoder := json.NewEncoder(file)

    // 编码并写入 JSON 数据
    person := Person{Name: "Bob", Age: 25}
    err = encoder.Encode(person)
    if err != nil {
        fmt.Println("编码写入失败:", err)
        return
    }

    // 打开文件并创建 JSON 解码器
    file, err = os.Open("person.json")
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    defer file.Close()

    decoder := json.NewDecoder(file)

    // 解码 JSON 数据
    var decodedPerson Person
    err = decoder.Decode(&decodedPerson)
    if err != nil {
        fmt.Println("解码失败:", err)
        return
    }

    fmt.Println(decodedPerson)
}
  1. JSON 标签:通过在结构体字段上添加 JSON 标签,可以控制 JSON 编码和解码时字段的命名和行为。
import (
    "encoding/json"
    "fmt"
)

type Person struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
}

func main() {
    person := Person{FirstName: "Alice", LastName: "Smith"}

    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("编码失败:", err)
        return
    }
    fmt.Println(string(jsonData))
}
  1. 空值处理encoding/json 包默认情况下会忽略 Go 结构体中的零值字段(例如,""0)。你可以使用 omitempty 标签来控制字段是否在编码时被忽略。
import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name  string `json:"name,omitempty"`
    Age   int    `json:"age,omitempty"`
    Email string `json:"email,omitempty"`
}

func main() {
    person := Person{} // 这些字段都是零值

    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("编码失败:", err)
        return
    }
    fmt.Println(string(jsonData))
}

encoding/json 包使得在 Go 语言中处理 JSON 数据非常方便。无论是编码还是解码 JSON,以及处理不同的 JSON 数据结构,它都提供了丰富的功能和选项。这使得 Go 能够轻松地与其他服务、前端应用程序等交换 JSON 数据。

15.8.encoding/xml

encoding/xml 包是 Go 语言标准库中的一个包,用于处理 XML(Extensible Markup Language)格式的数据。XML 是一种常见的数据交换格式,通常用于在不同系统之间传递和存储结构化数据。

encoding/xml 包提供了一组函数和类型,允许你在 Go 中进行 XML 数据的编码(将 Go 数据结构转换为 XML 格式)和解码(将 XML 数据转换为 Go 数据结构)操作。以下是一些 encoding/xml 包中的关键功能和类型:

  1. MarshalUnmarshal 函数xml.Marshal 用于将 Go 数据结构编码为 XML 格式的字节切片,而 xml.Unmarshal 用于将 XML 字节切片解码为 Go 数据结构。
package main

import (
    "encoding/xml"
    "fmt"
)

type Person struct {
    XMLName xml.Name `xml:"person"`
    Name    string   `xml:"name"`
    Age     int      `xml:"age"`
}

func main() {
    person := Person{Name: "Alice", Age: 30}

    // 编码为 XML 格式的字节切片
    xmlData, err := xml.Marshal(person)
    if err != nil {
        fmt.Println("编码失败:", err)
        return
    }
    fmt.Println(string(xmlData))

    // 解码 XML 字节切片
    var decodedPerson Person
    err = xml.Unmarshal(xmlData, &decodedPerson)
    if err != nil {
        fmt.Println("解码失败:", err)
        return
    }
    fmt.Printf("%+v\n", decodedPerson)
}
  1. XML 标签:通过在结构体字段上添加 XML 标签,你可以控制 XML 编码和解码时字段的命名和行为。这类似于 JSON 标签。
type Person struct {
    XMLName xml.Name `xml:"person"`
    FirstName string `xml:"name>first"`
    LastName  string `xml:"name>last"`
}
  1. XML 文档结构encoding/xml 包支持处理包含元素、属性、命名空间等复杂的 XML 文档结构。

  2. 编码选项encoding/xml 包提供了一些选项,例如编码时的缩进、注释等。

  3. 流式编码和解码:与 xml.NewEncoderxml.NewDecoder 类型一起使用,可以实现流式的 XML 编码和解码,适用于大型 XML 文档或流式数据。

encoding/xml 包使得在 Go 语言中处理 XML 数据变得相对容易。它允许你将 XML 数据与 Go 数据结构进行转换,以便在代码中使用。这对于处理配置文件、与外部服务交互以及处理其他基于 XML 的数据交换场景非常有用。

15.9.math包

math 包是 Go 语言标准库中的一个包,提供了数学函数和常量,用于执行各种数学运算。这个包包含了基本的数学运算函数,如加法、减法、乘法、除法、幂次方、三角函数等,以及一些数学常量,如π(π)和自然对数的底数(e)。

以下是一些 math 包中常用的函数和常量:

  1. 基本数学运算

    • math.Abs(x float64) float64:返回一个浮点数的绝对值。
    • math.Ceil(x float64) float64:将一个浮点数向上取整为最接近的整数。
    • math.Floor(x float64) float64:将一个浮点数向下取整为最接近的整数。
    • math.Max(x, y float64) float64:返回两个浮点数中的较大值。
    • math.Min(x, y float64) float64:返回两个浮点数中的较小值。
    • math.Pow(x, y float64) float64:返回 x 的 y 次方。
    • math.Sqrt(x float64) float64:返回一个浮点数的平方根。
    • math.Mod(x, y float64) float64:返回 x 除以 y 的余数。
  2. 三角函数

    • math.Sin(x float64) float64:返回角度 x 的正弦值。
    • math.Cos(x float64) float64:返回角度 x 的余弦值。
    • math.Tan(x float64) float64:返回角度 x 的正切值。
  3. 数学常量

    • math.Pi:π(圆周率)的近似值。
    • math.E:自然对数的底数 e 的近似值。
  4. 角度和弧度转换

    • math.Deg2Rad:将角度转换为弧度。
    • math.Rad2Deg:将弧度转换为角度。

这只是 math 包提供的一小部分函数和常量。这个包涵盖了许多数学运算和计算,用于各种数学和科学计算任务。在编写需要进行数学操作的 Go 程序时,math 包通常会派上用场。

15.9.http包

Go语言的net/http包提供了创建HTTP服务器和客户端的功能,是构建Web应用程序和处理HTTP请求的重要工具。以下是net/http包的详细介绍以及对应示例:

1. 创建HTTP服务器

http包允许您轻松创建HTTP服务器。以下是一个简单的HTTP服务器示例,它监听在本地的端口8080,并回复 “Hello, World!” 以响应任何传入的请求:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!")
    })

    http.ListenAndServe(":8080", nil)
}

在此示例中,http.HandleFunc用于指定处理请求的处理函数,然后使用http.ListenAndServe启动HTTP服务器。

2. 处理路由

http包允许您定义不同的路由和处理函数,以根据请求的路径和方法执行不同的操作。以下是一个简单的路由示例:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Welcome to the home page!")
    })

    http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "This is the about page.")
    })

    http.ListenAndServe(":8080", nil)
}

在这个示例中,根据请求的路径不同,服务器将响应不同的消息。

3. 处理HTTP请求参数

您可以使用r.URL.Query()来获取HTTP请求中的参数。以下是一个示例,演示如何从查询参数中获取数据:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        name := r.URL.Query().Get("name")
        if name == "" {
            name = "Guest"
        }
        fmt.Fprintf(w, "Hello, %s!", name)
    })

    http.ListenAndServe(":8080", nil)
}

在这个示例中,当访问/hello?name=John时,服务器将响应 “Hello, John!”,而在没有提供参数时,将响应 “Hello, Guest!”。

4. 发送HTTP请求

net/http包还允许您创建HTTP客户端并发送HTTP请求。以下是一个示例,演示如何发送GET请求:

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    url := "https://example.com"

    response, err := http.Get(url)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer response.Body.Close()

    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("HTTP Status Code:", response.Status)
    fmt.Println("Response Body:", string(body))
}

这个示例发送GET请求到https://example.com并打印HTTP状态码和响应体内容。

这些示例只是net/http包的一部分功能,您可以使用这个包构建更复杂的HTTP服务器和客户端应用程序,以满足各种需求。查看Go语言官方文档以获取更多详细信息和示例:https://golang.org/pkg/net/http/
在这里插入图片描述

http.HandleFunc(“/index”, hello) 出现在 http.ListenAndServe(“:8081”, nil) 之前,这是完全合法和有效的顺序。这是因为在 http.ListenAndServe 启动HTTP服务器之前,您需要先设置HTTP请求处理函数,以告诉服务器当收到请求时应该执行哪些操作。

具体流程如下:

http.HandleFunc(“/index”, hello) 表示将路径 “/index” 映射到名为 hello 的处理函数。这告诉HTTP服务器当收到 “/index” 路径的请求时,执行 hello 处理函数来处理该请求。

接着,通过 http.ListenAndServe(“:8081”, nil) 启动HTTP服务器,监听端口8081。此时,服务器已经配置好了路由,知道当请求 “/index” 路径时应该调用 hello 处理函数。

所以,将 http.HandleFunc(“/index”, hello) 放在启动服务器之前是完全正确的做法。这样在服务器启动后,任何请求到 “/index” 路径都会被正确处理。
注意点:!!结构体的属性首字母要大写,应为json在转换的时候底层是通过反射将和结构体转json,只能匹配大写的属性!!

十六.golang操作数据库和表

要在 Go 中操作 MySQL 数据库,你需要完成一些准备工作。以下是一些必要的步骤:

  1. 安装 MySQL 数据库:首先,你需要在你的计算机或服务器上安装 MySQL 数据库。你可以从 MySQL 官方网站下载适合你操作系统的 MySQL 安装程序,并按照官方文档的指导进行安装。

  2. 安装 Go MySQL 驱动:你需要安装 Go 语言的 MySQL 驱动程序,以便在 Go 代码中连接和操作 MySQL 数据库。一个常用的 MySQL 驱动是 "github.com/go-sql-driver/mysql"

    你可以使用以下命令安装 MySQL 驱动:

    go get -u github.com/go-sql-driver/mysql
    
  3. 导入 MySQL 驱动:在你的 Go 代码中,导入 "database/sql""github.com/go-sql-driver/mysql" 包,以便使用 MySQL 驱动。

    import (
        "database/sql"
        _ "github.com/go-sql-driver/mysql"
    )
    

    _ 操作符用于导入一个包,但不直接在代码中使用,这是为了让 MySQL 驱动程序注册自己。

  4. 创建数据库连接:使用 sql.Open 函数创建一个与 MySQL 数据库的连接。你需要提供数据库的连接字符串,包括用户名、密码、数据库名称和其他连接参数。

    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/mydb")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()
    
  5. 测试连接:使用 db.Ping() 函数测试数据库连接是否成功。

    err = db.Ping()
    if err != nil {
        panic(err.Error())
    }
    
  6. 执行 SQL 查询:你可以使用 db.Query() 函数执行 SQL 查询语句,并使用 db.Exec() 函数执行 SQL 命令(如插入、更新或删除操作)。

    rows, err := db.Query("SELECT name, age FROM users WHERE id = ?", 1)
    if err != nil {
        panic(err.Error())
    }
    defer rows.Close()
    
    // 处理查询结果
    for rows.Next() {
        var name string
        var age int
        err = rows.Scan(&name, &age)
        if err != nil {
            panic(err.Error())
        }
        fmt.Println("Name:", name, "Age:", age)
    }
    

这些步骤完成后,你就可以在 Go 中连接到 MySQL 数据库,并执行各种 SQL 操作。记得处理错误,以确保你的代码能够应对可能出现的问题。同时,还可以考虑使用数据库连接池来提高性能和资源利用率。
在 Go 中,你可以使用 database/sql 包来执行各种数据库操作,无论你使用哪种数据库(如MySQL、PostgreSQL、SQLite等),基本的数据库操作流程都大致相同。以下是一些常见的数据库操作,包括连接数据库、查询数据、插入、更新和删除等。

连接数据库

首先,你需要建立与数据库的连接。这里以 MySQL 数据库为例:

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/mydb")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // 测试数据库连接
    err = db.Ping()
    if err != nil {
        panic(err.Error())
    }

    fmt.Println("Connected to the database")
}

查询数据

执行 SELECT 查询并读取结果:

rows, err := db.Query("SELECT name, age FROM users WHERE id = ?", 1)
if err != nil {
    panic(err.Error())
}
defer rows.Close()

for rows.Next() {
    var name string
    var age int
    err = rows.Scan(&name, &age)
    if err != nil {
        panic(err.Error())
    }
    fmt.Println("Name:", name, "Age:", age)
}

插入数据

执行 INSERT 操作:

result, err := db.Exec("INSERT INTO users (name, age) VALUES (?, ?)", "Alice", 30)
if err != nil {
    panic(err.Error())
}

affectedRows, err := result.RowsAffected()
if err != nil {
    panic(err.Error())
}

fmt.Println("Rows affected:", affectedRows)

更新数据

执行 UPDATE 操作:

result, err := db.Exec("UPDATE users SET age = ? WHERE id = ?", 31, 1)
if err != nil {
    panic(err.Error())
}

affectedRows, err := result.RowsAffected()
if err != nil {
    panic(err.Error())
}

fmt.Println("Rows affected:", affectedRows)

删除数据

执行 DELETE 操作:

result, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
if err != nil {
    panic(err.Error())
}

affectedRows, err := result.RowsAffected()
if err != nil {
    panic(err.Error())
}

fmt.Println("Rows affected:", affectedRows)

以上示例中的 SQL 语句和操作是通用的,你可以根据你所使用的数据库类型(如 MySQL、PostgreSQL、SQLite 等)进行相应的修改。要执行不同类型数据库的操作,你需要安装适当的数据库驱动,并确保在连接字符串中正确指定数据库类型。

十七.golang的断言和反射操作

反射(Reflection)是一种在运行时检查变量的类型和值的机制,以及动态修改变量值、调用方法等的能力。在Go语言中,反射是通过reflect包来实现的。下面是反射的详细介绍和示例:

反射的基本概念

反射允许您在程序运行时检查对象的类型、值和结构,并且可以根据需要创建、修改和调用对象。Go中的反射主要涉及两个类型:reflect.Typereflect.Value

  • reflect.Type 表示类型信息,可以用于检查变量的类型。
  • reflect.Value 表示变量的值,可以用于读取和修改变量的值,以及调用方法。

反射示例

以下是一个使用反射的简单示例,演示如何使用reflect包来检查和修改变量:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    // 创建一个整数变量
    num := 42
    // 获取变量的类型信息
    t := reflect.TypeOf(num)
    // 获取变量的值信息
    v := reflect.ValueOf(num)

    // 打印类型信息和值信息
    fmt.Println("Type:", t)
    fmt.Println("Value:", v)

    // 将reflect.Value转换回原始类型并修改值
    newValue := v.Interface().(int)
    newValue += 10
    // 使用反射设置新的值
    v.SetInt(int64(newValue))

    fmt.Println("New Value:", num)
}

在这个示例中,我们创建了一个整数变量num,然后使用反射获取了它的类型和值信息,最后修改了变量的值并打印出来。要注意的是,反射操作可能会导致性能损失,因此应该谨慎使用。

使用反射调用方法

您还可以使用反射来调用结构体的方法。以下是一个示例,演示如何使用反射调用结构体的方法:

package main

import (
    "fmt"
    "reflect"
)

type MyStruct struct {
    X int
    Y string
}

func (s MyStruct) Hello() {
    fmt.Println("Hello from MyStruct!")
}

func main() {
    myInstance := MyStruct{X: 42, Y: "Hello"}

    // 获取结构体实例的类型信息
    t := reflect.TypeOf(myInstance)
    // 获取结构体实例的值信息
    v := reflect.ValueOf(myInstance)

    // 调用结构体的方法
    method := v.MethodByName("Hello")
    method.Call(nil)
}

在这个示例中,我们首先使用反射获取了结构体实例的类型和值信息,然后使用MethodByName方法获取方法的反射对象,并使用Call方法调用该方法。

总之,反射是一个强大的工具,但也需要小心使用,因为它使代码更加复杂,并且可能降低性能。只有在必要时才应该使用反射,例如在编写通用代码库或处理未知类型的数据时。在正常情况下,应该尽量避免使用反射。
断言(Assertion)是一种在编程中检查接口值的实际底层类型的机制。在Go语言中,通常使用类型断言来判断接口值的实际类型,并将其转换为具体的类型以进行操作。这通常用于与接口类型(如interface{})一起工作,以确定它们包含的具体类型。以下是Go语言中的类型断言示例:

package main

import "fmt"

func main() {
    var i interface{} = 42

    // 使用类型断言检查接口值的底层类型
    if val, ok := i.(int); ok {
        fmt.Println("i 是一个整数:", val)
    } else {
        fmt.Println("i 不是一个整数")
    }

    // 类型断言失败的情况
    var str interface{} = "Hello"
    if val, ok := str.(int); ok {
        fmt.Println("str 是一个整数:", val)
    } else {
        fmt.Println("str 不是一个整数")
    }
}

在这个示例中,我们首先创建了一个空接口变量i,然后使用类型断言检查它是否包含一个整数。在第二个示例中,我们尝试将一个字符串断言为整数,但由于类型不匹配,断言失败。

类型断言的一般形式是 x.(T),其中 x 是接口值,T 是断言的目标类型。如果断言成功,将返回目标类型的值和true;如果断言失败,将返回零值和false。通常,我们使用带有if语句来检查并处理断言的成功或失败。

请注意,如果不进行类型断言而直接尝试将接口值转换为错误的类型,会导致运行时恐慌。因此,在进行类型转换之前,始终建议使用类型断言来检查接口值的类型。
在Go语言中,类型断言的一般格式如下:

value, ok := x.(T)
  • x 是要进行类型断言的接口值。
  • T 是目标类型,表示您期望x包含的类型。

这个类型断言会尝试将接口值x转换为目标类型T。如果转换成功,value将包含转换后的值,而ok将为true。如果转换失败,value将包含目标类型的零值,而ok将为false

通常,我们使用带有if语句来检查和处理断言的结果,以确保安全的类型转换。以下是一个示例:

var i interface{} = 42

if val, ok := i.(int); ok {
    fmt.Println("i 是一个整数:", val)
} else {
    fmt.Println("i 不是一个整数")
}

在这个示例中,我们首先检查i是否包含一个整数类型,如果是,就将其转换为int并打印出来。如果不是,我们就会打印出一条不是整数的消息。

这种方式可以避免在类型转换时出现运行时恐慌,因此是一种更安全的类型转换方法。
反射(Reflection)是Go语言中强大的机制,允许您在运行时检查和操作程序的结构、类型和值。反射包括许多重要的函数和方法,以下是一些主要的反射函数和它们的用法:

  1. reflect.TypeOf(interface{}) reflect.Type

    用于获取接口值中的实际类型信息,返回一个reflect.Type,您可以使用该类型进行类型检查。

    var x interface{} = 42
    t := reflect.TypeOf(x)
    fmt.Println(t) // 输出: int
    
  2. reflect.ValueOf(interface{}) reflect.Value

    用于获取接口值的reflect.Value,通过它可以访问接口值的底层值并进行操作。

    var x interface{} = 42
    v := reflect.ValueOf(x)
    fmt.Println(v.Int()) // 输出: 42
    
  3. reflect.New(reflect.Type) reflect.Value

    创建一个新的指向类型的零值的reflect.Value

    t := reflect.TypeOf(42)
    v := reflect.New(t) // v 包含了 int 类型的零值指针
    
  4. Value.Interface() interface{}

    reflect.Value获取接口值。

    var x interface{} = 42
    v := reflect.ValueOf(x)
    x2 := v.Interface().(int)
    
  5. reflect.Value.MethodByName(string) reflect.Value

    用于按照方法名查找并返回方法的reflect.Value,以便进行调用。

    type MyStruct struct{}
    func (s MyStruct) MyMethod() {
        fmt.Println("Hello from MyMethod")
    }
    
    v := reflect.ValueOf(MyStruct{})
    method := v.MethodByName("MyMethod")
    method.Call(nil) // 调用 MyMethod
    
  6. reflect.Value.Set(reflect.Value)

    用于设置reflect.Value的值。

    var x int = 42
    v := reflect.ValueOf(&x) // v 包含指向 x 的指针
    newValue := reflect.ValueOf(10)
    v.Elem().Set(newValue) // 修改 x 的值为 10
    

这只是反射包的一些主要函数和方法。反射功能非常强大,但也需要小心使用,因为它可能会导致性能损失和代码复杂性增加。反射通常用于编写通用库、解析JSON或XML、动态创建对象等特殊情况。在通常情况下,尽量避免使用反射,而是采用静态类型检查的方式编写代码。

十八.golang的单元测试

go test 命令是一个按照一定约定和组织的代码驱动程序,在包目录中,所有以_test.go为后缀的源码文件都会被go test运行到
我们写的_test.go 源码文件不用担心内容过多,因为go build 命令不会将这些测试文件打包到最后的可执行文件中
test文件有4类,Test开头的,功能测试Benchmark开头的 性能测试 example模糊测试
以下是一个简单的 Go 语言单元测试示例,包括一个要测试的函数和相应的测试代码。在这个示例中,我们将编写一个函数来计算两个整数的和,然后编写测试来确保它的行为正确。

假设你有一个名为 math.go 的源代码文件,其中包含了一个 Add 函数,如下所示:

// math.go
package mymath

func Add(a, b int) int {
    return a + b
}

接下来,你可以创建一个与源代码文件相同的目录,并在该目录下创建一个名为 math_test.go 的测试文件。

// math_test.go
package mymath

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Expected %d, but got %d", expected, result)
    }
}

在这个示例中,我们编写了一个名为 TestAdd 的测试函数,该函数使用 Add 函数来计算 2 和 3 的和,并将结果与预期值进行比较。如果结果不等于预期值,它会使用 t.Errorf 函数来输出错误消息。

要运行这个测试,你可以使用以下命令:

go test

Go 工具会自动查找并运行以 _test.go 结尾的文件中的测试函数。在测试运行完毕后,你将看到测试结果的汇总,包括测试通过的数量和测试失败的数量。

这只是一个非常简单的示例,实际项目中,你可能需要编写更多的测试用例以覆盖不同的情况,并确保代码的质量和可维护性。你可以使用 Go 的测试框架来编写更复杂的
Go 的测试框架支持 short 模式,它允许你跳过一些测试,特别是那些运行时间较长的测试。你可以通过在测试函数名中添加 _test 来标记测试函数为 short 模式,然后在运行测试时使用 -short 标志来跳过这些测试。

以下是一个示例,演示如何在 Go 中使用 short 模式来跳过测试时间长的测试:

首先,创建一个名为 math.go 的源代码文件,其中包含了一个要测试的函数,如下所示:

// math.go
package mymath

import "time"

func LongRunningOperation() int {
    // 模拟长时间运行的操作
    time.Sleep(5 * time.Second)
    return 42
}

接下来,创建一个名为 math_test.go 的测试文件,其中包含测试长时间运行操作的测试函数,并将其标记为 short 模式:

// math_test.go
package mymath

import "testing"

func TestLongRunningOperation(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping long-running test in short mode.")
    }

    result := LongRunningOperation()
    expected := 42

    if result != expected {
        t.Errorf("Expected %d, but got %d", expected, result)
    }
}

在上面的测试函数中,我们首先检查 testing.Short() 函数的返回值,如果处于 short 模式,则使用 t.Skip() 跳过测试。否则,它将继续执行测试。

要运行测试并跳过 short 模式的测试,你可以使用以下命令:

go test -short

在 short 模式下,通过 -short 标志运行测试时,将跳过标记为 short 模式的测试函数,从而避免长时间运行的测试对测试套件的运行时间产生不必要的影响。这对于快速运行测试套件非常有用,而不必等待长时间运行的测试完成。
表格驱动测试是一种在 Go 中编写测试的模式,其中你使用测试数据表格来覆盖各种输入和期望输出情况。这可以使你更轻松地编写多个测试用例,并确保你的代码在各种情况下都能正确运行。以下是一个示例,演示如何在 Go 中使用表格驱动测试:

首先,创建一个名为 math.go 的源代码文件,其中包含了一个要测试的函数,如下所示:

// math.go
package mymath

func Add(a, b int) int {
    return a + b
}

接下来,创建一个名为 math_test.go 的测试文件,其中包含表格驱动测试函数:

// math_test.go
package mymath

import (
    "testing"
)

func TestAdd(t *testing.T) {
    testCases := []struct {
        a, b, expected int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
        {10, -5, 5},
    }

    for _, tc := range testCases {
        result := Add(tc.a, tc.b)
        if result != tc.expected {
            t.Errorf("Add(%d, %d) = %d; expected %d", tc.a, tc.b, result, tc.expected)
        }
    }
}

在上面的示例中,我们创建了一个名为 testCases 的测试数据表格,包含了多个测试用例,每个测试用例都有两个输入和一个预期输出值。然后,我们使用 for 循环迭代测试数据表格,依次对每个测试用例运行 Add 函数,并比较结果与预期输出。

要运行这个表格驱动测试,使用以下命令:

go test

Go 将执行测试并汇总结果,显示每个测试用例的成功或失败。这种模式使得更容易添加新测试用例,并确保代码在各种情况下都能正确运行。

在 Go 中进行性能测试是一种重要的实践,可以帮助你识别和解决性能问题。Go 的标准库提供了一种内置的性能测试工具,你可以使用 testing 包的 Benchamrk 函数来编写和运行性能测试。以下是一个简单的示例,演示如何在 Go 中进行性能测试:

// math.go
package mymath

func add(a, b int) int {
	return a + b
}
  1. 创建一个名为 math_test.go 的性能测试文件,使用 testing.B 类型的参数来编写性能测试。
// 性能测试

func BenchmarkAdd(bb *testing.B) {

	var a, b, c int
	a = 123
	b = 456
	c = 789

	for i := 0; i < bb.N; i++ {
		if actual := add(a, b); actual != c {
			fmt.Printf("期待%d,实际%d", c, actual)
		}
	}
}


  1. 使用 go test 命令和 -bench 标志来运行性能测试。
go test -bench=.

Go 将运行性能测试,并输出性能测试的结果,包括每个迭代的平均执行时间和迭代次数。

在性能测试中,你可以根据需要测试不同的输入值,以评估函数的性能。你还可以使用 -benchmem 标志来测量内存分配情况,以便进一步优化代码。

性能测试是一个重要的工具,帮助你发现代码中的性能问题并进行优化。但要记住,性能测试结果可能受多个因素影响,包括硬件、操作系统和编译器等。因此,要在实际环境中进行性能测试以获取更准确的性能数据。

性能测试2:字符串拼接


const numbers = 10000

func BenchmarkStringSprintf(b *testing.B) {
	// b.ResetTimer() 是一个重要的函数调用,用于重置计时器。这是因为在性能测试中,Go 测试框架会自动测量代码的执行时间,
	// 但这个计时器在测试函数开始时已经开始计时。如果你希望排除一些初始化代码的执行时间,以便更准确地测量目标函数的性能,就需要使用 b.ResetTimer()。
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var str string
		for j := 0; j < numbers; j++ {
			str = fmt.Sprintf("%s%d", str, j)
		}
	}
	// b.StopTimer() 用于停止计时器
	b.StopTimer()
}
func BenchmarkStringAdd(b *testing.B) {

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var str string
		for j := 0; j < numbers; j++ {
			str = str + strconv.Itoa(j)
		}
	}
	b.StopTimer()
}

func BenchmarkStringBuilder(b *testing.B) {

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var builder strings.Builder
		for j := 0; j < numbers; j++ {
			builder.WriteString(strconv.Itoa(j))
		}
		_ = builder.String()
	}
	b.StopTimer()
}

builder拼接字符串性能最高,结果如下
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值