基础

数据类型

引用类型是对底层数据结构的引用, 值类型是数据结构的副本。
y := x ,如果 x 是一个引用类型,那么 y 将指向与 x 相同的内存地址。但是,如果 x 是一个值类型,那么 y 将获得一个 x 的副本,而不是 x 的地址。

  • 值类型

    • 整数
    • 浮点数
    • 布尔值
    • 数组
    • 结构体
    • 字符串
  • 引用类型

    • 指针
    • 切片
    • 映射
    • 通道
    • 接口

结构体

结构体相当于面向对象中的Class,创建不进行new或者创建实例就不能进行使用,换句话说必须进行实例化后才可以使用。

使用未初始化的结构体变量

这种情况下,p 被声明为一个 Person 类型的变量。即使没有显式初始化,Go 语言会自动将其字段设置为零值。你可以安全地访问这些字段。

1
2
3
4
5
6
7
8
9
10

package main

func main() {
var p Person // 声明但未初始化,p 是一个零值初始化的结构体实例

fmt.Println(p.Name) // 输出空字符串 ""
fmt.Println(p.Age) // 输出 0
}

使用 new 函数

使用 new 函数分配内存后,p 是一个指向 Person 类型的新分配实例的指针。可以通过该指针访问和修改结构体字段。

1
2
3
4
5
6
7
8
9
10

package main

func main() {
p := new(Person) // p 是 *Person 类型的指针,指向一个新分配的零值结构体

fmt.Println(p.Name) // 输出空字符串 ""
fmt.Println(p.Age) // 输出 0
}

直接创建结构体实例

通过字面量创建并初始化一个结构体实例,可以直接访问和使用其字段。

1
2
3
4
5
6
7
8
9
10
11

package main

func main() {
p := Person{Name: "Alice", Age: 30} // 创建并初始化结构体实例

fmt.Println(p.Name) // 输出 "Alice"
fmt.Println(p.Age) // 输出 30
}


默认赋值

各种类型被声明但未初始化时的默认值

数值类型

  • 整数类型(int, int8, int16, int32, int64)和浮点数类型(float32, float64)的默认值是 0。
  • 无符号整数类型(uint, uint8, uint16, uint32, uint64, uintptr)的默认值也是 0。
  • 复数类型(complex64, complex128)的默认值是实部和虚部都为 0 的复数。

布尔类型

  • 布尔类型 bool 的默认值是 false。

字符串类型

  • 字符串类型 string 的默认值是空字符串 “”。

数组类型

  • 数组类型的默认值取决于数组元素的类型
    • 如果元素是数值类型,则默认值为 0;
    • 如果元素是布尔类型,则默认值为 false;
    • 如果元素是字符串类型,则默认值为 “”;
    • 如果元素是其他复合类型,则默认值为其对应类型的零值。

切片、映射和通道类型

  • 切片、映射和通道类型的默认值是 nil。

指针类型

  • 指针类型的默认值是 nil。

结构体类型

  • 结构体类型的默认值是其所有字段的默认值。

接口类型

  • 接口类型的默认值是 nil。

数组&切片

属性 数组 切片
长度 固定 可扩容

指针&取值

符号*在 Go 中作用

  • 取值操作符:当 * 用作变量前缀时,它表示取指针所指向的值。例如,如果有一个指向 int 类型的指针 ptr,那么 *ptr 就表示指针 ptr 所指向的实际 int 值。
  • 类型指针:当 * 用于类型前面时,它表示指向该类型的指针。例如,*Account 表示指向 Account 类型的指针。
  • 取值后赋值:*p=1取出p对应的值后赋值。

深拷贝&浅拷贝

判断深拷贝和浅拷贝的一个常用方法是修改原始数据后观察副本数据是否受到影响。

修改原始数据后,副本数据也发生了变化,则是浅拷贝;如果副本数据没有发生变化,则是深拷贝。

  • 深拷贝:对于结构体中的所有字段,它们的值都将被复制,包括指针字段所指向的值。这意味着原始结构体和副本结构体中的指针字段将指向不同的内存地址,但主副本所指向是不同的(值相同)。
  • 浅拷贝:只有结构体中的值被复制,而不包括指针字段所指向的值。在浅拷贝中,原始结构体和副本结构体中的指针字段将指向相同的内存地址,因此它们共享相同的底层数据。

关键字

defer

defer defer 语句用于延迟执行一个函数直到包含它的函数执行完毕,即函数返回之前。无论函数是正常结束还是因为出现错误而中止,defer 语句都会被执行。
会被用于:关闭数据库连接 关闭文件 等

defer 的执行顺序是按照其在函数中出现的顺序的相反顺序执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
fmt.Println("Start")

defer fmt.Println("Deferred 1")
defer fmt.Println("Deferred 2")
defer fmt.Println("Deferred 3")

fmt.Println("End")
}

结果:

1
2
3
4
5
6
Start
End
Deferred 3
Deferred 2
Deferred 1

new

