Go入门笔记 / Go beginner

Go入门笔记

Go是编译型语言:

Go 代码通过 go build 或 go run 等命令来编译成二进制文件。尽管 Go 允许使用 go run 直接执行代码,但背后其实是先将代码编译成二进制文件再执行,而非逐行解释执行。

Go语言常用的命名规范

在 Go 中,导出的(exported)函数名应该以大写字母开头,而 未导出的(unexported)函数名则以小写字母开头。

  • Hello: 这是一个导出的函数(首字母大写),因此它可以被包外的代码调用。导出的函数应该遵循 Go 的首字母大写命名规则。
  • randomFormat: 这是一个未导出的函数(首字母小写),只能在当前包内使用。未导出的函数名通常首字母小写。

Tutorial: Get started with Go

When you need your code to do something that might have been implemented by someone else, you can look for a package that has functions you can use in your code.

Tutorial: Create a Go module

1. Relation between module and package

In a module, you collect one or more related packages for a discrete and useful set of functions.

2. How to name a module

A module's name should look like this:

<prefix>/<descriptive-text>

The prefix is typically a string that partially describes the module, such as a string that describes its origin. This might be:

The location of the repository where Go tools can find the module’s source code (required if you’re publishing the module).

For example, it might be github.com/<project-name>/.

Use this best practice if you think you might publish the module for others to use. For more about publishing, see Developing and publishing modules.

If you’re not using a repository name, be sure to choose a prefix that you’re confident won’t be used by others. A good choice is your company’s name. Avoid common terms such as widgets, utilities, or app.

For the descriptive text, a good choice would be a project name. Remember that package names carry most of the weight of describing functionality. The module path creates a namespace for those package names.

3. 常见占位符和字符串表示

  • %v:%v 是最常用的占位符之一,它会根据值的类型来选择最合适的格式进行输出。它可以用于任何类型的数据(int、float、string、struct、slice 等)。对于复合类型(如结构体),%v 会输出字段值的默认格式。如果你想要更详细的输出,可以使用 %+v,这会在输出时包含字段名。
  • %q:用于格式化字符串,表示带有双引号的字符串。例如:"Hello"
  • %#q:格式化成带有转义的字符串,并且带上双引号。与 %q 相比,%#q 会转义所有不可打印的字符,例如换行符、制表符等。

