为什么学Go?

其实之前就有点想学,因为听说这个编程语言,既有Python的简单和方便,又有C的运行高效。
实际上第一次接触到GO,是因为向自己改一下ZeroBot-Plugin这个项目的一点点代码,也就是CPU核心读取不正常的问题,我找LLM修复了一下面向LLM编程(bushi。看了看代码,有Python的基础,挺多能看懂的。这也坚定了我想要学Golang的想法。
于是,这个寒假,在整理完成歌曲库,升级完成NAS后闲得蛋疼,我决定开始学习GO
本页面会随着我的学习进度逐步更新,权当是当个笔记了。

第一个Ciallo WorLd!

一般编程语言学习,第一个程序都是Hello World,但是这次我向整点不一样的:

package main
import "fmt"
func main()  {
    fmt.Println("Ciallo World!")
}

另外,有一个写代码小技巧,选取多行,按Tab可以整行缩进,Shift+Tab可以反向操作。Ctrl+/可以多行进行注释,对Debug很有用。

变量与数据类型

局部变量和全局变量

局部变量示例:

package main
import "fmt"
func main()  {
        var ss int = 233
    fmt.Println("Ciallo World!",ss)
}

全局变量示例:

package main
import "fmt"
var ss int = 233
func main()  {
    fmt.Println("Ciallo World!",ss)
}

或者这样,更加简洁:

package main
import "fmt"
var(
    ss = 2333
    st = "sxsxsx"
    fl = 1234.555
    fl2  = 2323E+2
)
func main()  {
    fmt.Println("Ciallo World!",ss,st,fl.fl2)
}

值得注意的是,在Go中,如果变量只声明而不使用,编译就会报错。

int数据类型

在Go语言中,int 类型与 int8, int16, int32, 和 int64 都是用来表示整数的数据类型,但它们之间存在一些关键区别,主要体现在大小、范围和平台依赖性上。下面是这些类型的区别列表说明:

类型占用空间数值范围适用场景
int88位(1字节)-128 到 127需要节省内存且数值范围较小的场景
int1616位(2字节)-32,768 到 32,767比int8更大的范围,但仍需控制内存使用时
int3232位(4字节)-2,147,483,648 到 2,147,483,647处理更广泛的数值,例如时间戳或计算密集型应用
int6464位(8字节)-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807非常大的整数值或者需要精确到极大值的应用场景
int取决于平台(32位系统上为4字节,在64位系统上为8字节)范围取决于系统的架构大小不考虑具体大小的通用情况,追求代码的可移植性和简洁性

声明变量:

  • 直接赋值
var a int8 = 10        // int8 类型变量a被赋予值10
var b int16 = -200     // int16 类型变量b被赋予值-200
var c int32 = 30000    // int32 类型变量c被赋予值30000
var d int64 = 400000   // int64 类型变量d被赋予值400000
var e int = -500       // 根据系统架构决定大小的int类型变量e被赋予值-500
  • 使用短变量声明
f := int8(100)         // 使用短变量声明为int8类型变量f赋值100
g := int16(-30000)     // 使用短变量声明为int16类型变量g赋值-30000
h := int32(50000)      // 使用短变量声明为int32类型变量h赋值50000
i := int64(600000)     // 使用短变量声明为int64类型变量i赋值600000
j := int(-600)         // 使用短变量声明为int类型变量j赋值-600

float数据类型

包括float32float64

类型占用空间精度数值范围(大约)适用场景
float3232位(4字节)小数点后约7位有效数字±1.5 × 10^-45 到 ±3.4 × 10^38对内存使用敏感且不需要高精度的场合,例如图形处理、物理仿真等需要大量浮点计算但对精度要求不是极高的应用
float6464位(8字节)小数点后约15位有效数字±5.0 × 10^-324 到 ±1.7 × 10^308需要高精度的数值计算,如科学计算、金融计算等

声明变量:

  • 直接赋值
var k float32 = 123.456    // float32 类型变量k被赋予值123.456
var l float64 = 789.101112 // float64 类型变量l被赋予值789.101112
  • 使用短变量声明
m := float32(1234.5678)    // 使用短变量声明为float32类型变量m赋值1234.5678
n := float64(9876.54321)   // 使用短变量声明为float64类型变量n赋值9876.54321

其实赋值的时候也可以使用科学记数法:

var a float = 888E+2
var b float = 888E-2

上述分别的值是888008.88

string数据类型

就是字符串嘛,常见的赋值方式有这些:

直接赋值

最简单直接的方式是通过字面量直接给变量赋值。

s1 := "Hello, World!"

使用 var 关键字声明并赋值

可以使用 var 来声明一个 string 类型的变量,并且可以在声明时或之后进行赋值。

var s2 string
s2 = "Hello, Go!"

// 或者同时声明和初始化
var s3 string = "Another Hello"

多行字符串

使用反引号( ` )可以定义多行字符串,这种方式下字符串内的换行符和其他空白字符都会被保留。

s4 := `这是一个
多行
字符串`

字符串拼接

可以通过 + 操作符将多个字符串连接在一起。

firstName := "John"
lastName := "Doe"
fullName := firstName + " " + lastName // 结果为 "John Doe"

使用 fmt 包格式化字符串

可以利用 fmt.Sprintf 函数来根据指定格式生成字符串。

age := 30
description := fmt.Sprintf("My age is %d", age) // 结果为 "My age is 30"

bool数据类型

基本概念

  • 类型bool
  • 取值:只有两个值,truefalse
  • 默认值false(如果变量声明但未赋值)

声明和初始化

可以使用 var 关键字来声明一个布尔变量,并且可以在声明时或之后进行赋值。

var isActive bool  // 声明一个布尔变量,初始值为 false
isActive = true    // 赋值为 true

// 或者同时声明并初始化
isEnabled := false // 使用短变量声明并初始化

逻辑运算符

布尔值通常与逻辑运算符一起使用来进行更复杂的条件判断:

  • 逻辑与 (&&):当两边的操作数都为 true 时,结果为 true
result := true && false // 结果为 false
  • 逻辑或 (||):只要一边的操作数为 true,结果就为 true
result := true || false // 结果为 true
  • 逻辑非 (!):对操作数的布尔值进行反转。
result := !true // 结果为 false

在控制结构中的应用

布尔值最常用于控制结构如 iffor 等语句中,以决定程序的执行路径。

if isActive {
    fmt.Println("The feature is active.")
}

for i := 0; i < 10 && isActive; i++ {
    fmt.Println(i)
}

比较运算符的结果

比较运算符(如 ==, !=, <, >, <=, >=)的结果也是布尔值,这使得它们非常适合用于条件判断。

isEqual := (5 == 5)   // 结果为 true
isGreater := (10 > 5) // 结果为 true

基本数据类型的互相转换

整型与整型之间的转换

可以直接通过类型转换操作来进行不同整型之间的转换。需要注意的是,当从小范围类型向大范围类型转换时(如int8int32)通常是安全的,反之则可能会导致数据丢失。

var a int8 = 10
b := int16(a) // 将int8类型的a转换为int16类型的b

整型与浮点型之间的转换

可以使用类型转换将整型转换为浮点型,反之亦然。注意,从浮点型转换为整型时,小数部分会被截断。

var c int = 5
d := float64(c) // 将int类型的c转换为float64类型的d

var e float64 = 5.75
f := int(e)     // 将float64类型的e转换为int类型的f,结果为5,小数部分被截断

字符串与数字之间的转换

  • 字符串转数字:可以使用标准库中的 strconv 包提供的函数来实现。
import "strconv"

s := "123"
i, err := strconv.Atoi(s) // 将字符串s转换为int类型的i
if err != nil {
    // 处理错误
}

f, err := strconv.ParseFloat("123.45", 64) // 将字符串"123.45"转换为float64类型的f
  • 数字转字符串:同样可以使用 strconv 包中的函数。
import "strconv"

num := 123
str := strconv.Itoa(num) // 将int类型的num转换为字符串

fnum := 123.45
fstr := strconv.FormatFloat(fnum, 'f', 6, 64) // 将float64类型的fnum转换为字符串,格式化参数根据需求调整

指针

基本概念

  • 取地址符(&):用于获取变量的内存地址。
  • 解引用符(*):用于访问指针所指向的变量的实际值。
  • 指针类型声明:使用*Type的形式声明一个指针类型的变量,其中Type是你希望指针指向的数据类型。

代码示例

下面是一个简单的例子,演示了如何声明指针、获取变量的地址以及如何通过指针访问或修改变量的值:

package main

import "fmt"

func main() {
    // 声明一个整型变量a并赋值为10
    a := 10
    
    // 使用&获取变量a的地址,并将该地址赋给指针变量p
    var p *int = &a
    
    // 输出变量a的值、变量a的地址和指针p的内容(即a的地址)
    fmt.Println("Value of a:", a)
    fmt.Println("Address of a:", &a)
    fmt.Println("Value of p (address of a):", p)
    
    // 使用*来解引用指针p,以访问它指向的值
    fmt.Println("Value pointed to by p:", *p)
    
    // 修改指针p指向的值(也就是修改变量a的值)
    *p = 20
    fmt.Println("Modified value of a:", a)
}

在这个例子中:

  • 我们首先声明了一个整型变量a并赋予初始值10。
  • 然后,我们声明了一个指向整型的指针p,并将变量a的地址赋给了p
  • 接下来,我们展示了如何打印出变量的值、它的地址以及指针变量的内容。
  • 最后,通过解引用指针p,我们直接修改了a的值,证明了通过指针可以直接访问和修改其指向的变量的值。

变量是否允许跨包使用

在变量命名中,如果是小写字母开头的变量,则只允许在当前的包中使用,类似于私有变量。
但是,如果是大写字母开头的变量,则可以在别的地方通过导入包进行使用。
例如:

var miku int = 39
var Rin int = 12

miku变量只能在当前包中的代码读取和使用,而Rin变量别的地方导入了这个包的话就可以使用。

设置自己的包

  1. 创建一个新的目录来存放你的包文件。例如,你可以创建一个名为mypackage的目录。
  2. plugin.go移动到这个新目录中。
  3. plugin.go中声明包名。如果你希望包名与目录名一致,则可以这样写:

    package mypackage
  4. test.go中导入你刚刚创建的包,并使用其中的变量。
    5.初始化Go模块

下面是一个具体的例子:
项目结构如下:

myproject/
├── test.go
└── mypackage/
    └── plugin.go

首先,在plugin.go中定义一个公共变量(注意:Go中的变量如果要导出,首字母必须大写):

package mypackage

var MyVariable = "Hello from plugin.go"

然后,在test.go中导入mypackage并使用MyVariable:

package main

import (
    "fmt"
    "myproject/mypackage"
)

func main() {
    fmt.Println(mypackage.MyVariable)
}

然后进入到该目录,初始化Go模块:

go mod init myproject

运算符

常见运算符

  1. 算术运算符
package main
import "fmt"

func main() {
    a := 10
    b := 5
    fmt.Println("a + b =", a + b) // 加法:15
    fmt.Println("a - b =", a - b) // 减法:5
    fmt.Println("a * b =", a * b) // 乘法:50
    fmt.Println("a / b =", a / b) // 除法:2
    fmt.Println("a % b =", a % b) // 取模:0
}
  1. 关系运算符
package main
import "fmt"

func main() {
    a := 10
    b := 5
    fmt.Println("a == b is", a == b) // false
    fmt.Println("a != b is", a != b) // true
    fmt.Println("a < b is", a < b)   // false
    fmt.Println("a > b is", a > b)   // true
    fmt.Println("a <= b is", a <= b) // false
    fmt.Println("a >= b is", a >= b) // true
}
  1. 逻辑运算符
package main
import "fmt"

func main() {
    a := true
    b := false
    fmt.Println("a && b is", a && b) // false
    fmt.Println("a || b is", a || b) // true
    fmt.Println("!a is", !a)         // false
}
  1. 位运算符
package main
import "fmt"

func main() {
    a := 60 // 二进制: 0011 1100
    b := 13 // 二进制: 0000 1101
    fmt.Printf("a & b = %d\n", a & b) // 按位与: 12 (0000 1100)
    fmt.Printf("a | b = %d\n", a | b) // 按位或: 61 (0011 1101)
    fmt.Printf("a ^ b = %d\n", a ^ b) // 按位异或: 49 (0011 0001)
    fmt.Printf("a << 2 = %d\n", a << 2) // 左移两位: 240 (1111 0000)
    fmt.Printf("a >> 2 = %d\n", a >> 2) // 右移两位: 15 (0000 1111)
}
  1. 赋值运算符
package main
import "fmt"

func main() {
    var a int = 5
    a += 3 // 相当于 a = a + 3
    fmt.Println("a += 3 =", a) // 8
    
    a -= 2 // 相当于 a = a - 2
    fmt.Println("a -= 2 =", a) // 6
    
    a *= 2 // 相当于 a = a * 2
    fmt.Println("a *= 2 =", a) // 12
    
    a /= 3 // 相当于 a = a / 3
    fmt.Println("a /= 3 =", a) // 4
    
    a %= 3 // 相当于 a = a % 3
    fmt.Println("a %= 3 =", a) // 1
}

获取用户输入

1. fmt.Scanf

fmt.Scanf允许你按照指定的格式字符串来解析输入。它的工作方式类似于C语言中的scanf函数。

package main

import (
    "fmt"
)

func main() {
    var name string
    var age int
    
    fmt.Print("请输入你的名字和年龄: ")
    n, err := fmt.Scanf("%s %d", &name, &age)
    if err != nil {
        fmt.Println("读取失败:", err)
    } else {
        fmt.Printf("成功读取了 %d 个值 - 名字: %s, 年龄: %d\n", n, name, age)
    }
}

注意:%s只读取一个单词,即遇到空格就会停止。

2. fmt.Scanln

fmt.Scanlnfmt.Scan类似,但它会在遇到换行符时停止扫描,并丢弃输入中的任何未读取的部分。这使得Scanln非常适合于读取整行输入,其中输入项由空格分隔。

package main

import (
    "fmt"
)

func main() {
    var name string
    var age int
    
    fmt.Print("请输入你的名字和年龄: ")
    n, err := fmt.Scanln(&name, &age)
    if err != nil {
        fmt.Println("读取失败:", err)
    } else {
        fmt.Printf("成功读取了 %d 个值 - 名字: %s, 年龄: %d\n", n, name, age)
    }
}

Scanf不同的是,Scanln不需要格式化字符串来指示如何解析输入,它自动根据变量类型解析输入。

区别

  • fmt.Scanf:需要提供格式化字符串来指定输入应该如何被解析。适用于你需要精确控制如何解释输入的情况。
  • fmt.Scanln:不需要格式化字符串,它会自动根据参数类型解析输入,直到遇到换行符为止。更适用于简单的输入场景。
  • fmt.Scan:与Scanln相似,但不会跳过末尾的换行符,并且可以在一行内读取多个值,不等待换行。

流程控制

if 流程控制

1. 单分支 if

单分支的if语句仅包含一个条件判断和相应的执行代码块。如果条件为真,则执行该代码块;否则跳过。

package main

import "fmt"

func main() {
    age := 20
    if age >= 18 {
        fmt.Println("成年人")
    }
}

2. 双分支 if-else

双分支的if-else语句除了有一个条件判断和相应的执行代码块外,还提供了一个else部分,当条件为假时执行。

package main

import "fmt"

func main() {
    age := 16
    if age >= 18 {
        fmt.Println("成年人")
    } else {
        fmt.Println("未成年人")
    }
}

3. 多分支 if-else if-else

多分支的if-else if-else语句允许你检查多个条件,并对每个条件执行不同的代码块。一旦某个条件为真,相应的代码块被执行,其余的条件将不会被检查。

package main

import "fmt"

func main() {
    score := 85
    if score >= 90 {
        fmt.Println("优秀")
    } else if score >= 75 {
        fmt.Println("良好")
    } else if score >= 60 {
        fmt.Println("及格")
    } else {
        fmt.Println("不及格")
    }
}

switch 流程控制

基本用法

package main

import (
    "fmt"
)

func main() {
    i := 2
    switch i {
    case 1:
        fmt.Println("数字是1")
    case 2:
        fmt.Println("数字是2")
    case 3:
        fmt.Println("数字是3")
    default:
        fmt.Println("不是1, 2, 或3")
    }
}

使用表达式作为switch的条件

switch不仅可以基于整数或字符串等简单类型的比较,还可以用于更复杂的表达式。

package main

import (
    "fmt"
)

func main() {
    a, b := 12, 6
    switch {
    case a+b == 18:
        fmt.Println("a + b 等于18")
    case a-b == 6:
        fmt.Println("a - b 等于6")
    default:
        fmt.Println("没有匹配的情况")
    }
}

fallthrough 关键字

如果你想在匹配的case后继续执行下一个case的代码,可以使用fallthrough

package main

import (
    "fmt"
)

func main() {
    i := 1
    switch i {
    case 0:
        fmt.Println("i 是0")
        fallthrough // 即使i不是0,也会尝试执行下一个case
    case 1:
        fmt.Println("i 是1或者从上一个case fallthrough过来")
    default:
        fmt.Println("i 不是0也不是1")
    }
}

switch 表达式类型为字符串

switch语句也可以直接用于字符串比较。

package main

import (
    "fmt"
)

func main() {
    str := "hello"
    switch str {
    case "hello":
        fmt.Println("你好")
    case "world":
        fmt.Println("世界")
    default:
        fmt.Println("未知字符串")
    }
}

循环

基本 for 循环

最基本的for循环形式包括初始化语句、条件表达式以及后续操作(通常用于递增或递减计数器)。这种形式与C或Java中的for循环相似。

package main

import "fmt"

func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println("Sum:", sum)
}

在这个例子中,循环从i=0开始,每次循环i增加1,直到i不小于10为止。

省略部分的 for 循环

  • 仅条件表达式:如果省略了初始化和后续操作,for循环就类似于其他语言中的while循环。
package main

import "fmt"

func main() {
    i := 0
    for i < 3 {
        fmt.Println(i)
        i++
    }
}
  • 无限循环:如果省略所有部分,就是一个无限循环。可以通过break语句来退出循环。
package main

import "fmt"

func main() {
    for {
        fmt.Println("This will loop forever")
        break // 使用break退出循环
    }
}

for range 循环

for range提供了一种简洁的方式来遍历数组、切片、字符串、映射和通道。对于每种类型,range返回的值有所不同。

遍历数组或切片

package main

import "fmt"

func main() {
    nums := []int{2, 3, 4}
    for index, value := range nums {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }
}

遍历字符串

package main

import "fmt"

func main() {
    str := "hello"
    for i, c := range str {
        fmt.Printf("Character at position %d is %c\n", i, c)
    }
}

遍历映射

package main

import "fmt"

func main() {
    m := map[string]int{"apple": 2, "banana": 3}
    for key, value := range m {
        fmt.Printf("%s -> %d\n", key, value)
    }
}

注意:遍历映射时,键值对的顺序不是固定的。

控制流程关键字

1. break

break 语句用来终止最内层的 forswitchselect 语句,并跳出该结构。它常用于提前退出循环或选择结构。

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        if i == 3 {
            break // 当i等于3时,跳出循环
        }
        fmt.Println("Number:", i)
    }
    fmt.Println("Loop finished")
}

注意事项: break 只能用于终止最近的循环或选择结构。如果需要从嵌套多层的循环中跳出,可以给循环命名并用标签指定要跳出的循环。

2. continue

continue 语句用于跳过当前循环体中剩余的语句,并继续执行下一次循环(如果有的话)。它通常用于忽略某些特定情况下的后续逻辑。

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        if i == 2 {
            continue // 当i等于2时,跳过本次循环的剩余部分
        }
        fmt.Println("Number:", i)
    }
}

注意事项: continue 同样只能影响最近的循环。对于复杂的嵌套结构,可能需要更清晰的逻辑设计来避免混淆。

3. goto

goto 语句允许程序直接跳转到由标签标记的地方。虽然它提供了灵活的控制流,但过度使用会导致代码难以理解和维护。

package main

import "fmt"

func main() {
    i := 0
start:
    if i < 5 {
        fmt.Println("Number:", i)
        i++
        goto start // 跳转到名为start的标签处
    }
}

注意事项: Go语言鼓励限制使用goto,因为它可能导致“意大利面条式”代码(即代码逻辑混乱,难以跟踪)。一般情况下,应尽量使用结构化的控制流工具如forif等代替。

4. return

return 语句用于从函数中返回,可选地带有一个返回值(对于有返回值的函数)。它是结束函数执行的标准方式。

package main

import "fmt"

func add(a, b int) int {
    return a + b // 返回a+b的结果
}

func main() {
    result := add(3, 4)
    fmt.Println("Result:", result)
    
    // 使用return提前结束main函数
    if result > 5 {
        fmt.Println("结果大于5")
        return
    }
}

注意事项: 在定义了返回类型的函数中,必须确保每次执行路径都有一个return语句,否则会导致编译错误。同时,return语句可用于在满足特定条件时提前终止函数执行。

函数与包

函数

基本函数定义与调用

首先,我们来看一个简单的函数定义和调用的例子:

package main

import "fmt"

// 定义一个名为add的函数,接受两个int类型的参数,返回它们的和
func add(a int, b int) int {
    return a + b
}

func main() {
    // 调用add函数并打印结果
    result := add(5, 3)
    fmt.Println("5 + 3 =", result)
}

函数的重要特性

  1. 多重返回值:Go语言允许函数返回多个值。这在错误处理等方面非常有用。
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}
  1. 命名返回值:可以为返回值指定名称,并且可以直接从函数体内部返回这些值。
func profile(name string) (fullname string) {
    fullname = name + " Doe"
    return // 省略了返回值,但会返回fullname变量的当前值
}
  1. 匿名函数与闭包:Go支持匿名函数,可以作为参数传递或赋值给变量。
func main() {
    // 定义一个匿名函数并立即执行
    func(msg string) {
        fmt.Println(msg)
    }("Hello, World!")
    
    // 或者将其赋值给变量后调用
    sayHello := func(msg string) {
        fmt.Println(msg)
    }
    sayHello("Hello again!")
}
  1. 可变参数函数:类似于其他语言中的varargs,Go语言允许函数接受不定数量的参数。
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3)) // 输出6
}
  1. defer语句:用于安排一个函数调用在包含它的函数返回之后执行,常用于资源清理工作。
func main() {
    defer fmt.Println("World")
    fmt.Println("Hello")
}
// 输出顺序是先"Hello"后"World"

包的定义

首先,每个.go文件必须以package声明开头,这表示该文件属于哪个包。例如,创建一个名为mathutil的包:

// 文件名: mathutil.go
package mathutil

import "fmt"

// 定义一个公共函数(注意首字母大写)
func Add(a, b int) int {
    return a + b
}

// 私有函数(首字母小写),仅在包内可见
func privateAdd(a, b int) int {
    fmt.Println("This is a private function.")
    return a + b
}

在这个例子中,Add函数因为其名字以大写字母开头,所以它是公共的,可以从其他包中访问。相反,privateAdd函数的名字以小写字母开头,因此它是私有的,只能在mathutil包内部使用。

包的导入和使用

要使用其他包中的功能,需要使用import关键字导入相应的包。继续上面的例子,我们可以在另一个文件中导入并使用mathutil包:

// 文件名: main.go
package main

import (
    "fmt"
    "path/to/mathutil" // 假设mathutil位于/path/to/目录下
)

func main() {
    result := mathutil.Add(10, 20)
    fmt.Printf("The result of adding is %d\n", result)
}

请注意,在导入自定义包时,你需要提供相对于$GOPATH/src或Go modules根目录的路径。

包的别名

当你想为导入的包指定一个简短或不同的名称时,可以使用别名。

示例:包别名

// 文件: main.go
package main

import (
    m "path/to/mypackage" // 使用m作为mypackage的别名
)

func main() {
    result := m.Add(5, 3)
    println(result)
}

包的目录结构安排

通常,每个独立的功能模块都应该有自己的目录,并且在这个目录下包含一个或多个.go文件,它们都声明相同的包名。
也就是一个文件夹下的.go文件归为一个包。

推荐的项目结构:

myproject/
├── cmd/
│   └── myapp/
│       └── main.go
├── pkg/
│   └── mypackage/
│       ├── add.go
│       └── subtract.go
└── go.mod
  • cmd/:存放应用的入口点。
  • pkg/:存放可重用的包。

mypackage为包名,导入的时候采用这个名字,但是实际调用函数的时候,以包中定义的package xxx为准。
例如:ccc = xxx.Add(1,2)

init 函数

在Go语言中,init函数是一个特殊的函数,它不需要调用即可自动执行。每个.go文件可以包含一个或多个init函数,这些函数主要用于初始化操作,如设置变量的初始值、验证配置等。下面将详细介绍init函数的工作机制、使用场景以及一些需要注意的细节。

init函数的基本特性

  1. 自动执行init函数无需显式调用,编译器会确保在程序启动时自动执行。
  2. 无参数和返回值init函数不能接受任何参数,也不能有返回值。
  3. 包级别初始化init函数用于包级别的初始化,这意味着它们会在导入包时被调用,但仅限于该包内部。
  4. 执行顺序

    • 在同一个文件内,init函数按照它们声明的顺序执行。
    • 如果一个包中有多个文件,init函数按文件名的字典顺序执行。
    • 包的所有init函数执行完毕后,才会执行导入它的包中的init函数。

使用场景

  • 初始化不可变变量:对于需要复杂计算才能确定其值的常量,可以在init函数中进行计算并赋值。
  • 验证配置:可以在程序运行之前检查配置的有效性。
  • 资源加载:如数据库连接、配置文件读取等。

示例代码

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

package main

import "fmt"

var globalVar string

func init() {
    fmt.Println("First init function called.")
    globalVar = "Initialized"
}

func init() {
    fmt.Println("Second init function called.")
    if globalVar != "Initialized" {
        fmt.Println("globalVar was not properly initialized!")
    }
}

func main() {
    fmt.Println("Main function called.")
    fmt.Printf("Global var: %s\n", globalVar)
}

输出结果为:

First init function called.
Second init function called.
Main function called.
Global var: Initialized

注意事项

  • 避免过度使用:虽然init函数很方便,但过度依赖它可能会导致代码难以理解和维护。尽量让初始化逻辑清晰明确,特别是在大型项目中。
  • 依赖管理:由于init函数会自动执行,如果存在依赖关系(例如一个包的初始化依赖另一个包的初始化),需要小心处理初始化顺序,以避免潜在的问题。
  • 测试困难:由于init函数是隐式执行的,这可能使得某些类型的单元测试变得复杂。考虑将初始化逻辑移到显式的函数中,以便更容易地进行测试。

匿名函数

在Go语言中,匿名函数(Anonymous Function)是指那些未命名的函数,它们可以直接赋值给变量、作为参数传递给其他函数或在需要的地方直接定义和使用。匿名函数提供了极大的灵活性,特别是在需要短生命周期的函数或者当函数逻辑较为简单且仅需使用一次时。

匿名函数的基本形式

匿名函数的语法与普通函数相似,但没有函数名。以下是匿名函数的基本结构:

func(参数列表) 返回值类型 {
    函数体
}

例如,一个接受两个整数并返回它们之和的匿名函数可以这样定义:

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

在这个例子中,sum变量现在指向这个匿名函数,可以通过sum(3, 4)来调用它。

立即执行的匿名函数

在Go中,还可以定义后立即执行匿名函数。这种模式通常用于避免变量泄漏到外部作用域,或者当你只需要一次性使用的函数时非常有用。要立即执行匿名函数,只需在定义后加上圆括号并传入参数(如果有的话):

result := func(a, b int) int {
    return a + b
}(5, 3)

fmt.Println("Result:", result) // 输出: Result: 8

闭包

闭包(Closure)是编程中的一个重要概念,尤其是在支持匿名函数的语言中,如Go语言。理解闭包可以帮助你编写更加灵活和模块化的代码。下面我将尽量简单详细地解释什么是闭包,以及如何在Go语言中使用它们。

什么是闭包?

简而言之,闭包是一个函数加上该函数创建时的环境。这里的“环境”指的是函数定义时可以访问到的所有变量。换句话说,即使函数在其原始作用域之外被调用,它仍然能够记住并访问其创建时的作用域中的变量。

为什么需要闭包?

闭包允许我们封装值,并在不同的上下文中使用这些封装的值。这在处理回调函数、延迟计算或封装私有数据时特别有用。

Go语言中的闭包示例

考虑一个简单的例子,展示如何在Go中创建和使用闭包:

package main

import "fmt"

// makeAdder 返回一个匿名函数,这个匿名函数形成一个闭包
func makeAdder() func(int) int {
    sum := 0 // 这个变量是makeAdder内部的局部变量
    return func(x int) int {
        sum += x // 匿名函数可以访问并修改sum
        return sum
    }
}

func main() {
    adder := makeAdder() // adder现在指向一个闭包
    fmt.Println(adder(1)) // 输出: 1
    fmt.Println(adder(2)) // 输出: 3
    fmt.Println(adder(3)) // 输出: 6
}

在这个例子中,makeAdder函数返回了一个匿名函数,这个匿名函数形成了一个闭包。闭包捕获了makeAdder函数内部的sum变量。因此,每次调用adder函数时,它都能记住之前的sum值,并在此基础上进行累加。

闭包的工作原理

  • 外部函数执行完毕后,通常情况下其局部变量会被销毁。但是,如果有一个闭包引用了这些局部变量,那么这些变量将不会被销毁,而是继续存在,直到不再有任何引用为止。
  • 在上面的例子中,尽管makeAdder函数已经执行完毕,但由于返回的匿名函数(闭包)仍然持有对sum变量的引用,所以sum变量会持续存在于内存中,供闭包使用。

defer 关键字

在Go语言中,defer关键字用于延迟函数或方法的执行直到其所在的goroutine即将结束。通常,defer用于简化函数调用后的清理代码,比如关闭文件、解锁互斥锁等。当有多个defer语句时,它们会按照后进先出(LIFO)的顺序执行。

基本用法

考虑一个简单的例子,其中我们想要确保在函数结束前打印一条消息。

package main

import "fmt"

func main() {
    defer fmt.Println("世界")
    fmt.Println("你好")
}

输出将是:

你好
世界

这里,尽管defer fmt.Println("世界")写在了前面,但由于defer的作用,它会在包含它的函数main结束之前才执行。

多个defer语句

当你在一个函数中有多个defer语句时,它们会以LIFO顺序执行。

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        defer fmt.Print(i)
    }
}

这段代码将输出:

43210

可以看到,数字是按相反顺序打印出来的,这是因为每个defer语句都是在循环迭代结束时被安排执行,并且按照后进先出的顺序执行。

使用defer进行资源管理

defer最常用的场景之一是在打开资源(如文件、数据库连接等)之后立即计划如何释放这些资源,即使在处理过程中出现错误也能保证资源得到正确释放。

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    // 确保在函数退出时关闭文件
    defer file.Close()

    // 执行文件操作...
}

在这个例子中,无论文件操作是否成功,file.Close()都会被执行,从而避免了资源泄露。

错误处理

利用deferrecover来处理错误

package main

import (
    "fmt"
    "log"
)

// 定义一个函数用于执行可能抛出panic的操作
func divide(a, b int) {
    // 使用defer与recover来捕获panic
    defer func() {
        if r := recover(); r != nil {
            // 记录错误信息
            log.Printf("Recovered in divide: %v", r)
        }
    }()

    // 执行除法操作
    result := a / b
    fmt.Printf("%d / %d = %d\n", a, b, result)
}

func main() {
    // 正常情况
    divide(10, 2)

    // 可能造成除以0的情况
    divide(10, 0)

    // 确认程序继续执行
    fmt.Println("Program continues after potential panic.")
}

代码解释:

  • recover():在Go中,当发生panic时,可以使用recover来捕获并恢复正常执行流程。它必须在一个延迟调用的函数内部调用。
  • deferdefer语句会将函数推迟到外层函数返回之后执行。在这个例子中,我们将包含recover的匿名函数推迟执行,以便在发生panic时能够恢复控制。
  • divide函数中,如果第二个参数b为0,则会发生除以零的运行时错误,这会导致一个panic。但是,因为我们使用了deferrecover,这个panic会被捕获,相应的错误消息也会被记录下来,而不会导致整个程序停止执行。
  • main函数中,我们首先正常地调用了divide函数,然后尝试用0作为除数再次调用它,以此展示即使遇到可能导致程序崩溃的错误,我们的程序仍能继续执行。

自定义错误

自定义错误但程序正常运行

package main

import (
    "errors"
    "fmt"
)

// 自定义一个函数,它可能会返回一个错误
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    // 正常情况
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("Result: %d\n", result)
    }

    // 可能导致错误的情况
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("Result: %d\n", result)
    }

    // 确认程序继续执行
    fmt.Println("Program continues after handling the error.")
}

注释

  • divide函数中,我们检查了除数是否为0。如果是,则返回一个自定义的错误信息。
  • main函数中,我们通过检查返回的错误值来决定下一步操作。即使发生了错误(如除以零),程序仍然能够继续执行。

直接抛出错误终止程序

package main

import (
    "fmt"
)

// 自定义一个函数,当遇到错误时直接抛出异常终止程序
func divide(a, b int) {
    if b == 0 {
        panic("division by zero") // 直接抛出错误并终止程序
    }
    fmt.Printf("%d / %d = %d\n", a, b, a/b)
}

func main() {
    // 正常情况
    divide(10, 2)

    // 这将导致程序崩溃
    divide(10, 0)

    // 下面这条消息不会被打印,因为程序已经由于panic而终止
    fmt.Println("This message will not be printed.")
}

注释

  • 在这个例子中,当尝试除以零时,divide函数使用panic来抛出一个运行时错误,并立即终止程序的执行。
  • panic是一个内置函数,用于停止常规的控制流。一旦panic被调用,程序会停止当前函数的执行,并开始回溯goroutine的调用栈,直到找到一个recover(如果没有recover,则程序终止)。
  • 因此,在main函数中,当divide(10, 0)被调用时,程序会立即终止,不会执行后续的任何代码(例如最后的fmt.Println语句)。