new 用于分配基本类型(如 int、float、struct)和组合类型(如数组、指针),并返回这些类型的指针

new 作用

  1. 分配内存new(T) 会为类型 T 分配零值初始化的内存。零值取决于类型,例如,对于数值类型,零值是 0,对于字符串类型,零值是空字符串,对于指针类型,零值是 nil

  2. 返回指针new(T) 返回一个指向类型 T 的指针,类型是 *T

例如,对于一个结构体,new(Person) 分配了一块内存用于存储 Person 结构体,并返回指向这块内存的指针 p。然后可以通过 p 来访问和修改 Person 结构体的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Person struct {
Name string
Age int
}

func main() {
// 使用 new 分配一个 Person 类型的内存并获取指针
p := new(Person)
fmt.Println(p) // 输出:&{ 0}
p.Name = "Alice"
p.Age = 30
fmt.Println(p) // 输出:&{Alice 30}
}

make

make 用于分配和初始化内置引用类型(如 slice、map 和 channel),并返回这些类型的值

使用 make 创建切片(slice)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
// 使用 make 创建一个长度为 5,容量为 10 的 int 类型切片
slice := make([]int, 5, 10)

// 输出切片的长度和容量
fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)

// 修改切片中的元素
slice[0] = 1
slice[1] = 2
slice[2] = 3

fmt.Println("Modified slice:", slice)
}

输出:

1
2
len=5 cap=10 slice=[0 0 0 0 0]
Modified slice: [1 2 3 0 0]

使用 make 创建映射(map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
// 使用 make 创建一个 string 到 int 的映射
m := make(map[string]int)

// 向映射中添加键值对
m["Alice"] = 25
m["Bob"] = 30

// 输出映射的内容
fmt.Println("Map:", m)

// 访问映射中的元素
age := m["Alice"]
fmt.Println("Alice's age:", age)
}

输出:

1
2
Map: map[Alice:25 Bob:30]
Alice's age: 25

使用 make 创建通道(channel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
// 使用 make 创建一个缓冲区大小为 2 的 int 类型通道
ch := make(chan int, 2)

// 向通道中发送数据
ch <- 1
ch <- 2

// 从通道中接收数据
fmt.Println("Received from channel:", <-ch)
fmt.Println("Received from channel:", <-ch)
}

输出:

1
2
Received from channel: 1
Received from channel: 2

特殊符号

...

可变参数

函数定义中使用 … 时,表示该函数可以接受可变数量的参数。具体来说,这意味着可以将多个参数作为一个切片传递给函数。

1
2
3
4
5
6
7
8
9
10
11
12
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}

func main() {
fmt.Println(sum(1, 2, 3, 4)) // 输出: 10
}

展开操作符

函数调用中使用 … 时,表示将一个切片中的元素作为单独的参数传递给函数。

1
2
3
4
5
func main() {
numbers := []int{1, 2, 3, 4}
fmt.Println(sum(numbers...)) // 输出: 10
}

进阶

接口

在 Go 中,任何类型(包括基本类型、结构体、数组、切片、映射等)都可以实现接口,只要它们定义了接口中所需的方法。

传参

Go中传递参数有两种方式:1. 值传递 2. 引用传递

值传递

你希望传递的是结构体的拷贝,函数内部对结构体的修改不会影响到函数外部的原始结构体。

值类型的值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Person struct {
Name string
Age int
}

// 使用值类型传递
func modifyPersonValue(person Person) {
person.Age = 25
}

func main() {
person := Person{Name: "Alice", Age: 30}

// 使用值类型传递
modifyPersonValue(person)
fmt.Println("Person after modifyPersonValue:", person) // 输出:Person after modifyPersonValue: {Alice 30}

}

引用类型的值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

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

func main() {
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println(slice) // 输出 [100 2 3]
}

引用传递

你希望函数内部修改结构体等复杂类型的字段并且想要这些修改反映到函数外部的变量上,你需要使用引用传递(指针传递)。

值类型的引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Person struct {
Name string
Age int
}


// 使用指针类型传递
func modifyPersonPointer(person *Person) {
person.Age = 25
}

func main() {
person := Person{Name: "Alice", Age: 30}

// 使用指针类型传递
modifyPersonPointer(&person)
fmt.Println("Person after modifyPersonPointer:", person) // 输出:Person after modifyPersonPointer: {Alice 25}
}

引用类型的引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

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

func main() {
slice := []int{1, 2, 3}
modifySlice(&slice)
fmt.Println(slice) // 输出 [100 2 3]
}

值/指针接收者

引用接收者(Pointer Receiver)

引用接收者的方法是针对指针类型(struct)的操作,即在方法调用时,接收者是原始实例的引用。引用接收者允许方法修改接收者的状态,或者避免在传递大的结构体时产生的性能开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Person struct {
name string
age int
}

// 引用接收者方法
func (p *Person) IncrementAge() {
p.age++
}