另外,如果要打印的目标字符串当中含有双引号,那么可以使用反引号`来包围目标字符串,避免冲突。

而且,反引号包含的字符串是原始字符串,内部的字符都会被逐字解释,不会被转义。

Python当中,双引号和单引号都能用来表示字符串,但是Go当中只能用双引号;

Go中单引号只能用来表示单个字符,Go中单个字符的数据类型是rune,本质上是int32的别名;而C语言中char是int8。

示例:

want := regexp.MustCompile(`\b` + name + `\b`)

这里我们定义了一个正则表达式want,假设name变量是zzf,那么我们在括号里得到的就是`\bzzf\b`,当正则表达式引擎接受到这个字符串时,\字符被解析为转义字符,把b转义成:word boundary

4. 命名空间

每个package都是一个独立的命名空间,使用来自其他module中的函数时,要在函数名前加上相应package的限定,类似于C++的命名空间。

5.Go的指针

Go 和 C 语言中的指针对比

1. 基本概念(相同点)

  • Go 和 C 都支持指针:
    • 在 Go 和 C 中,指针都是用于存储另一个变量的内存地址。
    • 通过指针,你可以间接访问或修改指向的变量的值。
  • 获取指针和解引用:
    • 在 Go 和 C 中,符号 & 用于获取变量的地址,符号 * 用于通过指针访问变量的值。

Go 示例:

var a int = 10
var p *int = &a  // p 是一个指向 a 的指针
fmt.Println(*p)  // 解引用 p,输出 a 的值,即 10

C 示例:

int a = 10;
int *p = &a;  // p 是一个指向 a 的指针
printf("%d\n", *p);  // 解引用 p,输出 a 的值,即 10

2. 主要区别

1. 没有指针运算

Go: Go 不允许对指针进行算术运算。例如,不能对指针进行 p++p-- 这样的操作。

C: C 允许指针运算,能够通过指针运算遍历数组、内存块等。这在 C 中是一个重要的特性,用于内存管理和数组操作。

C 示例:

int arr[3] = {1, 2, 3};
int *p = arr;
p++;  // 通过指针算术运算访问下一个元素
printf("%d\n", *p);  // 输出 2

Go 示例:

var arr = [3]int{1, 2, 3}
var p *int = &arr[0]
p++  // 编译错误:Go 不支持指针运算
2. 安全性

Go: Go 语言在设计时更加关注内存安全,自动管理内存,并且有垃圾回收机制。指针操作受到更多的限制,防止出现像 C 中常见的悬空指针(dangling pointer)和内存泄漏等问题。

C: C 语言中,指针的使用更加灵活,但也更容易出错,导致内存泄漏、野指针等问题。C 中的内存管理是手动的,程序员需要负责释放动态分配的内存。

3. 垃圾回收

Go: Go 有自动垃圾回收机制,程序员不需要手动释放内存,指针所指向的对象在没有任何引用后会被垃圾回收。

C: C 没有垃圾回收,程序员需要使用 malloccalloc 动态分配内存,并在不需要时使用 free 手动释放内存。否则,会造成内存泄漏。

4. 零值指针

Go: Go 中的指针类型有一个默认的零值 nil,表示这个指针没有指向任何对象。Go 中的 nil 类似于 C 中的 NULL,但 Go 提供了对 nil 的更严格检查。

C: C 中未初始化的指针是一个悬空指针,它没有指向任何有效的内存地址。虽然 C 中也有 NULL 表示空指针,但未初始化的指针容易引发问题。

Go 示例:

var p *int  // 默认值是 nil
if p == nil {
    fmt.Println("Pointer is nil")
}

C 示例:

int *p;  // p 是一个未初始化的悬空指针
if (p == NULL) {
    printf("Pointer is NULL\n");
}
5. 传递指针的方式

Go: Go 在函数参数传递中默认是值传递。如果你想修改传入的值,必须使用指针。

C: C 也有类似的值传递和指针传递的机制。通过指针传递变量地址可以在函数内部修改变量的值。

Go 示例:

func increment(val *int) {
    *val++
}

var a = 5
increment(&a)
fmt.Println(a)  // 输出 6

C 示例:

void increment(int *val) {
    (*val)++;
}

int a = 5;
increment(&a);
printf("%d\n", a);  // 输出 6

3. 小结

特性 Go C
指针运算 不支持指针运算 支持指针运算,例如 p++, p--
内存管理 自动垃圾回收,无需手动释放 手动管理内存,使用 malloc/free
指针初始化 未初始化的指针默认为 nil 未初始化指针可能导致悬空指针
内存安全性 严格的内存安全检查 指针操作容易导致内存泄漏和野指针
传递指针修改变量 通过指针传递引用并修改 通过指针传递引用并修改

总结来说,Go 语言在指针的设计上更加简洁、安全,减少了直接操作内存的复杂性,并自动管理内存,避免了许多 C 语言中常见的内存管理问题。而 C 语言中的指针功能更为灵活,但同时也更容易出现内存相关的错误。

6.编写测试

Go的测试文件有命名规范:

需要以_test.go结尾。

Go的测试函数有命名规范:

标准形式为:TestName。

其中,Name用来描述它测试什么。

且所有的测试函数都要传入一个测试指针,例如:


func TestHelloName(t *testing.T)

7.编译和安装应用程序

go run和go build的运行结果

在Go语言中,go run命令不会直接生成一个持久的二进制可执行文件,但它仍然会编译代码并运行。其背后原理是这样的:

  1. 编译过程:当你使用go run运行Go程序时,Go编译器实际上会首先将你的Go代码编译成一个临时的二进制可执行文件。这是和C语言类似的步骤,只不过这个二进制文件存放在系统的临时目录中,而不是你项目目录下。

  2. 执行:编译完成后,Go会立即运行这个临时生成的二进制文件。程序执行结束后,临时的可执行文件会被删除,所以你在文件系统中看不到它。

相比于手动使用go build生成二进制文件,go run命令可以让你在开发阶段更方便地进行测试,因为它简化了“编译-执行”这两个步骤,并自动管理临时文件的创建和删除。

在C语言中,传统的编译过程往往会手动生成一个可执行文件(比如a.out),然后你需要明确地运行它。而Go则通过go run命令简化了这个流程。

go install安装的位置

  • 如果 GOBIN 设置了,go install 会将可执行文件安装到 GOBIN 指定的目录。
  • 如果 GOBIN 没有设置,go install 会将可执行文件安装到 $GOPATH/bin 目录下,默认路径通常为 $HOME/go/bin%USERPROFILE%\go\bin

Getting started with multi-module workspaces

  1. Go work use: 该命令传入的目录如果不存在,Go就会在.work文件里删除相应的行。

  2. Go work sync:(官网说明)go work sync syncs dependencies from the workspace’s build list into each of the workspace modules

     

  3. 举例说明 go work sync 的用法

    假设你有一个工作空间,其中包含两个 Go 模块:moduleAmoduleB。这两个模块可能依赖于同一个第三方库 libraryC,但是它们各自在 go.mod 文件中指定了不同版本的 libraryC

    1. 创建 go.work 文件: 首先,在你的工作空间的根目录下创建一个 go.work 文件。在这个文件中,你可以使用 use 指令来引用这两个模块:

      use ( ./moduleA ./moduleB )
    2. 运行 go work sync: 当你在工作空间目录下运行 go work sync 命令时,Go 工具链会检查 moduleAmoduleB 中列出的依赖,并将它们同步,确保所有模块都使用一致的依赖版本。这意味着如果 moduleAmoduleB 中对 libraryC 的依赖版本不同,go work sync 将帮助解决这种版本冲突,使得两个模块能够在构建时引用相同版本的 libraryC

    3. 结果: 执行该命令后,工作空间内的所有模块将更新它们的 go.mod 文件,以反映相互一致的依赖版本。这样,无论你在工作空间中的哪个模块下工作,使用的依赖都将是同步的,从而避免了版本冲突带来的问题。

    这个功能特别适用于大型项目或多模块项目,其中模块间可能因依赖不同版本的相同库而导致构建或运行时错误。通过使用 go work sync,可以有效地管理和协调这些模块之间的依赖关系,确保项目的整体一致性和可维护性。

  4. go mod中与go work sync类似的命令

    在 Go 语言的模块系统中,go mod 提供了管理模块依赖的各种命令。如果你在查找与 go work sync 功能相似的 go mod 命令,你可能想了解的是如何同步或更新模块的依赖信息。这里有几个 go mod 命令,它们与 go work sync 在功能上有一定的相似性,主要用于管理和更新模块的依赖。

    1. go mod tidy

    这个命令用于清理你的模块的 go.mod 文件和 go.sum 文件,移除不再需要的依赖,并添加缺失的依赖。这有助于保持模块依赖的清晰和最新状态。

     
    go mod tidy

    go mod tidy 会确保 go.mod 文件中的依赖项准确反映了代码中实际引用的依赖。如果项目中的代码没有引用某个依赖,该命令会从 go.mod 文件中移除该依赖。同时,它也会检查缺失的依赖项,并尝试下载和添加这些依赖到 go.mod 文件中。

    2. go mod download

    这个命令用于下载 go.mod 文件中列出的所有依赖到本地缓存中。

     
    go mod download

    该命令主要用于预下载依赖包,确保在构建或运行代码之前,所有必要的依赖都已经下载到本地缓存中。这对于确保构建过程的顺利进行非常有帮助,特别是在网络环境不稳定的情况下。

    3. go mod verify

    这个命令用来检查当前模块的依赖是否已经下载并且下载的内容是否没有被篡改过。

     
    go mod verify

    go mod verify 确保所有已下载的依赖文件与它们的 go.sum 文件中的哈希值相匹配,从而验证依赖的完整性和安全性。

    这些命令虽然不是直接与 go work sync 完全一样,但它们在管理和维护 Go 项目的依赖方面提供了相似的功能,帮助开发者保持依赖的更新和正确性。

Accessing a relational database

1. 在powershell当中设置环境变量

set DBUSER=root这是cmd风格的环境变量赋值,powershell需要改用$env:DBUSER = "root",同样地,要查看环境变量,也与cmd当中不同,需要这样查看echo $env:DBUSER

2. defer关键字的使用

defer 用于在函数结束时自动执行一段代码,无论函数是正常返回还是遇到错误提前结束。它常用于资源释放、文件关闭、解锁等需要在函数退出时执行的操作。

语法简单,直接在函数中调用 defer 即可:

 
func example() { defer fmt.Println("执行完毕!") // 在函数退出时才会执行 fmt.Println("正在处理...") }
  • defer 语句会将其后的函数延迟执行,直到所在函数的所有代码执行完毕(包括遇到 return 语句后)。
  • 多个 defer 语句按栈的顺序执行(即后进先出)。

常见场景:关闭文件、数据库连接、解锁互斥锁等。

 
func fileOperation() { f, err := os.Open("example.txt") if err != nil { log.Fatal(err) } defer f.Close() // 确保文件在函数结束时关闭 // 文件操作代码 }

defer 使代码更加简洁可靠,减少手动管理资源的负担。

3.MYSQL的占位符

mysql的格式化字符串使用问号作为占位符

4.各个错误处理返回错误的类型

1. db.Query

db.Query("SELECT * FROM album WHERE artist = ?", name) 是一个用于查询数据库的函数,它可能在以下情况返回 error

  • 数据库连接问题:如果数据库连接已关闭或不可用,db.Query 会返回错误。
  • SQL 语句语法错误:如果 SQL 语句的语法有误,例如表名拼写错误或 SQL 语法不正确,会导致查询失败并返回错误。
  • SQL 参数错误:例如在 ? 参数中插入了不支持的类型,或者绑定的参数格式不正确。
  • 数据库权限问题:如果执行该查询的用户没有权限访问 album 表,也会导致错误。
  • 其他数据库内部错误:比如服务器内部故障、超时等问题,可能会导致 db.Query 返回错误。

2. rows.Err

rows.Err() 用来检查在迭代 rows 时是否发生了错误。它可能在以下情况返回 error

  • 扫描行时发生错误:在循环 for rows.Next() 时,rows.Scan() 可能会出现问题,比如尝试将数据库中的值映射到错误类型的字段。如果这些错误没有直接抛出,它们会在 rows.Err() 中返回。
  • 结果集传输错误:如果在从数据库获取结果的过程中(例如网络中断或数据库连接中断),出现问题,会在 rows.Err() 中捕获到这些错误。
  • 迭代过程中数据库错误:如果在结果集迭代过程中数据库内部出现问题,可能也会导致 rows.Err() 返回错误。

总结:

  • db.Query查询数据库时遇到问题会返回 error
  • rows.Err遍历结果集的过程中如果遇到问题会返回 error

5. :=赋值方式的用法

:=通常用于声明并初始化变量,并不能单独作用于一个已经声明过的变量;

如果同时对多个变量赋值,只要其中有一个新变量,就能够正常赋值

6.Scan和Scanln行为比较

在 Go 语言中,fmt.Scanlnfmt.Scan 都用于读取用户输入,但它们在处理输入的方式上存在一些重要的区别。

1. fmt.Scanln

  • 工作原理Scanln 读取用户输入,直到遇到**换行符(\n)**为止。在遇到换行符时,它会停止读取,确保每次只能读取一行。
  • 特性Scanln 要求用户在每次输入后按下回车键。输入项之间的分隔符可以是空格换行符,但它在输入末尾必须有换行符。
  • 行为:如果在一行输入中,存在未读取的多余内容,Scanln 会返回错误并跳过这些多余的内容。

示例

var name string var age int fmt.Println("Enter your name and age:") fmt.Scanln(&name, &age) fmt.Printf("Name: %s, Age: %d\n", name, age)

在这里,Scanln 会读取一行输入,然后解析这行中的 nameage

2. fmt.Scan

  • 工作原理Scan 从输入中逐个读取空格分隔的项目,直到输入结束。它会继续读取,直到遇到符合要求的数量的输入项,或者输入被完全消耗。
  • 特性Scan 允许从输入中读取多个项,输入项之间可以通过空格换行符分隔。与 Scanln 不同的是,它不关心是否有换行符,它可以跨行读取数据。
  • 行为Scan 会自动跳过输入中的换行符,继续读取下一个值,直到输入结束。

示例

var name string var age int fmt.Println("Enter your name and age:") fmt.Scan(&name, &age) fmt.Printf("Name: %s, Age: %d\n", name, age)

这里的 Scan 可以处理输入项的分隔符为空格或换行符,并且不要求在每次输入后按下回车键。

区别总结:

  • 输入处理
    • Scanln:读取一行输入,遇到换行符停止,读取不完整时返回错误。
    • Scan:逐个读取空格或换行符分隔的输入,遇到空格或换行符不会停止,可以跨行读取。
  • 应用场景
    • Scanln:适合每次处理一行完整输入的情况。
    • Scan:适合连续从多行输入中读取数据。

例子对比:

1. Scanln 行为:

var name string fmt.Print("Enter your name: ") fmt.Scanln(&name) // 输入:John Smith fmt.Println("Name:", name) // 输出只会读取到 "John"

由于 Scanln 读取一行并在遇到空格后只读取第一个输入,后面的输入会被忽略。

2. Scan 行为:

var name, surname string fmt.Print("Enter your full name: ") fmt.Scan(&name, &surname) // 输入:John Smith fmt.Println("Full Name:", name, surname) // 输出:John Smith

Scan 会自动读取所有空格分隔的输入,即使它们来自同一行或不同行。

在 Go 语言中,使用 fmt.Scan 进行输入时,终止输入的方式取决于具体的输入环境和操作系统:

  1. 在命令行(终端/控制台)中

    • Windows:可以按下 Ctrl + Z,然后按 Enter 来终止输入。这会发送 EOF(End of File)信号,表示输入结束。
    • Linux/macOS:可以按下 Ctrl + D 来终止输入,表示 EOF 信号。
  2. 对于多行输入的情况: 如果你使用 fmt.Scan 从终端中读取多行输入,那么直到读取到所需数量的变量,或者收到 EOF 信号时输入才会停止。

如果输入的内容达到了Scan期望的数量,Scan就会自动终止,例如Scan的括号里只指定了两个变量,那么读取到两个以后就会自动终止

Developing a RESTful API with Go and Gin

1.关于slice的初始化

如下的语句

var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

显然它是一个声明和赋值的结合,但是它不能写成

albums := { {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, }

虽然:=在一般情况下可以自动识别数据类型,但用在slice身上还是需要指明数据类型的,所以上面的正确写法应该是

albums := []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

2.Gin.H快捷方式

Gin 框架(一个 Go 语言的轻量级 HTTP web 框架)中,gin.H 是一个快捷方式,用于创建返回给客户端的 JSON 数据。它本质上是一个map[string]interface{} 类型的别名,用于简化返回 JSON 数据时的键值对构建。

具体定义:

type H map[string]interface{}
  • map[string]interface{}:表示一个键为字符串(string),值为任意类型(interface{})的映射。这意味着你可以在 gin.H 中存储任何类型的值,包括 stringintfloatstruct 等。

用法示例:

1. 在 Gin 中返回 JSON 数据:
package main import ( "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/albums", func(c *gin.Context) { c.JSON(200, gin.H{ "id": 1, "title": "Blue Train", "artist": "John Coltrane", "price": 56.99, }) }) router.Run("localhost:8080") }

在这个例子中:

  • gin.H 允许你以简洁的方式构建一个 map[string]interface{},然后将其作为响应的 JSON 数据返回给客户端。
  • c.JSON(200, gin.H{...}) 是 Gin 中用来发送 JSON 响应的常见用法。
2. 返回更多复杂的数据类型:

你可以在 gin.H 中嵌套其他复杂的数据类型,比如数组、切片、struct 等。

c.JSON(200, gin.H{ "status": "success", "data": gin.H{ "id": 1, "title": "Blue Train", "artist": "John Coltrane", "tracks": []string{"Moment's Notice", "Blue Train", "Locomotion"}, }, })

gin.H 的优势:

  1. 简洁性gin.Hmap[string]interface{} 的别名,使用时不需要显式声明复杂的 map[string]interface{},可以直接传递键值对,代码更加简洁。
  2. 灵活性:由于 interface{} 是 Go 中的万能类型,gin.H 可以存储任何类型的数据,方便构建灵活的 JSON 响应。
  3. 易读性:对于返回 JSON 数据来说,gin.H 使代码更加直观和易读。

总结:

  • gin.H 是 Gin 框架中一个便捷的工具,用来构建 JSON 数据。
  • 它是 map[string]interface{} 的别名,帮助开发者以更加简洁的方式生成并返回 JSON 响应。

3.Go当中的interface

1. 其他编程语言中的接口(如 Java、C#)

在大多数面向对象编程语言中,接口(Interface) 通常是一个契约,它定义了一组方法,这些方法必须在实现该接口的类中被实现。接口本质上是一种行为规范,指定了实现该接口的类需要提供哪些行为。

例如,在 Java 中,接口是定义一组方法的契约,具体类需要实现这些方法:

 
interface Animal { void makeSound(); } class Dog implements Animal { public void makeSound() { System.out.println("Bark"); } }

在这种情况下,接口 Animal 定义了 makeSound 方法,类 Dog 必须实现这个方法。

2. Go 中的接口

在 Go 中,接口 也是一种行为规范,但是与其他语言不同的是,它更加灵活和动态。Go 语言的接口不仅可以用来定义特定行为,还可以用来描述任意类型的变量——这就是 interface{} 的特殊作用。

Go 的接口:

  • 在 Go 中,接口是一个类型,描述了一个对象可以做什么(即它可以调用哪些方法)。
  • 一个类型满足某个接口,意味着它实现了该接口的所有方法,而无需显式声明“实现”关系。

interface{} 是什么?

  • interface{} 是 Go 中的一个特殊的接口类型,表示可以存储任何类型的值。它相当于一种“万能类型”,可以容纳任何类型的值,因为在 Go 中,所有类型都至少实现了空接口 interface{}
 
var i interface{} // i 可以是任意类型 i = 42 // i 是 int 类型 i = "hello" // i 是 string 类型 i = 3.14 // i 是 float64 类型

为什么 interface{} 能存储任意类型?

在 Go 中,任何类型都隐式地实现了空接口 interface{},因为空接口没有任何方法,因此任何类型都满足这个条件。这使得 interface{} 成为一个灵活的容器,可以存储任意类型的数据。

Go 中接口的用途:

  1. 行为规范:Go 中的接口可以像其他语言一样,用于定义行为规范。任何实现了接口中所有方法的类型,都被认为实现了该接口。

     
    type Animal interface { Speak() string } type Dog struct{} func (d Dog) Speak() string { return "Woof!" } var a Animal = Dog{} // Dog 类型实现了 Animal 接口 fmt.Println(a.Speak()) // 输出:Woof!
  2. 空接口 interface{}:可以用来存储和传递任何类型的数据,这在处理动态数据(如 JSON 解析,或者泛型函数)时非常有用。

     
    func PrintAnything(i interface{}) { fmt.Println(i) // 可以打印任意类型 } PrintAnything(42) // 输出:42 PrintAnything("hello") // 输出:hello PrintAnything(3.14) // 输出:3.14

interface{} 和其他语言中的类型对比:

  • Go 的 interface{} 类似于其他语言中的 Object 类型,例如 Java 或 C# 中的 Object,因为它可以存储任何类型的值。
  • 然而,Go 的接口更加灵活,因为它不仅仅是一个基类,而且还可以描述行为,甚至用于泛型编程的某种形式(在泛型正式引入之前)。

总结:

  • Go 中,interface{} 是一个特殊的空接口类型,可以用来存储任意类型的值。这与其他编程语言中的接口有所不同。
  • 其他编程语言中的接口(如 Java 或 C#)通常是用于定义一组必须实现的行为,而在 Go 中,接口不仅可以定义行为,还可以通过 interface{} 处理任意类型的数据。

4.路由数量的计算

1. 路由定义的处理器(Handler)数量

每个路由可以有一个或多个处理器函数。在 Gin 中,你可以为一个路由注册多个处理器函数,它们会按顺序执行。因此,处理器的数量首先取决于你为该路由指定了多少个处理器。

例如:

router.GET("/albums", middleware1, middleware2, getAlbums)

在这个例子中,/albums 路由注册了 3 个处理器函数:middleware1middleware2getAlbums,因此它有 3 个 handlers。

2. 全局中间件(Global Middleware)

通过 router.Use() 注册的全局中间件会应用到所有路由。这些中间件函数会在所有路由的处理器之前执行。每注册一个中间件,它就会被算作一个 handler。

例如:

router := gin.Default() // gin.Default() 会默认加载 Logger 和 Recovery 中间件 // 你也可以通过 router.Use() 添加其他全局中间件 router.Use(customMiddleware)

gin.Default() 中,默认会添加两个全局中间件:

  • Logger():记录请求日志。
  • Recovery():处理 panic,防止程序崩溃。

这意味着即使你没有为某个路由注册任何中间件,默认情况下 gin.Default() 会自动为每个路由添加这两个处理器。

3. 局部中间件(Route-Specific Middleware)

除了全局中间件,你还可以为特定的路由或路由组定义局部中间件。局部中间件只会影响特定的路由,并且会在该路由的处理器函数之前执行。

例如:

authorized := router.Group("/albums", authMiddleware) authorized.GET("", getAlbums)

在这个例子中,authMiddleware 只会应用到 /albums 路由,因此它会被计算为一个 handler。

综上,决定 handler 数目的因素:

  1. 全局中间件数量:通过 router.Use() 注册的中间件会影响每个路由的 handler 数目。
  2. 局部中间件数量:如果某个路由或路由组注册了局部中间件,这些中间件会增加该路由的 handler 数目。
  3. 路由的实际处理函数:即执行业务逻辑的 handler(如 getAlbums),也是一个 handler。

总结:

在你的例子中,[GIN-debug] GET /albums --> main.getAlbums (3 handlers) 显示 /albums 路由有 3 个处理器(handlers)。这些处理器的数量可能是由以下因素组成的:

  • Gin 的默认中间件(如 Logger()Recovery())。
  • 你是否为 /albums 路由添加了其他中间件。

例如,如果你使用的是 gin.Default(),默认就有两个中间件,再加上实际的处理函数 getAlbums,因此总共有 3 个处理器。

5.全局变量的赋值

当我使用:=运算符的时候,go在当前的作用域内部声明新的变量,而不是给全局变量赋值,所以需要用=来给全局变量赋值

Getting started with generics

1. Go的comparable类型

在 Go 中预先声明了 comparable constraint。它允许其值可用作比较运算符 == 和 != 的操作数的任何类型的类型。Go 要求 map 键具有可比性。

2. 接口声明中~的作用

type Numbers interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

这里有波浪号,那么如果是自定义的类型,比如MyInt,只要他的底层是实现是int,那么就能够被看做实现了这个接口;

如果没有波浪号,那么就会严格限制,只接受int类型

Getting started with fuzzing

1.通过下标访问string中的字符

在 Go 语言中,string 是不可变的字节序列,虽然你可以通过下标访问字符串中的某个位置,但你访问到的是该位置的 byte,即该字符的 UTF-8 编码的某个字节,而不是直接访问字符。

因此,如果字符串包含了汉字这样的非ASCII字符,我们就需要先把string转换成rune切片,然后再根据下标访问

package main

import (
    "fmt"
)

func main() {
    s := "世界"
    runes := []rune(s)   // 将字符串转换为 rune 切片
    fmt.Println(runes[0]) // 输出: 19990,这是 '世' 的 Unicode 码点
    fmt.Printf("%c\n", runes[0]) // 输出: 世,打印字符
}

如果我们用%q作为占位符来输出,会输出字符的unicode编码;这里需要用%c占位符才能输出字符本身

2.测试命令

在 Go 语言中,从 Go 1.18 开始引入了 Fuzzing 测试功能,允许使用 -fuzz 选项运行模糊测试。-fuzz 选项后可以指定测试函数的名字,通常用于选择 Fuzz 测试函数,但除了 Fuzz 测试函数之外,它还可以选择其他值来指定如何运行 Fuzz 测试。

-fuzz 的主要用法:

  • 选择 Fuzz 测试函数:当使用 -fuzz 选项时,可以指定模糊测试的名字。通常 Fuzz 测试函数的名字以 Fuzz 开头,比如 FuzzMyFunction。可以通过 -fuzz=FuzzMyFunction 来指定运行该函数。

其他常见的 -fuzz 相关选项:

虽然 -fuzz 主要用于选择 Fuzz 测试函数,但 Go 的 go test 工具提供了多个相关的选项来配置 Fuzz 测试行为。以下是与 Fuzz 测试相关的一些常见选项:

  1. -fuzztime

    • 作用:指定 Fuzz 测试的持续时间。可以设置为 count(指定测试次数)或 time(指定测试时间,默认为 time.Duration 格式)。
    • 用法示例:
       
      go test -fuzz=FuzzMyFunction -fuzztime=30s # 测试 30 秒
  2. -run

    • 作用:在运行 Fuzz 测试之前,通常会通过 -run 选项来指定要运行的非 Fuzz 测试。
    • 用法示例:
       
      go test -run=TestMyFunction -fuzz=FuzzMyFunction
      这个命令首先运行名为 TestMyFunction 的单元测试,之后再运行 FuzzMyFunction 进行 Fuzz 测试。
  3. -parallel

    • 作用:指定运行 Fuzz 测试时的并行度,即同时运行多少个 Fuzz 测试实例。
    • 用法示例:
       
      go test -fuzz=FuzzMyFunction -parallel=4 # 并行运行 4 个实例
  4. -fuzzminimizetime

    • 作用:指定用于最小化发现的问题的时间。当 Fuzz 测试找到一个导致崩溃的输入时,Go 会尝试简化输入来更容易复现问题。-fuzzminimizetime 控制最小化过程的持续时间。
    • 用法示例:
       
      go test -fuzz=FuzzMyFunction -fuzzminimizetime=5s # 5 秒内简化输入
       
      如果把-fuzz参数设置为Fuzz,那么Go就会去匹配所有前缀为Fuzz的函数并且运行它们进行测试

Find and fix vulnerable dependencies with govulncheck

govulncheck输出的

=== Informational ===

部分是依赖不稳定,但是我们没有调用的地方,可以暂时先不修复

Tour of Go

1.slice的增长

slice只有用append函数加入元素的时候,才能出发capacity的增长,正常操作时,其长度不能超过原始的capacity

2.range遍历channel和slice的行为区别

range遍历slice每次会返回两个值,第一个是index,第二个才是value,而且循环到slice末尾就自动终止了;

range遍历channel每次只会返回value一个值,并且会一直进行,直到channel被输出端关闭

3.创建特定的多维slice

以二维为例:

// Create a 2D slice
    twoDSlice := make([][]int, x)  // Slice of length x (rows)

    // Initialize each row with a slice of length y (columns)
    for i := range twoDSlice {
        twoDSlice[i] = make([]int, y)
    }

4.函数闭包

Go functions may be closures. A closure is a function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is "bound" to the variables.

For example, the adder function returns a closure. Each closure is bound to its own sum variable.

package main

import "fmt"

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

pos和neg会分别记住自己绑定的sum值,每次加i都会累加到这个sum当中

5.函数和方法处理指针的区别

普通函数如果接受一个指针参数,例如*T,那么传入参数时就必须是一个指针,需要&T

但如果是一个有着指针接收者的方法,例如func (t *T) f(),那么t.f()(&t).f()都是合法的,Go编译器会自动给前者加上取地址符

反过来同样成立,综合来说,即:

Comparing the previous two programs, you might notice that functions with a pointer argument must take a pointer:

var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK

while methods with pointer receivers take either a value or a pointer as the receiver when they are called:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

For the statement v.Scale(5), even though v is a value and not a pointer, the method with the pointer receiver is called automatically. That is, as a convenience, Go interprets the statement v.Scale(5) as (&v).Scale(5) since the Scale method has a pointer receiver.

 

The equivalent thing happens in the reverse direction.

Functions that take a value argument must take a value of that specific type:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!

while methods with value receivers take either a value or a pointer as the receiver when they are called:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

In this case, the method call p.Abs() is interpreted as (*p).Abs().

6. struct是否实现interface

假定我们有一个interface ty,有一个结构体str,一个函数f的接收者是str*指针,那么,只能说str*实现了该接口,str并没有,如果我们定义一个ty类型的变量a,那么我们只能把str*指针赋值给它,不能把str值赋值给它;

反过来也一样,如果f的接收者是str,那么就不能说str*实现了该接口;

一个函数的接收者只能是value或者ptr,不能同时兼具两者

7. 实现Error()接口可能导致的死循环

Create a new type

type ErrNegativeSqrt float64

and make it an error by giving it a

func (e ErrNegativeSqrt) Error() string

method such that ErrNegativeSqrt(-2).Error() returns "cannot Sqrt negative number: -2".

Note: A call to fmt.Sprint(e) inside the Error method will send the program into an infinite loop. You can avoid this by converting e first: fmt.Sprint(float64(e)). Why?
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

因为这里我们正在实现一个Error接口,所以如果我们调用fmt里的Print相关功能,就会导致e被认为是一个error类型,然后输出函数就会尝试去调用它的Error接口,于是下一层又会继续尝试……

最终造成了死循环;因此,在实现Error接口的时候,我们要保证我们输出的都是已经明确有了Error实现的类型,来避免死循环

8. 不能对临时值取地址

因为它们的生命周期很短,并不存放在固定的位置,并且随时可能会销毁;

同理,C++当中也不能对右值引用取地址

9. Reader的调用

如果直接手动调用一个Reader接口的Read函数,为了保证能够读取完所有的信息,我们需要手动循环,多次读取

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

但是在实现Read函数的时候,我们不用考虑这个循环

func (r *rot13Reader)Read(b []byte) (n int, err error) {
		if err != nil && err != io.EOF {
			return n, err
		}
		for i := range b {
			b[i] = rot13(b[i])
		}
		if err == io.EOF {
			return n, io.EOF
		}
	return n, err
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

因为外层的调用者会自动循环,在前面的例子中,这个循环需要我们手动地显式执行,这里的io.Copy函数会自动执行这个循环过程

10. Go与C++强转语法的差别

C++当中的强转的写法是(type)var,而且没有硬性要求;而Go是反过来的,且有硬性要求,必须是type(var)

11. Package目录的规则

导入包时,导入路径和存储路径是绑定的,且并不会显示出module与package的关系:

例如,导入image包的路径是image,而导入color包的路径是image/color,显然这并不表明它们之间有任何从属关系,因为包和包是平级的,只跟它们的存储路径有关;

而我们导入时是以package为单位的,并不关心它们属于哪个module;而开发则是以module为单位的

12. Go指针的初始化

这种指针的初始化方式在C++中是合法的:

List<int64_t>* list = new List<int64_t>{nullptr, 2};

但是在Go当中,类似的写法是不合法的:

var list *List[int64] {nil, 2}

因为Go 语言不允许在声明变量时使用花括号 {} 直接初始化一个指针类型的变量。在 Go 中,直接用花括号 {} 的方式初始化的是 非指针 的结构体,而不是指针。

可以先声明list变量,然后再取地址

list := &List[int64]{nil, 2}

13. Go的if-else注意事项

Go的else必须写在if语句块的右括号的同一行!

posted @ 2024-09-03 00:13  Gold_stein  阅读(4)  评论(0编辑  收藏  举报