func main() {
p := Person{name: "Alice", age: 30}
p.IncrementAge()
fmt.Println("Age:", p.age) // 输出: Age: 31
}

值接收者(Value Receiver)

值接收者的方法是针对值类型(struct)的拷贝进行操作,即在方法调用时,接收者是被复制的一份新的实例。值接收者通常用于那些不需要修改接收者状态的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

package main

import "fmt"

type Stringer interface {
String() string
}

type Person struct {
name string
age int
}

// 值接收者实现接口
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.name, p.age)
}

func main() {
p := Person{name: "Alice", age: 30}
var s Stringer = p // 可以将结构体值赋给接口
fmt.Println(s.String())
}

选择使用值接收者还是引用接收者

  • 不需要修改接收者的情况下,使用值接收者。这样可以避免不必要的指针传递,代码会更简洁。
  • 需要修改接收者的情况下,使用引用接收者。这样可以直接操作原始数据,而不是它的副本。
  • 涉及较大的结构体时,使用引用接收者可以避免复制结构体带来的性能开销。

import

go的import关键字不仅可以导入包中的 变量 函数 还可以自动执行init()

可见性

Go 语言中,包(package)是管理代码和组织代码的基本单位。每个 Go 源文件都属于某个包,通过 package 关键字定义。包中的代码,包括变量、函数、类型等,可以是包内共享的,也可以是包外可见的,这取决于它们的可见性(visibility)。

可见性规则

  1. 包内共享(内部可见)
    • 变量、函数、类型等如果以小写字母开头,它们是包私有的,只能在定义它们的包内部访问。即使是同一目录下的其他包的代码也无法访问这些标识符。
  2. 包外可见(导出)
    • 变量、函数、类型等如果以大写字母开头,它们是导出的,这意味着它们不仅在定义它们的包内可见,也在其他导入这个包的包中可见。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package system

// 包内共享的变量
var internalVar = "this is internal"

// 包外可见的变量
var ExportedVar = "this is exported"

// 包内共享的函数
func internalFunction() {
// ...
}

// 包外可见的函数
func ExportedFunction() {
// ...
}

在另一个包中使用 system 包时,只能访问到导出的标识符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"system"
)

func main() {
fmt.Println(system.ExportedVar) // 正常访问导出的变量
system.ExportedFunction() // 正常调用导出的函数

// fmt.Println(system.internalVar) // 错误:无法访问包内共享的变量
// system.internalFunction() // 错误:无法调用包内共享的函数
}

普通导入

1
2
3
4
5
6
7
8
9
// mypackage/mypackage.go
package mypackage

import "fmt"

func init() {
fmt.Println("mypackage initialized")
}

导入时,自动执行mypackage中的init(),并且会导入除了执行init(),还会导入mypackage中包外可见的变量和函数。

1
2
3
4
5
6
7
8
9
10
11
12
// main.go
package main

import (
"path/to/mypackage"
"fmt"
)

func main() {
fmt.Println("main function executed")
}

执行

1
2
mypackage initialized
main function executed

匿名导入

导入一个包却不直接使用它的任何符号(变量、函数等),在正常情况下会引发编译错误。为了避免这种情况,可以使用匿名导入(也叫做空白导入),即在导入路径前加一个下划线 _

只执行 init 函数,不导入任何符号,避免了编译器因为未使用导入的符号而报错。

特性 普通导入 匿名导入
导入符号 导入包中的所有导出符号(函数、变量等) 不导入任何符号
执行 init 函数
避免编译错误 不会,如果不使用符号会引发编译错误 会,即使不使用符号也不会引发编译错误
用途 显式使用包中的符号 触发副作用,执行初始化逻辑
1
2
3
4
5
6
7
8
package plugin

import "fmt"

func init() {
fmt.Println("Plugin registered")
}

1
2
3
4
5
6
7
8
9
10
package main

import (
_ "path/to/plugin"
)

func main() {
fmt.Println("Main function executed")
}

执行

1
2
Plugin registered
Main function executed

实战

常用命令

参考:

Cnblog-Binb:Go编译工具命令

初始化go.mod

1
2
# 初始化一个go.mod,将本项目定位至<仓库地址>,例: github.com/anthdm/gobank
go mod init <仓库地址>

编译可执行文件

1
2
# 编译输出bin文件
go build -o <bin文件位置>

go build 命令不仅编译项目代码,还负责确保所有需要的依赖项都已下载并可用。

  • 解析依赖项
  • 检查本地缓存
  • 下载缺失的依赖项
  • 验证和保存

整理依赖

1
go mod tidy
  • 移除未使用的依赖
  • 添加缺失的依赖
  • 更新 go.sum 文件,添加缺失的哈希值和移除不再需要的哈希值
  • go mod tidy 会根据项目中实际使用的依赖版本,整理并更新 go.mod 文件中的依赖版本号,确保项目使用的是正确的版本。

编译添加go包

这将下拉源码并编译后可在命令行使用go程序,以下是

1
go install github.com/swaggo/swag/cmd/swag@latest