Golang,Let‘s GO!

认识 GO 语言

创建工程目录:

  1. 在 Linux 终端输入 echo $GOPATH,即可查看到工程的目录:

  2. 创建GO工程文件夹:

    shell 复制代码
    mkdir -p $GOPATH/bin $GOPATH/pkg $GOPATH/src
    # 如果权限不够,需要加上 sudo
    sudo mkdir -p $GOPATH/bin $GOPATH/pkg $GOPATH/src

    工程文件的各个文件夹解释:

    • bin: 存放编译成功后的可执行文件。
    • pkg: 存放自定义的第三方库。
    • src: 存放工程项目的源码地址。
  3. src目录下创建自己的项目:

    shell 复制代码
    mkdir GolangStudy
  4. GolangStudy中创建程序:

    shell 复制代码
    mkdir firstGolang
  5. 进入 firstGolang 文件夹中,输入 code .即可使用VSCode打开该文件(前提是已经安装有 VSCode),语法:code + 路径即可使用 VSCode 打开指定的路径。

    shell 复制代码
    code /path1/path2/...

第一个程序

1. 实例:

go 复制代码
package main	// 当前程序的包名,一个项目只能有一个 main 包(主包)

// 倒入格式化输出包
import "fmt"
// 倒入time包
import "time"

// 导入多个包
// import (    
//     "fmt"
//     "time"
// )



// main 函数
func main(){    // 函数的左花括号必须与函数名同行
    fmt.Println("Hello Go!")
    time.Sleep(1 * time.Second)
}

// 终端命令:
// 编译并运行程序: go run 包名.go
// 分布运行:1. go build 包名.go => 生成编译后的"包名"文件; 2. 直接输入"./包名" 可以直接运行。

// 注意:可以加分号也可以不加分号,建议不加分号

2. 导入包的两种方式:

  1. 一个包一个包导入:

    go 复制代码
    import "fmt"
    import "time"
  2. 一次导入多个包:

    go 复制代码
    import (    
        "fmt"
        "time"
    )

3. 终端程序的两种运行方式:

  1. go run 包名.go,可以自动编译并运行文件,但是不会生成编译文件。
  2. go build 包名.go,可以生成编译文件,并且编译文件的名称与包名一致,但是没有后缀,然后在终端输入./包名即可运行程序。

注意

  • 对于 Go 语言,每一条语句都可以加分号也可以不加分号,建议不加分号。
  • 函数名后面的左花括号不能单独占用一行,只能与函数名在同一行,否则会报错。
  • 声明package main,只能是在有main函数下,是工程的入口,且一个工程只能有一个main包。

声明变量的方式:

1. 声明单个变量:

  • 方式一var 变量名 变量类型,没有赋初始值,默认是0

    go 复制代码
    var a int
  • 方式二var 变量名 变量类型 = 值,直接赋初始值

    go 复制代码
    var b int = 100
  • 方式三var 变量名 = 值,不直接声明变量类型,而是通过值去推测变量类型

    go 复制代码
    var c = 100
  • 方式四(常用)变量名 := 值,省略var变量类型,直接使用:=对变量赋值

    go 复制代码
    g := 3.14

注意

  • 声明全局变量 时,可以使用方式一、方式二、方式三;但是不能使用方式四
  • 方式四只能用在局部变量,只能用在函数中。

2. 声明多个变量:

  • 相同类型var 变量名1, 变量名2, ... 变量类型 = 值1, 值2, ...

    go 复制代码
    var xx, yy int = 100, 200
  • 不同类型var 变量名1, 变量名2, ... = 值1, 值2, ...

    go 复制代码
    var kk, ll = 100, "Ace"
  • 多行声明变量:

    go 复制代码
    var (
        vv int = 50
        jj bool = true
    )

3. 实例:

go 复制代码
package main
/*
	四种变量的声明方式
*/
import (
	"fmt"
)

// 声明全局变量,方法一、方法二、方法三都是可以的
var gA int = 100
var gB int = 200

// 方法四不能用来声明全局变量,只能声明局部变量。
// := 只能用在函数体内。
// gC := 300  // 错误

func main(){
	// 方法一:声明一个变量, 默认的值是0
	var a int
	fmt.Println("a =", a)

	// 方式二: 生命一个变量,并初始化值
	var b int = 100
	fmt.Println("b =", b)
	fmt.Printf("type of b = %T\n", b)

	var st_const string = "aabb"
	fmt.Printf("st_const = %s, type of st_const = %T\n", st_const, st_const)  // 格式化字符串,%s 表示输出字符串,%T 表示输出变量类型

	// 方式三: 在初始化的时候,可以省去数据类型,通过值自动匹配当前的变量的数据类型
	var c = 100
	fmt.Println("c =", c)
	fmt.Printf("type of c = %T\n", c)

	var st_auto string = "aabb"
	fmt.Printf("st_auto = %s, type of st_auto = %T\n", st_auto, st_auto)

	// 方式四:(最常用的方法),省去 var 关键字,直接自动匹配
	e := 100
	fmt.Println("e =", e)
	fmt.Printf("type of e = %T\n", e)

	f := "ffff"
	fmt.Println("f =", f)
	fmt.Printf("type of f = %T\n", f)

	g := 3.14
	fmt.Println("g =", g)
	fmt.Printf("type of g = %T\n", g)


	// 声明多个变量
	// 相同类型
	var xx, yy int = 100, 200
	fmt.Println("xx =", xx, "yy =", yy)

	// 不同类型
	var kk, ll = 100, "Ace"
	fmt.Println("kk =", kk, "ll =", ll)

	// 多行的多变量声明
	var (
		vv int = 50
		jj bool = true
	)
	fmt.Println("vv =", vv, "jj =", jj)
}

常量类型

1. 常量的声明方式:

Go 语言中,常量的声明,使用关键字const

  1. 定义一个简单的常量:

    go 复制代码
    const MINIMUM int = 1
  2. 使用 const 关键字来定义枚举:

    go 复制代码
    const (
    	BEIJING = 0
    	SHAGNHAI = 1	
    	SHENZHEN = 2	
    )
  3. 对于有规律且需要大量赋值的枚举,可以使用 iota 关键字,iota 关键字是按行递增的(增幅为 1),iota 的默认初值为 0:

    go 复制代码
    const (
    	// 可以在const()添加一个关键字iota,每行的iota都会累加1,第一行的iota的默认值是0, iota 只能在const内进行累加
    	BEIJING = iota	// iota = 0, => BEIJING = 0
    	SHAGNHAI		// iota = 1, => SHAGNHAI = 1
    	SHENZHEN		// iota = 2, => SHENZHEN = 2
    )
  4. 可以使用复杂的规则进行对const的枚举类型进行赋值,以达到目的:

    go 复制代码
    const (
    	a, b = iota + 1, iota + 2	// iota = 0, a = iota + 1, b = iota + 2 => a = 1, b = 2
    	c, d						// iota = 1, c = iota + 1, d = iota + 2 => c = 2, d = 3
    	e, f						// iota = 2, e = 3, f = 4
    
    	g,  h = iota * 2, iota * 3	// iota = 3, g = iota * 2, h = iota * 3 => g = 6, h = 9
    	i, k						// iota = 4, i = 8, k = 12
    )

注意

  • iota 关键字只能用在const的枚举类型中,不能用在其他地方。
  • iota 初值为0,并且按行递增,增幅为 1

2. 实例:

go 复制代码
package main

import "fmt"

// const 来定义枚举
// const (
// 	BEIJING = 0
// 	SHAGNHAI = 1	
// 	SHENZHEN = 2	
// )

// const (
// 	// 可以在const()添加一个关键字iota,每行的iota都会累加1,第一行的iota的默认值是0, iota 只能在const内进行累加
// 	BEIJING = iota	// iota = 0
// 	SHAGNHAI		// iota = 1
// 	SHENZHEN		// iota = 2
// )

const (
	// 可以在const()添加一个关键字iota,每行的iota都会累加1,第一行的iota的默认值是0
	BEIJING = 10 * iota	// iota = 0
	SHAGNHAI		// iota = 1
	SHENZHEN		// iota = 2
)

const (
	a, b = iota + 1, iota + 2	// iota = 0, a = iota + 1, b = iota + 2 => a = 1, b = 2
	c, d						// iota = 1, c = iota + 1, d = iota + 2 => c = 2, d = 3
	e, f						// iota = 2, e = 3, f = 4

	g,  h = iota * 2, iota * 3	// iota = 3, g = iota * 2, h = iota * 3 => g = 6, h = 9
	i, k						// iota = 4, i = 8, k = 12
)


func main() {
	// 常量
	const length int = 10
	fmt.Println("length =", length)

	fmt.Println("BEIJING =", BEIJING)
	fmt.Println("SHAGNHAI =", SHAGNHAI)
	fmt.Println("SHENZHEN =", SHENZHEN)


	fmt.Println("a =", a, "b =", b)
	fmt.Println("c =", c, "d =", d)
	fmt.Println("e =", e, "f =", f)

	fmt.Println("g =", g, "h =", h)
	fmt.Println("i =", i, "k =", k)
}

函数

1. 定义函数的几种方式:

用关键字 func 来定义函数,参数的书写方式为 参数名 参数类型,并且可以在函数名后写返回值类型,如果没有返回值可以不写,因此,其语法可以概括如下:

go 复制代码
func 函数名 (参数名1 参数类型 参数名2 参数类型, [参数名n 参数类型, ...]) 返回值类型 {
    ...
    return ...
}
  1. 定义一个简单的函数,只有一个返回值:

    go 复制代码
    func foo1(a string, b int) int {
    	fmt.Println("a =", a)
    	fmt.Println("b =", b)
    
    	c := 100
    
    	return c
    }
  2. 有多个返回值,返回值只声明返回值类型,但是没有返回值名称(匿名返回值):

    go 复制代码
    func foo2(a string, b int) (int , int) {
    	fmt.Println("a =", a)
    	fmt.Println("b =", b)
    
    	return 666, 777
    }
  3. 有多个返回值,返回值有返回值名称,有返回值名称的函数,在返回的时候直接使用return即可,并且初始化值为0(int 类型),在对其赋值的时候,直接使用返回值名赋值即可:

    go 复制代码
    func foo3(a string, b int)(r1 int, r2 int) {	// r1, r2初始化赋值为0
    	fmt.Println("---- foo3 ----")
    	fmt.Println("a =", a)
    	fmt.Println("b =", b)
    
    	// 给有名称的返回值变量赋值
    	r1 = 1000
    	r2 = 2000
    
    	// 直接进行return即可
    	return
    }
  4. 多个返回值有名函数的另一种写法,对于多个返回值且类型相同,可以直接声明返回值名称,并在最后书写返回值类型即可:

    go 复制代码
    func foo4(a string, b int)(r1, r2 int){
    	fmt.Println("---- foo4 ----")
    	fmt.Println("a =", a)
    	fmt.Println("b =", b)
    
    	r1 = 1000
    	r2 = 2000
    
    	return
    }

2. 实例:

go 复制代码
package main

import "fmt"

// 数据类型和返回值类型都写在后面,如果没有返回值可以不写
func foo1(a string, b int) int {
	fmt.Println("a =", a)
	fmt.Println("b =", b)

	c := 100

	return c
}

// 多个返回值(都是匿名的)
func foo2(a string, b int) (int , int) {
	fmt.Println("a =", a)
	fmt.Println("b =", b)

	return 666, 777
}

// 返回多个值(有返回值名称)
func foo3(a string, b int)(r1 int, r2 int) {	// r1, r2初始化赋值为0
	fmt.Println("---- foo3 ----")
	fmt.Println("a =", a)
	fmt.Println("b =", b)

	// 给有名称的返回值变量赋值
	r1 = 1000
	r2 = 2000

	// 直接进行return即可
	return
}

func foo4(a string, b int)(r1, r2 int){
	fmt.Println("---- foo4 ----")
	fmt.Println("a =", a)
	fmt.Println("b =", b)

	// 给有名称的返回值变量赋值
	r1 = 1000
	r2 = 2000

	// 直接进行return即可
	return
}

func main() {
	c := foo1("abc", 555)

	fmt.Println("c =", c)

	ret1, ret2 := foo2("haha", 99)
	fmt.Println("ret1 =", ret1)
	fmt.Println("ret2 =", ret2)

	ret1, ret2 = foo3("foo3", 333)
	fmt.Println("ret1 =", ret1, "ret2 =", ret2)

	ret1, ret2 = foo4("foo4", 444)
	fmt.Println("ret1 =", ret1, "ret2 =", ret2)
}

import导包与init方法

1. 程序的流程图:

可以看到 init() 方法的执行时机在 main() 执行时机前,因此可以在 main() 前执行一些初始化的操作。

2. 实例:

项目结构

├── lib1

│ └── lib1.go

├── lib2

│ └── lib2.go

└── main.go

main.go

go 复制代码
package main

import (
	"GolangStudy/init/lib1"
	"GolangStudy/init/lib2"
)

func main() {
	lib1.Lib1Test()
	lib2.Lib2Test()
}

lib1.go

go 复制代码
package lib1

import "fmt"

// 当前lib1包提供的API, 函数名首字母大写的话,说明该函数是一个对外开放的函数,否则只能在的当前的包内调用
func Lib1Test() {
	fmt.Println("lib1Test()...")
}

func init() {
	fmt.Println("lib1. init()...")
}

lib2.go

go 复制代码
package lib2

import "fmt"

// 当前lib2包提供的API
func Lib2Test() {
	fmt.Println("lib2Test()...")
}

func init() {
	fmt.Println("lib2. init()...")
}

注意:对于各个包中的函数来讲,函数名首字母大写,说明该函数是一个对外开放的函数,否则只能在的当前的包内调用。

3. 导自定义包失败问题:

参考链接go引入自建包名报错 package XXX is not in std_package is not in std-CSDN博客

对于 go1.23.2 版本(之前或之后的版本的可能也有这种问题),导入自定义的包会失败,错误如下:

此时需要查看是否关闭GO111MODULE,使用命令 go env 进行查看,如下是没有关闭的状态,此时不会去$GOPATH路径下寻找包,$GOPATH路径是我们写程序的路径:

此时需要关闭GO111MODULE,命令:

shell 复制代码
go env -w GO111MODULE=off

再次使用命令 go env,查看结果和下图一致,说明修改正确:

此时即可运行程序了。

注意 :导包的时候只会从$GOPATH路径下的src目录下进行寻找,因此在导包的时候,路径要从src的下一级开始写。

匿名导包与别名导包

Golang 语言是一门严谨的语言,如:对于声明的变量但是下文没有使用,这会报错,并且无法进行编译;对于导包,但是下文中没有使用,这也会报错,并且无法编译。

对于导包但是下文没有使用的问题,可以使用匿名导包的方式进行解决。

匿名导包:

  • 匿名导包的方式import _ "包路径",注意如果使用这种方式的话,在代码的下文中,就不能再使用改包中的函数了,但是还会 调用改包的init()方法,如:

    go 复制代码
    import _ "GolangStudy/init/lib1"
    
    // 或
    
    import (
    	_ "GolangStudy/init/lib1"
    	"GolangStudy/init/lib2"
    )
  • 别名导包import ex "包路径",这里使用ex来作为包的别名,下文中可以直接使用ex直接调用包中的函数,如:

    go 复制代码
    import (
    	ex "GolangStudy/init/lib1"
    	"GolangStudy/init/lib2"
    )
    
    func main() {
    	ex.Lib1Test()
    	lib2.Lib2Test()
    }
  • 直接导入import . "包路径",这里使用.来进行导包,相当于直接将包中的代码与该程序中的代码进行了合并,可直接使用导入包的函数名称,如:

    go 复制代码
    import (
    	. "GolangStudy/init/lib1"
    	"GolangStudy/init/lib2"
    )
    
    func main() {
    	Lib1Test()			// 注意这里的函数是 lib1 包中的函数
    	lib2.Lib2Test()
    }

注意 :直接导入的方式不要轻易使用,因为这种方式很可能会导致命名冲突问题,即在一个程序中导入多个包时,不同的包之间的变量或函数会出现相同的名字,从而产生命名冲突。

指针

Go 语言中也存在指针的概念,与C语言和C++中的指针相似,如果学习过C或C++那么你将会很快的上手Go语言中的指针。

指针的使用方式:

  1. 取地址 :语法& 变量名,使用这种方式可以直接取到变量的物理内存地址,如:

    go 复制代码
    var a int = 10
    // 取 a 的地址
    fmt.Println("a 的物理内存地址为:", &a)
  2. 指针 :指针的作用就是指向变量的物理内存地址,语法var 指针名 *变量的数据类型 = &变量名var 指针名 = &变量名指针名 := &变量名,对于指针,对指针使用* 指针名表示得到指针指向的变量的值,例子:

    go 复制代码
    var a = 10
    // 使用指针指向变量a的地址
    var pointer0_a *int = &a		// 注意:如果声明数据类型,则数据类型要与指向变量的数据类型相同
    var pointer1_a = &a
    pointer2_a := &a
    
    // 得到指针指向变量的值
    fmt.Println("value of pointer2_a =", *pointer2_a)

实例:交换变量的值:

go 复制代码
package main

import "fmt"

func swap(temp_a *int, temp_b *int) bool{
	fmt.Println("temp_a =", *temp_a, "temp_b =", *temp_b)
	fmt.Println("temp_a 的值是a的物理,temp_b的值是b的物理地址 : temp_a =", temp_a, "temp_b =", temp_b)
	// 进行交换
	swap := *temp_a
	*temp_a = *temp_b
	*temp_b = swap

	return true
}

func main() {
	var a, b int = 10, 5
	fmt.Println("a =", a, "b =", b)
	
	if swap(&a,  &b) {
		fmt.Println("交换成功!!!: a =", a, "b =", b)
	} else {
		fmt.Println("很遗憾,交换失败!!!")
	}

	// 输出 a的地址与 b的地质
	fmt.Println("输出 a的地址与b的地址: &a =", &a, "&b =", &b)
}

defer 关键字

defer 关键字用于在函数的生命周期结束的前调用一次。

使用方式defer 函数名(),这样可以在函数生命周期将要结束的时候 调用一次defer之后函数,通常用于关闭操作,如:在读取或写入完文件的时候关闭文件流。

注意 :对于多个defer的函数,其执行顺序是按照栈(Stack)的方式执行的,即先入后出的方式执行,其形式流程图如下:

实例:输出的顺序

go 复制代码
package main

import (
	"fmt"
)

func main() {
	defer fmt.Println("first - 1")
	defer fmt.Println("second - 2")
	defer fmt.Println("third - 3")

	fmt.Println("main")
	fmt.Println("main - end")
}

实例:函数的执行顺序

go 复制代码
package main

import (
	"fmt"
)

func foo1(){
	fmt.Println("I am foo1.")
}

func foo2(){
	fmt.Println("I am foo2")
}

func foo3(){
	fmt.Println("I am foo3")
}

func main() {
	defer foo1()
	defer foo2()
	foo3()
}

实例:return 与 defer的执行顺序

go 复制代码
package main

import (
	"fmt"
)

func returnFunction() string {
	fmt.Println("return implement!!!")
	return "SUCCEED"
}

func deferFunction() string{
	fmt.Println("defer implement!!!")
	return "SUCCEDD"
}

func compareFunction() string {
	defer deferFunction()

	return returnFunction()
}

func main() {
	compareFunction()
}

数组与动态数组

数组:是一种内置的数据类型,具有固定的大小,一旦声明,其长度就不能改变。数组在内存中是连续存储的。

**切片(动态数组)**是一种更加灵活的数据结构,它引用了一个数组的一段区域。切片没有固定的长度,你可以创建一个新的切片来引用原数组的不同部分,或者扩展原切片的长度。切片在内存中不一定是连续存储的,它们是对底层数组的一种抽象。

普通数组的声明方式:

  1. 使用var关键字进行声明,语法:var 数组名称 [数组长度] 数组元素类型 ,如:

    go 复制代码
    var myArray [10] int
  2. 使用:=进行声明,语法: 数组名称 := [数组长度] 数组元素类型 {元素1, 元素2, 元素3,...},注意使用这种方式必须要给数组赋初值,并且这种方式的声明的数组只能作用在函数体内,不能用作全局变量,如:

    go 复制代码
    myArray := [10] int {1, 2 ,3, 4, 5}

动态数组的声明方式:

在Go语言中,切片这种数据结构就是所谓的动态数组,可以使用切片来表示动态数组。

  1. 使用var关键字进行声明,语法:var 动态数组名称 [] 动态数组元素类型 = [] 动态数组元素类型 {元素1, 元素2, 元素3,...},声明时必须要给元素赋初值,如:

    go 复制代码
    var mySlice []int = []int{1, 2, 3}
  2. 使用:=进行声明,语法:动态数组名称 := [] 动态数组元素类型{元素1, 元素2, 元素3,...},声明时必须要给元素赋初值,如:

    go 复制代码
    mySlice := []int{1, 2, 3}

注意

  • 普通的数组对函数进行传参的时候,传递的实际上是数组的拷贝,对参数数组进行操作,并不会影响真实的数组。
  • 使用切片(动态数组)的方式进行传参,传递的实际上是数组的引用,对参数数组进行操作,实际上是对真实的数组进行操作。

遍历数组的两种方式:

  1. 使用索引进行遍历,如:

    go 复制代码
    for i := 0; i < len(myArray1); i++ {
        fmt.Println(myArray1[i])
    }
  2. 使用for循环结合range关键字进行遍历,这样不仅会遍历到数组中每一个元素的值,还可以遍历到每一个元素的索引,如:

    go 复制代码
    for index, value := range myArray2 {
        fmt.Println("index =", index, "value =", value)
    }

实例:固定长度数组

go 复制代码
package main

import (
	"fmt"
)

// 数组传参
func printArray(myArray [4]int){
	for index, value := range myArray {
		fmt.Println("index =", index, "value =", value)
	}
}


func main() {
	// 声明一个固定长度的数组
	var myArray1 [10] int

	myArray2 := [10] int {1, 2, 3, 4}
	myArray3 := [4] int {11, 22, 33, 44}


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

	for index, value := range myArray2 {
		fmt.Println("index =", index, "value =", value)
	}

	// 查看数组的数据类型
	fmt.Printf("myArray1 types = %T\n", myArray1)
	fmt.Printf("myArray2 types = %T\n", myArray2)
	fmt.Printf("myArray3 types = %T\n", myArray3)

	printArray(myArray3)
}

实例:动态数组

go 复制代码
package main

import (
	"fmt"
)

func printArray(myArray []int) {	//
	// _ 表示匿名变量
	for _, value := range myArray {
		fmt.Println("value =", value)
	}

	myArray[0] = 100
}

func main() {
	// 动态数组, 切片 slice,传参的时候相当于是一个引用传递,相当于传递的是一个指针,动态数组类一个指针,指向内存的一片地址
	myArray := []int {1, 2, 3, 4}
	fmt.Printf("myArray types = %T\n", myArray)

	printArray(myArray)

	fmt.Println(" ----- ")
	for _, value := range myArray {
		fmt.Println("value =", value)
	}
}

slice(切片)的声明方式

四种声明方式:

slice(切片)的声明方式一共有四种,其声明的方法如下:

  1. 使用:=关键字,声明一个切片,并且初始化值,语法:切片名称 := [] 切片数据类型 {元素1, 元素2, 元素3,...}如:

    go 复制代码
    slice1 := []int {1, 2, 3}
  2. 使用var关键字进行声明(注意:这种声明方式没有给切片分配内存空间,因此不能直接对其进行赋值操作,需要分配内存空间之后才能进行赋值操作),语法:var 切片名称 [] 切片数据类型,分配内存空间需要使用方法make(数据类型, 长度大小),分配空间后数组元素的初始的值为0,如:

    go 复制代码
    var slice2 []int	// 此时只是声明了,并没有实际的内存空间,因此不能进行赋值操作。
    // 给slice2开辟空间
    slice2 = make([]int, 3)		// 开辟3个空间,默认值为0
  3. 使用var关键字声明并且直接分配内存空间,语法:var 切片名称 [] 切片数据类型 = make(数据类型, 长度大小),如:

    go 复制代码
    var slice3 []int = make([]int , 3)
  4. 使用:=进行声明,并且直接分配内存空间,语法:切片名称 := make(数据类型, 长度大小),如:

    go 复制代码
    slice4 := make([]int , 3)

判断数组为空的方式(即没有内存空间) :使用关键字nil,例子如下:

go 复制代码
	if slice4 == nil {
		fmt.Println("slice4 是一个空切片")
	} else {
		fmt.Println("slice4 是有空间的")
	}

实例:

go 复制代码
package main

import (
	"fmt"
)

func main() {
	// 四种切片的方式
	// 第一种方式, 声明是一个切片,并且初始化值为1, 2, 3, 长度为3
	slice1 := []int {1, 2, 3}
	fmt.Printf("len = %d, slice1 = %v\n", len(slice1), slice1)		// %v 表示打印出详细的信息

	// 第二种方式:声明slice2是一个切片,但是并没有给slice分配空间,在没有空间的时候是不能够赋值的
	var slice2 []int
	// 给slice2开辟空间
	slice2 = make([]int, 3)		// 开辟3个空间,默认值为0
	slice2[0] = 100
	fmt.Printf("len = %d, slice2 = %v\n", len(slice2), slice2)

	// 第三种方式:声明一个slice3,并且分配3个空间
	var slice3 []int = make([]int , 3)
	fmt.Printf("len = %d, slice3 = %v\n", len(slice3), slice3)

	// 第四种方式:声明一个slice4,并且分配3个空间,使用 := 方式进行推导
	slice4 := make([]int , 3)
	fmt.Printf("len = %d, slice4 = %v\n", len(slice4), slice4)

	// 判断 slice是否为空
	if slice4 == nil {
		fmt.Println("slice4 是一个空切片")
	} else {
		fmt.Println("slice4 是有空间的")
	}
}

slice的追加与切片

slice的追加方式:

使用方法append(数组,元素),如:

go 复制代码
numbers = append(numbers, 1)	// 这里的number是一个slice(切片)

slice的属性:

slice(切片)的属性有长度(length)和容量(capacity)。

  • 长度(length):切片的长度是指切片中包含的元素数量。你可以使用内置的 len() 函数来获取切片的长度。例如,如果你有一个切片 s,那么 len(s) 将返回 s 中的元素数量
  • 容量(capacity):切片的容量是指切片可以增长到的最大长度,而不需要重新分配底层数组。容量总是大于或等于长度。你可以使用内置的 cap() 函数来获取切片的容量。例如,如果你有一个切片 s,那么 cap(s) 将返回 s 的容量。

在创建 slice 的时候,使用make()方法不仅可以分配切片长度,还可以分配容量大小,语法make(数据类型, 长度大小, 容量大小),如:

go 复制代码
var numbers = make([]int, 3, 5)		// 给 numbers 切片分配3个长度大小,并且分配的总容量大小为5

注意 :由于slice可以充当动态数组 ,因此如果不断的向slice中添加元素,直到超过了分配总容量大小,此时go语言底层会再次分配给该切片一个初始容量大小,如初始的时候使用make分配给slice容量大小为5,当不断向slice中添加元素的时候,超过了5,即切片中需要添加第6个元素,此时go语言底层会在次分配给该slice 5个容量大小,总容量变为了10,如果添加元素超过了10,那么会再次分配5个容量大小,如此往复。

slice的截取方式:

slice 的截取方式与 Python 的切片方式一样,遵循左闭右开的原则,截取的结果实际上与原来的动态数组所指向的内存空间一致,如:

go 复制代码
s := []int{1, 2, 3}

// 截取索引0, 1, 左闭右开,与Python类似,但是截取的结果实际上是同一片内存空间,及s1和s指向同一片内存空间
s1 := s[0: 2]

slice 相当于一个指针,指向内存的一片区域,截取相当于在该内存区域添加一个新的指针,并且规定新的指针的首和尾,对截取进行操作,原来的slice也会进行相应的改变。

深度拷贝:

深度拷贝使用方法copy(待拷贝的变量,新的变量),这样会重新开辟一个内存空间然后将值复制到该内存空间中,注意新的变量的内存空间一定要大于等于待拷贝的变量的内存空间,否则会报错,如:

go 复制代码
s2 := make([]int, 3)	// s2 = [0, 0, 0]
// 将s中的值拷贝到s2中
copy(s2, s)

实例:slice的追加

go 复制代码
package main

import (
	"fmt"
)

func main() {
	var numbers = make([]int, 3, 5)		// 长度为3, 容量为5, 容量表示切片内存的总量是多少,数组大小不能越界

	fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers),  numbers)

	// 追加元素
	numbers = append(numbers, 1)
	fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers),  numbers)

	numbers = append(numbers, 2)
	fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers),  numbers)

	// 此时超过了切片的最大容量,go 语言会在底层开辟新的空间,新的空间的大小与原来空间大小相等
	numbers = append(numbers, 3)
	fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers),  numbers)

	// 如果不去声明,则设置的容量与长度一致
	var numbers2 = make([]int, 3)
	fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2),  numbers2)
	numbers2 = append(numbers2, 1)
	fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2),  numbers2)
}

fmt.Printf()中使用%v表示能够输出任何数据类型的信息。

实例:slice的切片

go 复制代码
package main

import (
	"fmt"
)

func main() {
	s := []int{1, 2, 3}		// len = 3, cap = 3

	// 截取索引0, 1, 左闭右开,与Python类似,但是截取的结果实际上是同一片内存空间,及s1和s指向同一片内存空间
	s1 := s[0: 2]
	s1[0] = 100
	fmt.Println(s1)
	fmt.Println(s)

	// 深度拷贝, copy
	s2 := make([]int, 3)	// s2 = [0, 0, 0]
	// 将s中的值拷贝到s2中
	copy(s2, s)
	fmt.Println(s2)
	s2[2] = -1		// 此时不会改变s中的值,因为是深拷贝
	fmt.Println("s2 =", s2)
	fmt.Println("s =", s)
}

练习题:斐波那契数列

go 复制代码
package main

import (
	"fmt"
)
// 实现斐波那契数列 ------ 动态规划
func fb(number int) (int, [] int){
	temp_array := make([]int, number + 1, number + 1)
	if number <= 0 {
		return 0, temp_array
	} else if number == 1 {
		temp_array[number] = 1
		return 1, temp_array
	} else if number == 2 {
		temp_array[1] = 1
		temp_array[2] = 1
		return temp_array[number], temp_array
	}
	temp_array[1] = 1
	temp_array[2] = 1
	for i := 3; i <= number; i++{
		temp_array[i] = temp_array[i - 1] + temp_array[i - 2]
	}
	return temp_array[number], temp_array
}

// 遍历切片
func printSlice(slice []int) {
	fmt.Println("====== 《遍历切片》 ======")
	fmt.Println("- 切片的总长度为: ", len(slice))
	for _, value := range slice {
		fmt.Printf("%d, ", value)
	}
	fmt.Println()
}

func main() {
	var input int		// 用于存储控制台中的值
	fmt.Printf("- 请输入要计算的第几个斐波那契数:")
	fmt.Scanln(&input)
	fb_value, slice := fb(input)
	fmt.Println("- 斐波那契数为:", fb_value)
	fmt.Println("- 该斐波那契数前的所有值为:", slice)
	printSlice(slice)
}

map 的声明方式

map 等同于 Python 中的字典格式,其底层是基于哈希表实现的。在Go语言中,map 的底层实现也使用了哈希表,但它使用了一个不同的策略来存储键值对。

三种声明方式:

在Golang语言中,一共有三种声明 map(又称:映射) 的方式。

  1. 使用var关键字进行声明,语法:var 映射名称 map[key数据类型] value数据类型,注意,这种声明方式并没有给map划分存储空间,所以需要进行分配内存空间后才能正常使用,使用map前需要使用make分配内存空间,和slice一样,会动态开辟内存空间,如:

    go 复制代码
    var myMap1 map[int] string
    myMap1 = make(map[int] string, 10)
  2. 使用:=方式进行声明,语法:映射名称 := make(map[key数据类型] value数据类型),注意,如果不确定要分配多少内存,可以省略make()方法的第二个参数,如:

    go 复制代码
    myMap2 := make(map[string]string)
  3. 声明map的同时,给映射赋初值,注意这种方式,赋初值的每一个键值后都必须要有一个,(逗号),包括最后一个键值对也必须要有一个逗号结尾,如:

    go 复制代码
    myMap3 := map[string]string {
        "one": "php",
        "two": "c++",
        "three": "python",		//  注意每一个及键值对后面都需要有一个逗号
    }

map 的使用方式:

map 的增、删、改、查操作。

  1. 添加数据 ,直接给key进行赋值即可,如:

    go 复制代码
    cityMap["China"] = "Beijing"
    cityMap["Japan"] = "Tokyo"
    cityMap["USA"] = "NewYork"
  2. 遍历数据 :使用forrange进行遍历,注意,这种遍历方式会返回映射的keyvalue,如:

    go 复制代码
    for key, value := range cityMap {
        fmt.Printf("key = %s, value = %s\n", key, value)
    }
  3. 删除数据 :使用delete()方法进行删除,语法delete(映射名称, key),如:

    go 复制代码
    delete(cityMap, "China")
  4. 修改数据 :直接对映射中的key进行赋值即可,如:

    go 复制代码
    cityMap["USA"] = "DC"

注意map 的传参方式是引用传参,改变参数的值会导致实际的map的值发生变化。如:

go 复制代码
func ChangeValue(cityMap map[string]string) {
	cityMap["England"] = "London"		// 会改变实际的cityMap映射的 key = England 的值
}

实例:map的三种声明方式

go 复制代码
package main

import (
	"fmt"
)

// map 的三种声明方式
func main() {
	// 第一种声明方式:key 是 int类型, value 是string类型
	var myMap1 map[int] string
	if myMap1 == nil {
		fmt.Println("myMap1 是一个空map")
	}
	// 给 map 开辟内存空间,使用map前需要使用make分配内存空间,和slice一样,会动态开辟内存空间
	myMap1 = make(map[int] string, 10)

	myMap1[100] = "JAVA"
	myMap1[200] = "PYTHON"
	myMap1[300] = "C ++"

	fmt.Println(myMap1)


	// 第二种声明方式
	myMap2 := make(map[string]string)	// 如果不确定要分配多少内存,可以省略第二个参数
	myMap2["one"] = "JAVA"
	myMap2["two"] = "PYTHON"
	myMap2["three"] = "C ++"

	fmt.Println(myMap2)

	// 第三种声明方式
	myMap3 := map[string]string {
		"one": "php",
		"two": "c++",
		"three": "python",		//  注意每一个及键值对后面都需要有一个逗号
	}
	fmt.Println(myMap3)
}

实例:map的使用方法

go 复制代码
package main

import "fmt"

// map 的使用方法

// 传参方法
func printMap(cityMap map[string]string) {
	// cityMap 是一个引用传递
	for key, value := range cityMap {
		fmt.Printf("key = %s, value = %s\n", key, value)
	}
}

func ChangeValue(cityMap map[string]string) {
	cityMap["England"] = "London"
}

func main() {
	cityMap := make(map[string]string)
	// 添加
	cityMap["China"] = "Beijing"
	cityMap["Japan"] = "Tokyo"
	cityMap["USA"] = "NewYork"
	// 遍历
	printMap(cityMap)

	// 删除
	delete(cityMap, "China")
	
	// 修改
	cityMap["USA"] = "DC"
	ChangeValue(cityMap)

	fmt.Println("=============")
	// 遍历
	for key, value := range cityMap {
		fmt.Println("key =", key)
		fmt.Println("value =", value)
	}

}

结构体

Golang 语言中声明结构体使用struct关键字进行声明.

结构体的声明方法:

  • 换名 :可以使用type关键字对数据类型启别名,可以理解为声明一种新的数据类型,如:

    go 复制代码
    type myint int		// 这样在程序中使用 myint 等价于使用 int
  • 结构体 :结构体相当于自定义的数据类型,与面向对象语言中的很相似,定义方法如下:

    go 复制代码
    type Book struct {
    	title string
    	auth string
    }

结构体的使用方法

  1. 创建结构体:创建结构体的方法,与创建常规的变量类似,只不过将常见的数据类型转变为自定义结构体的数据类型,如:

    go 复制代码
    var book1 Book
  2. 修改内部属性 :更改结构体变量的内部属性,直接使用.的方法进行引用即可,如:

    go 复制代码
    book1.title = "Golang"
    book1.auth = "zhangsan"
  3. 结构体传参:结构体传参与常规的普通数据变量传参一致,可以直接使用结构体变量名称进行传参,这样传参的方式实际上传递的是结构体变量的副本,对副本进行修改不会影响实际的结构体变量数据,也可以使用引用传参的方式,引用传参的方式传递的是结构体变量的地址,具体例子如下:

    go 复制代码
    // 普通的传参方式
    func changeBook(book Book) {
    	// 传递一个book副本
    	book.auth = "666"
    }
    
    // 引用传参方式,传递的实际上是一个结构体变量的地址
    func changeBook2(book *Book) {
    	// 指针传递
    	book.auth = "777"
    
    }
    
    
    changeBook(book1)
    changeBook2(&book1)

实例:

go 复制代码
package main

import "fmt"

// 结构体
// type 用于声明一种新的数据类型,是int的一个别名
type myint int

// 定义一个结构体
type Book struct {
	title string
	auth string
}

// 结构体传参
func changeBook(book Book) {
	// 传递一个book副本
	book.auth = "666"
}

func changeBook2(book *Book) {
	// 指针传递
	book.auth = "777"

}

func main(){
	// var a myint = 10
	// fmt.Printf("a = %d, type of a = %T\n", a, a)

	var book1 Book
	book1.title = "Golang"
	book1.auth = "zhangsan"
	fmt.Printf("%v\n", book1)

	changeBook(book1)

	fmt.Printf("%v\n", book1)

	changeBook2(&book1)
	fmt.Printf("%v\n", book1)
}

在 Golang 语言中,使用关键字struct来定义一个类,类的方法是一个独立的函数,并不是写在类中的。

定义一个类:

使用关键字struct来定义一个类,可以在类中定义属性,如:

go 复制代码
type Hero struct {
	// 属性名称首字母大写,表示共有属性;首字母小写,表示私有属性。
	id int
	Name string
	Level int
}

定义类的方法:

定义一个类的方法与定义一个函数格式类似,其语法为:func (this 类名) 方法名(参数1, ...) 返回类型 {方法体},如:

go 复制代码
func (this Hero) SetName(newName string) {
	// this 调用的实际上是对象的副本(拷贝)
	this.Name = newName
}

注意:这种类方法不会改变对象的属性的值,因为传入方法中的函数实际上对象的副本,如果需要改变对象的属性值,需要使用指针类型才能够真正意义上改变对象的属性值,如:

go 复制代码
func (this *Hero) GetName() string {
	fmt.Printf("Name =", this.Name)
	return this.Name
}

实例:

go 复制代码
package main

import "fmt"


// 声明一个类
// 类名首字母大写表示公有类,即可以被包外的程序访问,首字母小写表示私有的类,不能被包外的程序
type Hero struct {
	// 属性名称首字母大写,表示共有属性;首字母小写,表示私有属性。
	id int
	Name string
	Level int
}

// 创建类的方法
// 类方法名称大写,表示共有方法,小写表示私有的方法
func (this Hero) Show() {
	fmt.Println("ID =", this.id)
	fmt.Println("Name =", this.Name)
	fmt.Println("Level =", this.Level)
}

/*
func (this Hero) GetName() string {
	fmt.Printf("Name =", this.Name)
	return this.Name
}

func (this Hero) SetName(newName string) {
	// this 调用的实际上是对象的副本(拷贝)
	this.Name = newName
}
*/

// 指针类型的类方法
func (this *Hero) GetName() string {
	fmt.Printf("Name =", this.Name)
	return this.Name
}

func (this *Hero) SetName(newName string) {
	// 使用指针类型this来引用对象的地址,this参数指向的实际上是目标对象的内存地址
	this.Name = newName
}


func main() {
	// 创建类对象
	hero := Hero{id: 1, Name: "HK", Level: 100}
	hero.Show()

	hero.SetName("DJ")

	fmt.Println("=====================")
	hero.Show()
}

封装

在 Golang 语言中,类的封装是通过其名称的首字母的大小写来表示的。

  1. 类的封装
    • 首字母大写:表示类是一个共有类,可以被该类所在的包外的其他程序调用。
    • 首字母小写:表示类是一个私有类,不可以被该类所在的包外的其他程序调用。
  2. 属性封装
    • 首字母大写:表示该属性是一个共有属性,可以被该类所在的包外的其他程序调用。
    • 首字母小写:表示该属性是一个私有属性,不可以被该类所在的包外的其他程序调用。
  3. 方法封装
    • 首字母大写:表示类是一个共有类,可以被该类所在的包外的其他程序调用。
    • 首字母小写:表示类是一个私有类,不可以被该类所在的包外的其他程序调用。

继承

Golang 语言中,直接在类中的第一行写需要继承的类名即可实现继承,如:

go 复制代码
type Human struct {
	name string
	sex string
}

type SuperHuman struct {
	Human	// 直接声明需要继承的父类
	level int	// 子类的属性
}

如果需要从写父类的方法,直接给子类定义与父类方法名称相同的方法即可,如:

go 复制代码
// 父类的 Eat 方法
func (this *Human) Eat() {
	fmt.Println("Human Eating ...")
}

// 子类SuperHuman 继承父类Human,重写父类的Eat()方法
func (this *SuperHuman) Eat() {
	fmt.Println("SuperHuman Eat ...")
}

实例:

go 复制代码
package main

import "fmt"

type Human struct {
	name string
	sex string
}

func (this *Human) Eat() {
	fmt.Println("Human Eating ...")
}

func (tihs *Human) Sleep() {
	fmt.Println("Human Sleeping ...")
}

type SuperHuman struct {
	Human	// 直接声明需要继承的父类
	level int	// 子类的属性
}

// 子类重写父类的方法
func (this *SuperHuman) Eat() {
	fmt.Println("SuperHuman Eat ...")
}

// 子类的新的方法
func (this *SuperHuman) Fly() {
	fmt.Println("SuperHuman Fly ...")
}

func (this *SuperHuman) Print() {
	fmt.Println("name =", this.name)
	fmt.Println("sex =", this.sex)
	fmt.Println("level =", this.level)
}

func main() {
	soym := Human{name: "soyMilk", sex: "male"}
	soym.Eat()
	soym.Sleep()

	fmt.Println("======================")
	// 定一个子类(方式一)
	// supSoym := SuperHuman{Human{"soyMilk", "male"}, 99}
	// (方式二)
	var supSoym SuperHuman
	supSoym.name = "soyMilk"
	supSoym.sex = "male"
	supSoym.level = 99

	supSoym.Eat()	// 子类重写父类的方法
	supSoym.Sleep()	// 子类继承父类的方法
	supSoym.Fly()	// 子类自己的方法

	fmt.Println("=======================")
	supSoym.Print()

}

定义类对象的两种方法:

  1. 使用关键字var来定义,这种方法很直观,如果是声明一个继承的子类,需要将父类以及子类中的所有属性给初始化:

    go 复制代码
    var supSoym SuperHuman
    supSoym.name = "soyMilk"
    supSoym.sex = "male"
    supSoym.level = 99
  2. 使用:=来定义,这种方法稍微有一些不直观,如果是声明一个子类,需要显示的将父类中的属性给初始化:

    go 复制代码
    supSoym := SuperHuman{Human{"soyMilk", "male"}, 99}		// 这是一个继承字 Human 类的子类的实例对象

多态

实现多态的方式需要使用到关键字interfaceinterface用来定义一个接口,接口中可以声明方法,只是声明而不去实现这些方法,接口的实例对象相当于是一个指针,在赋值时需要赋值具体类的地址。

定义接口:

go 复制代码
type AnimalIF interface {
	Sleep()
	GetColor() string	// 得到动物的颜色
	GetType() string	// 得到动物的种类
}

实现接口的类:

如果要实现接口的方法,需要创建一个类,来实现接口中所有声明的方法(注意:这里需要实现接口中定义的所有声明的方法,不能是其中的一部分),这样才算类实现了该接口。

go 复制代码
type AnimalIF interface {
	Sleep()
	GetColor() string	// 得到动物的颜色
	GetType() string	// 得到动物的种类
}

// 具体的类
type Cat struct {
	Color string	// 表示猫的颜色
}

// 重写接口的方法,需要将接口中的所有方法全部重写,否则就仅仅只是类自己的方法
func (this *Cat) Sleep() {
	fmt.Println("Cat Sleep ...")
}

func (this *Cat) GetColor() string {
	return this.Color
}

func (this *Cat) GetType() string {
	return "Cat"
}
// Dog
type Dog struct {
	Color string
}

func (this *Dog) Sleep() {
	fmt.Println("Dog Sleep ...")
}

func (this *Dog) GetColor() string {
	return this.Color
}

func (this *Dog) GetType() string {
	return "Dog"
}

实例:

go 复制代码
package main

import "fmt"

// interface (接口) 本质上是一个指针
type AnimalIF interface {
	Sleep()
	GetColor() string	// 得到动物的颜色
	GetType() string	// 得到动物的种类
}

// 具体的类
type Cat struct {
	Color string	// 表示猫的颜色
}

// 重写接口的方法,需要将接口中的所有方法全部重写,否则就仅仅只是类自己的方法
func (this *Cat) Sleep() {
	fmt.Println("Cat Sleep ...")
}

func (this *Cat) GetColor() string {
	return this.Color
}

func (this *Cat) GetType() string {
	return "Cat"
}


// Dog
type Dog struct {
	Color string
}

func (this *Dog) Sleep() {
	fmt.Println("Dog Sleep ...")
}

func (this *Dog) GetColor() string {
	return this.Color
}

func (this *Dog) GetType() string {
	return "Dog"
}

func showAnimal(animal AnimalIF) {		// 对传入的不同的类对象,会直接调用不同类对象的自己的对应的方法。
	animal.Sleep()
	fmt.Println("Color =", animal.GetColor())
	fmt.Println("Kind =", animal.GetType())
}

func main() {
	// var animal AnimalIF	// 接口类型的数据
	
	// animal = &Cat{"Brown"}	// 这里体现出接口是一个指针的具体含义了
	// animal.Sleep()		// 调用的就是Cat的Sleep方法

	// animal = &Dog{"Yellow"}
	// animal.Sleep()		// 调用的是Dog的Sleep方法
	cat := Cat{"Brown"}
	dog := Dog{"Yellow"}

	showAnimal(&cat)	// 注意这里传递的是对象的地址
	showAnimal(&dog)
}

interface 万能接口

在go语言中基本的数据类型(int,string,bool,float32,float64,struct,...)都实现了 interface{},因此interface{}可以引用任意的数据类型,因此又称为万能的数据类型,一个最简单的例子,如:

go 复制代码
var test string = "TEST"
var intest interface{} = test		// 这里声明的一个interface{}数据类型可以直接赋值为test的值(string)
fmt.Println("intest =", intest)

断言机制:

断言(Assertion)是编程中用来验证程序状态的一种机制。它允许开发者声明某个条件必须为真,如果条件为假,则程序会抛出错误或异常。断言通常用于调试目的,以确保程序的某个部分按照预期工作。

断言的基本思想是,如果程序的某个部分在逻辑上应该总是为真,那么可以通过断言来验证这一点。如果断言失败(即条件为假),则程序会立即停止执行,并提供有关失败位置的信息,这有助于开发者快速定位和修复问题。

Go 语言中,使用变量名.(数据类型)来对某一个变量进行断言,这种方式返回两个变量:valueerr,其中value表示该变量的值,err表示该变量的是否与指定的数据类型一致,是一个bool类型的变量,如:

go 复制代码
value, ok := arg.(string)	// 这里的arg是一个interface{}类型的形参

实例:

go 复制代码
package main

import "fmt"

// interface{} 是万能的数据类型,
func myFunc(arg interface{}) {
	fmt.Println("myFunc is called ...")
	fmt.Println(arg)

	// 给 interface{} 提供 "断言" 的机制
	value, ok := arg.(string)		// 判断args是否是字符串类型
	if !ok {
		fmt.Println("arg is not string type")
	} else {
		fmt.Println("arg is string type, value =", value)

		fmt.Printf("value type is %T\n", value)
	}

}

type Book struct {
	auth string
}

func main() {
	book := Book{"Golang"}

	myFunc(book)
	myFunc(100)
	myFunc("abc")
	myFunc(3.14)
}

/*
interface 是一个通用万能类型,是一个空接口; int, string, fload32, float64, struct... 等数据类型都实现了 interface{}, 因此可以使用
interface{}类型 来引用任意的数据类型。
*/

pair结构详解

每一个实例对象都包含一个pair<type, value>内置结构,这个结构不是显示的,直观上来讲,每一个实例对象都有一个数据类型和一个具体的值,这是毫无异议的。这里说的实例对象表示的是具有具体的值和类型的对象,如:整型变量、结构体变量、类对象等都属于实例对象,都是某一种数据类型的具体实现。

pair结构图

注意 :pair结构并不是显式存在的,它是一种隐含的结构,在每一个实例对象中。

实例:简单的pair结构变换

go 复制代码
package main

import "fmt"

func main() {
	var a string
	// pair<statictype:string, value: "abcde">
	a = "abcde"

	// pair<type: string, value: "abcde">
	var allType interface{}
	allType = a		// 这里将a的数据类型赋值给了allType,其值也赋值给了allType了

	str, _ := allType.(string)
	fmt.Println(str)
}

实例:调用linux终端

go 复制代码
package main

import (
	"fmt"
	"os"
	"io"
)

/* 改代码只是一个pair的变化过程的演示,并无实际意义 */

func main() {
	// "/dev/tty" 表示linux的终端
	// tty: pair<type: *os.File, value: "/dev/tty"文件描述符>
	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

	if err != nil {
		fmt.Println("open file error", err)
		return
	}

	// r: pair<type: , value: >
	var r io.Reader
	// r: pair<type: *os.File, value: "/dev/tty"文件描述符>
	r = tty

	// w: pair<type: , value: >
	var w io.Writer
	// w: pair<type: *os.File, value: "/dev/tty"文件描述符>
	w = r.(io.Writer)		// 这一步可以理解为强制类型转换

	w.Write([]byte("HELLO THIS is A TEST !!!\n"))
}

实例:

go 复制代码
package main

import (
	"fmt"
)

type Reader interface {
	ReadBook()
}

type Writer interface {
	WriteBook()
}

// 具体的类
type Book struct {

}

func (this *Book) ReadBook() {
	fmt.Println("Read a Book")
}

func (this *Book) WriteBook() {
	fmt.Println("Write a Book")
}

func main() {
	// b: pair<type: Book, value: book{}地址>
	b := &Book{}

	// r: pair<type: , value: >
	var r Reader
	// r: pair<type: Book, value: book{}地址>
	r = b

	r.ReadBook()

	var w Writer
	// w: pair<type: Book, value: book{}地址>
    w = r.(Writer)		// 此处会断言成功,是因为w和r具体的type是一致的,都是interfac{} 数据类型

	w.WriteBook()
}

reflect反射机制

Go语言中的反射(reflection)是一种在运行时检查程序本身的机制,它允许程序在运行时动态地操作对象的类型和值。Go的反射机制主要通过reflect包实现,它提供了TypeValue两个核心类型,分别代表了Go的类型信息和值信息。

概述:

导包方法

go 复制代码
import "reflect"

// 或

import (
	"reflect"
)
  • 动态的取到变量的数据类型type ),语法reflect.TypeOf(变量名称),该方法返回的是变量的数据类型:

    go 复制代码
    fmt.Println("type: ", reflect.TypeOf(arg))
  • 动态的取到变量的值value ),语法reflect.ValueOf(变量名称),该方法返回的是变量的值:

    go 复制代码
    fmt.Println("value: ", reflect.ValueOf(arg)

注意:在使用reflect.TypeOf(变量名称)的时候,传递的变量如果是一个指针,那么返回的将会是一个地址,如:变量是一个指向结构体的指针,那么直接打印返回的数据类型,将会得到&{main.结构体名称},这是一个地址类型的数据类型,并且可读性很差,如果要得到指针指向的具体的数据类型,可以在返回的变量后添加.Name()来得到具体的数据类型,而不是一个地址,如:

go 复制代码
inputType := reflect.TypeOf(input)		// input 是一个指针,指向一个结构体类型。
fmt.Println("inputType is :", inputType.Name())	
  • 通过TyepOf()来得到类或结构体中每一个字段(属性),可以划分为两个步骤:

    1. 得到一共有多少个字段(属性),语法:inputType.NumField(),这里的 inputTypeTypeOf()返回的值。这样可以得到字段(属性)的个数:

      go 复制代码
      nums := inputType.NumField()
    2. 得到每一个字段,语法:inputType.Field(i),表示得到第i个字段(属性)。可以通过for循环来得到所有的字段信息:

      go 复制代码
      for i := 0; i< inputType.NumField(); i++ {
          field := inputType.Field(i)		// 这里得到的是每一个字段实例
          value := inputValue.Field(i).Interface()
      
          fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
      }

      注意 :在Go语言的反射(reflection)中,reflect.ValueInterface()方法被用来将reflect.Value对象转换回其原始类型的值 ,这个原始类型的值被封装在Go的interface{}类型中。

实例:简单的反射机制

go 复制代码
package main

import (
	"fmt"
	"reflect"
)

func reflectNum(arg interface{}) {
	fmt.Println("type: ", reflect.TypeOf(arg))		// reflect.TypeOf可以取到变量的数据类型
	fmt.Println("value: ", reflect.ValueOf(arg))	// reflect.ValueOf可以取到变量的值

}

func main() {
	var num float64 = 1.2345

	reflectNum(num)
}

实例:reflect进阶

go 复制代码
package main

import (
	"reflect"
	"fmt"
)

type User struct {
	Id int
	Name string
	Age int
}

func (this User) Call() {
	fmt.Println("user is called ..")
	fmt.Printf("%v\n", this)

}

func main() {
	user := User{1, "Aceld", 18}

	DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
	// 获取input的type
	inputType := reflect.TypeOf(input)
	fmt.Println("inputType is :", inputType.Name())		// 直接打印出(干净的)当前数据类型的名称
	// 获取input的value
	inputValue := reflect.ValueOf(input)
	fmt.Println("inputValue is :", inputValue)
	// 通过type获取里面的属性
	// 1. 获取interface的reflect.Type,通过Type得到NumField(一共有多少个属性)
	// 2. 得到每个field,数据类型
	// 3. 通过field有一个Interface()方法得到对应的value
	for i := 0; i< inputType.NumField(); i++ {
		field := inputType.Field(i)
		value := inputValue.Field(i).Interface()

		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 通过type 获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}

结构体标签(Tag)

在Go语言中,结构体(struct)的字段可以被"标签"(tag),这是一种特殊的字符串,它紧跟在字段声明的类型后面,用反引号(```)包裹。标签提供了一种为结构体字段添加元数据的方式,这些元数据可以被反射(reflection)用来在运行时读取。

反射解析结构体标签:

  1. 定义结构体标签,在结构体中,每一个字段后可以使用反引号来写入标签,如:

    go 复制代码
    type resume struct {
    	Name string `info:"name" doc:"我的名字"`		// 使用反引号来写标签, 注意这里使用空格进行分隔,不要无故的添加空格
    	Sex string `info:"sex"`
    }
  2. 使用reflect来解析结构体标签,首先得到结构体中的每一个字段(方法在上一节中【reflect反射机制】),对每一个字段使用Get("标签名"),这样就可以得到每一个字段标签的值了,如:

    go 复制代码
    t := reflect.TypeOf(str).Elem()		// Elem需要获取指针所指的结构体类型,所以需要传递指针
    
    for i := 0; i < t.NumField(); i++ {
        tagstring := t.Field(i).Tag.Get("info")
        tagdoc := t.Field(i).Tag.Get("doc")
        fmt.Println("info: ", tagstring, "doc: ", tagdoc)
    }

    注意

    在Go语言的反射(reflection)中,Elem()方法用于获取一个指针类型的底层类型。当你使用reflect.TypeOf()方法时,如果传递给它的是一个指针,那么返回的Type对象将代表指针类型,而不是指针指向的类型。

结构体与Json之间的转换:

结构体标签还可以与 Json 进行结合使用,可以将结构体转换为 Json 类型,同样可以将 Json 类型转换为结构体类型。需要使用到包encoding/json来进行解析。

导包方法

go 复制代码
import "encoding/json"

// 或

import (
	"encoding/json"
)
  1. 声明一个结构体,并且添加结构体标签,注意这里的结构体标签要以json为标签的名称,json标签对应的值可以转换为json中的键,并且结构体中字段的值可以转换为json中的值,如:

    go 复制代码
    type Movie struct {
    	Title string	`json:"title"`
    	Year int		`json:"year"`
    	Price int		`json:"rmb"`
    	Actors []string	`json:actors`
    }
    // 对于这部分,转换为json数据后,键值对可以表示为:"title": Title.Value()
  2. 带标签的结构体转换为Json数据 ,需要使用方法:json.Marshal(结构体对象实例),该方法返回两个值jsonStrerrjsonStr表示结构体转换为的json字符串,err表示是否转换成功了,是一个bool的数据类型,:

    go 复制代码
    jsonStr, err := json.Marshal(movie)
  3. Json数据转换为结构体变量 ,使用方法:json.Unmarshal(Json变量名称, &结构体空变量),该方法返回一个值err,表示是否转换成功,如:

    go 复制代码
    myMovie := Movie{}							// 注意,要想将json数据转换为结构体类型,需要先定义一个空的结构体类型
    err = json.Unmarshal(jsonStr, &myMovie)

实例:反射解析结构体标签

go 复制代码
package main

import (
	"fmt"
	"reflect"
)

type resume struct {
	Name string `info:"name" doc:"我的名字"`		// 使用反引号来写标签, 注意这里使用空格进行分隔
	Sex string `info:"sex"`
}

func findTag(str interface{}) {
	// 如果明确指导str是一个非指针变量,就可以不加Elem(),否则就需要加Elem()
	t := reflect.TypeOf(str).Elem()		// Elem需要获取指针所指的结构体类型,所以需要传递指针

	for i := 0; i < t.NumField(); i++ {
		tagstring := t.Field(i).Tag.Get("info")
		tagdoc := t.Field(i).Tag.Get("doc")
		fmt.Println("info: ", tagstring, "doc: ", tagdoc)
	}
}

func main() {
	var re resume

	findTag(&re)
}

实例:Json与结构体之间的转换

go 复制代码
package main

import (
	"fmt"
	"encoding/json"
)

// 定义一个结构体
type Movie struct {
	Title string	`json:"title"`
	Year int		`json:"year"`
	Price int		`json:"rmb"`
	Actors []string	`json:actors`
}

func main() {
	// 实例化一个结构体
	movie := Movie{"戏剧之王", 2000, 10, []string{"zhouxingxing", "zhangbozhi"}}

	// 编码过程
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		fmt.Println("json marshal error", err)
		return 
	}

	fmt.Printf("jsonStr = %s\n", jsonStr)


	// 解码过程
	// jsonStr = {"title":"戏剧之王","year":2000,"rmb":10,"Actors":["zhouxingxing","zhangbozhi"]}
	myMovie := Movie{}
	err = json.Unmarshal(jsonStr, &myMovie)
	if err != nil {
		fmt.Println("json unmarshal error", err)
		return
	}

	fmt.Printf("%v\n", myMovie)
}

协程(coroutine)

进程、线程、协程:

定义:
  • 进程:进程是操作系统进行资源分配和调度的一个独立单位。它是应用程序运行的实例,拥有独立的内存空间。
  • 线程:线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自身不拥有系统资源,只拥有一点在运行中必不可少的资源(如执行栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
  • 协程:协程是一种程序组件,它允许挂起和恢复执行,通常用于处理I/O密集型任务。协程是一种用户态的轻量级线程,由程序员显式创建和管理。
区别:
  1. 资源管理
    • 进程拥有独立的资源。
    • 线程共享进程资源。
    • 协程通常不拥有资源或只拥有少量资源。
  2. 调度方式
    • 进程由操作系统调度。
    • 线程由操作系统调度。
    • 协程由程序本身调度,这句话的意思是协程的执行流程是由程序员通过代码控制的,而不是由操作系统的调度器来管理。
  3. 开销
    • 进程的创建和销毁开销最大,线程次之,协程最小。
  4. 并发性
    • 进程和线程可以实现并发执行,协程则通过协作式调度实现并发

进程

注意 :这里的单个进程的操作系统也可以使用时间片轮转算法,也可以一定程度解决阻塞问题,这里列举的是较为早期的操作系统,用以突出多进程的优势,多进程的最大的优势是并行处理

线程

并发与并行

  • 并发(Concurrent)

    • 并发指的是两个或多个计算或任务在同一时间段内开始、运行至完成,但其执行点不必同时发生。
    • 例子:一个Web服务器处理多个客户端请求,虽然请求是并发处理的,但服务器可能只有一个CPU核心,通过快速切换任务来给用户并发处理的假象。
  • 并行(Parallel)

    • 并行指的是两个或多个计算或任务在同一时刻真正地同时执行。
    • 例子:一个支持多核处理器的科学计算程序,可以并行地在每个核心上运行不同的计算任务,从而加快整体计算速度。

协程

线程:协程 = 1:1

对于线程:协程 = 1:1模型,本质上还是一个线程模式,没有发挥出协程的优势,协程之间的切换等同于线程之间的切换,开销较大。

线程:协程 = 1:N

对于线程:协程 = 1:N模式,当某个协程因为缺少数据资源而进行阻塞时,此时其他的协程会等待该协程,直到时间片结束或者得到了缺少的资源而结束,这就耗费了一定的时间与系统资源,因此需要对模型进一步改进。

线程:协程 = N:N

调度器

早期调度器

  • G:goroutine 协程
  • M:线程

早期调度器的几个缺点

  1. 创建、销毁、调度GO协程(G)都需要每个线程(M)获取锁,这就形成了激烈的锁竞争
  2. 线程(M) 转移GO协程(G)时会造成延迟和额外的系统负载 。(局部性问题:在执行一个GO协程时,该GO协程可能会生成新的协程,从而占用其他的线程)
  3. 系统调用(CPU 在线程(M)之间切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销

GMP

工作模式

Go 调度器的设计策略:

  • 线程复用:避免频繁的创建、销毁线程,而是对线程进行复用。
  • 并行:利用多核CPU并行执行多个goroutine。
  • 抢占:Go 1.14引入了抢占式调度,如果一个goroutine执行时间过长,调度器会强制中断它的执行,将控制权交给其他goroutine。
  • 全局goroutine队列:除了每个P维护的本地队列,还有一个全局队列,用于存储所有待执行的goroutine。

创建协程

在 Go 语言中,使用关键字go来创建一个协程实例,每一个协程实例都是一个函数匿名函数

  1. 创建一个普通函数协程 的语法,go 函数名(参数),如:

    go 复制代码
    func newTask() {
    	i := 0
    	for {
    		i ++
    		fmt.Printf("new Goroutine :i = %d\n", i)
    		time.Sleep(1 * time.Second)
    	}
    }
    
    func main() {
    	go newTask()	// 使用关键字 go
    }
  2. 创建匿名函数协程,如:

    go 复制代码
    go func(a int, b int) bool {
        fmt.Println("a =", a, "b =", b)
        return true
    }(10, 20)	// 这里在匿名函数后直接使用一个()表示调用该匿名函数,可以根据具体的匿名函数传参。

注意

  • 使用函数或匿名函数协程,如果函数有返回值,并不能直接得到返回值,如:ans := go func(),这是不正确 的,函数的返回值不能直接通过赋值得到,需要使用下一节所讲的通道channel得到函数的返回值。
  • 如果主函数结束,那么响应的子协程也会跟着结束,即使子协程还没有执行完毕,通常会设置一个标记,来表示子协程执行完毕,父协程(主函数)才能结束。

实例:创建函数协程

go 复制代码
package main

import (
	"fmt"
	"time"
)

func newTask() {
	i := 0
	for {
		i ++
		fmt.Printf("new Goroutine :i = %d\n", i)
		time.Sleep(1 * time.Second)
	}
}

func main() {
	// 创建一个go协程,执行newTask()流程
	go newTask()	// 使用关键字 go



	fmt.Println("main Groutine exit.")

	// i := 0
// 	for {
// 		i ++
// 		fmt.Printf("main Goroutine :i = %d\n", i)
// 		time.Sleep(1 * time.Second)
// 	}
}

实例:创建匿名函数协程

go 复制代码
package main

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

func main() {
	// 使用go创建一个匿名函数协程
	// go func() {
	// 	defer fmt.Println("A.defer")

	// 	// return 
	// 	func() {
	// 		defer fmt.Println("B.defer")
	// 		// return
	// 		// 退出当前goroutine
	// 		runtime.Goexit()
	// 		fmt.Println("B")
	// 	}()		// 使用小括号来调用匿名函数

	// 	fmt.Println("A")
	// }()


	// 有参匿名函数
	// 注意:无法直接得到返回值,不能写 ans :=go func(a int, b int) bool{} 来得到返回值
	go func(a int, b int) bool {
		fmt.Println("a =", a, "b =", b)
		return true
	}(10, 20)

	// 死循环
	for {
		time.Sleep(1 * time.Second)
	}
}

Channel

Channel是Go语言中的一个核心类型,用于在不同的goroutine之间同步和传递数据。goroutine是Go语言中的轻量级线程,由Go运行时管理。使用channel可以安全地在goroutine之间进行通信,避免共享内存时出现的竞态条件。

基本使用方法:

  • 创建/定义一个channel:使用make进行创建channel变量。

    go 复制代码
    c := make(chan, int)	// make给变量c
  • 数据传递 :使用符号<-尽心数据的传递。

    go 复制代码
    c <- 666	// 将数据 666 传递到通道 c 中
    
    num := <- c	// 从c中接受数据,并赋值给num
    
    <-c			// 也可以不写num,直接扔掉 
  • 创建有缓冲的channel :使用make进行创建。

    go 复制代码
    c := make(chan int, 3)		// 创建一个channel,并指定缓冲为3。
  • 关闭channel :使用close(通道名)进行关闭,注意,关闭channel之后就不能在对channel进行读写操作了。

    go 复制代码
    close(c)
    
    // 判断channel是否关闭
    if data, ok := <- c; ok {
        fmt.Println("channel 没有关闭")
    } else {
        fmt.Pringln("channel 已经关闭")
        break
    }
  • 使用range读取通道数据 :关键字range

    go 复制代码
    for data := range c {		// 可以不断的从channel中读取数据,只有c中有数据,或不断地存入数据
        fmt.Println(data)
    }
  • select 语句select 是一个用于处理多个channel操作的控制结构。它类似于 switch 语句,但是每个 case 都是针对channel的发送或接收操作。select 语句会阻塞,直到其中一个通信操作可以进行。

    go 复制代码
    select {
    case <-channel1:
        // 当channel1可以接收数据时执行的代码
    case channel2 <- x:
        // 当可以向channel2发送数据时执行的代码
    case <-channel3, ok := channel4:
        // 当channel4可以接收数据时执行的代码,同时接收数据和检查channel是否关闭
    default:
        // 如果以上所有case都不满足时执行的代码
    }

有缓冲与无缓冲的区别

  • 无缓冲channel

    • 无缓冲channel在发送和接收数据时是同步的,也就是说,发送操作必须等待对应的接收操作才能完成,反之亦然。

    • 它在创建时不会分配任何缓冲空间,因此每次发送操作都必须有对应的接收操作,否则发送方goroutine将会被阻塞。

    • 无缓冲channel通常用于需要确保两个goroutine之间严格同步的场景,它们也被称为同步channel。

    • 无缓冲channel的创建语法是 ch := make(chan Type)ch := make(chan Type, 0)

  • 有缓冲channel

    • 有缓冲channel允许在发送数据时,如果没有接收者,数据可以被存储在channel的缓冲区中,直到缓冲区满。

    • 接收操作也是类似的,如果缓冲区中没有数据,接收者goroutine将会被阻塞,直到缓冲区中有数据可读。

    • 有缓冲channel可以提高数据传输的效率,因为它们允许发送者和接收者以不同的速率工作,而不必每次都进行同步。

    • 有缓冲channel的创建语法是 ch := make(chan Type, capacity),其中capacity是缓冲区的大小。

实例:定义channel

go 复制代码
package main

import (
	"fmt"
)


// 阻塞机制
func main() {
	// 定义一个channel
	c := make(chan int)

	go func() {
		defer fmt.Println("goroutine 结束")

		fmt.Println("goroutine 正在运行...")

		c <- 666	// 将666 发送到c通道
	}()

	// 从c中接受,并赋值给num,也可以不写num,直接扔掉, <-c
	num := <- c
	fmt.Println("num =", num)
	fmt.Println("main goroutine 结束...")
}

实例:channel 传值

go 复制代码
package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建有缓冲的channel
	c := make(chan int, 3)

	fmt.Println("len(c) =", len(c), ", cap(c) =", cap(c))

	go func() {
		defer fmt.Println("子go协程结束")

		for i:= 0; i < 4;i ++ {
			c <- i
			fmt.Println("子go协程正在运行: 发送的元素:", i, " len(c)", len(c), ", cap(c) =", cap(c))
		}
	}()

	// 休眠2秒,待数据存满缓冲之后在进行接下来的读取操作。
	time.Sleep(2 * time.Second)

	for i := 0; i < 4;i ++ {
		num := <- c		// 从c中接受数据,并赋值给num
		fmt.Println("num =", num)
	}
	// time.Sleep(1 * time.Second)
	fmt.Println("main 结束")
}

实例:关闭 channel

go 复制代码
package main

import (
	"fmt"
)

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

	go func() {
		for i := 0; i < 5; i ++ {
			c <- i
		}

		// close可以关闭一个channel
		close(c)		// 如果没有close就会出现死锁状态
	}()

	for {
		// ok 如果为true表示channel没有关闭,如果为false表示channel已经关闭
		if data, ok := <- c; ok {
			fmt.Println(data)
		} else {
			break
		}
	}

	fmt.Println("Main Finished ...")
}

实例:range 遍历通道

go 复制代码
package main

import (
	"fmt"
)

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

	go func() {
		for i := 0; i < 10; i ++ {
			c <- i
		}

		// close可以关闭一个channel
		close(c)		// 如果没有close就会出现死锁状态
	}()

	// 使用range进行读取数据,可以迭代不断从c通道中读取数据
	for data := range c {
		fmt.Println(data)
	}

	fmt.Println("Main Finished ...")
}

实例:select 语句

go 复制代码
package main

import (
	"fmt"
)

func fibnacii(c, quit chan int) {
	x, y := 1, 1

	for {
		select {
		case c <- x:
			// 如果c可写,则该case就会进来
			x = y
			y = x + y
		case <- quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)

	go func() {
		for i := 0; i < 10; i ++ {
			fmt.Println(<- c)
		}

		quit <- 0
	}()

	// main go
	fibnacii(c, quit)	// 通道传参是引用传参
}

GoPath 与 GoModules

GoPath 模式

在Go 1.11版本之前,安装 GO 一定要在环境变量中配置 GoPath,我们可以简单的将其理解成是工作目录。目录结构如下:

  • bin:存放编译后生成的二进制可执行文件。
  • pkg:存放编译后生成的 .a 文件。
  • src:存放项目的源代码,可以是你自己写的代码,也可以是你 go get 下载的包。
GoPath 模式的缺点
  1. GOPATH模式下,go get命令总是获取依赖的最新版本,没有版本号的概念,无法指定特定版本的依赖包,这在多人协作开发时可能会导致不同开发者使用的依赖版本不一致,从而引发错误
  2. 在GOPATH模式下,所有项目代码和第三方依赖包都放在GOPATH/src目录下,这会导致项目结构混乱,难以管理,特别是当项目数量增多时,不同项目的依赖包会相互交织,难以区分。
  3. 不同的项目可能需要不同版本的同一依赖,但在GOPATH模式下,无法做到这一点。每个项目都需要下载所有依赖的完整副本,这不仅导致磁盘空间的浪费,也增加了管理的复杂性。
  4. 在团队协作中,需要确保所有开发成员的GOPATH/src目录下的依赖包保持一致,这在实际操作中非常困难,容易出错且不易排查原因。
  5. 当需要升级某个依赖包时,会导致所有使用该依赖的项目都升级到新版本,这可能带来兼容性问题,因为你无法预知新版本在其他项目中的表现。
  6. 在GOPATH模式下,项目代码必须放在GOPATH/src目录下,这限制了项目的存放位置,不灵活。
  7. 在Go 1.11之前,由于GOPATH的局限性,社区出现了多种依赖管理工具,如godepgovendor等,这些工具的使用增加了学习成本,且没有统一的标准。

GoModules 模式

Go Modules 在 1.11 版本正式推出,在发布的 v1.14 版本中,官方正式发话,称其已经足够成熟,可以应用于生产上。

在 go env 中多了一个环境变量,GO111MODULE,这里的 111 表示的是 1.11 版本的象征,该环境变量用于开启或关闭 go mod 模式,可以在终端中输入 go env 来查看 Go 语言所有的环境变量:

GO111MODULE

该环境变量是开启GO MODULES的开关,其中 GO111MODULE有三个可选值:

  1. off:禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。
  2. on:启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖。
  3. auto:当项目在$GOPATH/src外且项目根目录有go.mod文件时,自动开启模块支持。

开启GO Modules模式的命令

shell 复制代码
go env -w GO111MODULE="on"
GOPROXY

该环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。GOPROXY的默认值是:'https://proxy.golang.org,direct',其中网络地址proxy.golang.org:国内访问不了,需要设置国内的代理。

设置国内代理

shell 复制代码
go env -w GOPROXY=https://goproxy.cn,direct

这条命令会将 Go 代理设置为国内的代理地址 https://goproxy.cn,并且忽略任何可能存在的代理缓存。

GOSUMDB

它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go moduleproxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。

GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是GOSUMDB可以被 Go模块代理所代理(详见:ProxyingaChecksumDatabase),因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY后,你可以不需要过度关心。另外若对GOSUMDB的值有自定义需求,其支持如下格式:

  • 格式1:<SUMDB_NAME>+<PUBLIC KEY>
  • 格式2:<SUMDB NAME>+<PUBLIC KEY> <SUMDB URL>

也可以将其设置为"off",也就是禁止Go在后续操作中校验模块版本。

GONOPROXY/GONOSUMDB/GOPRIVATE

这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。

更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database都无法访问到的模块时的场景。而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE。

它们的值都是以一个逗号,分割模块路径前缀,也就是说可以设置多个值,例如:

shell 复制代码
go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"

如此设置之后,前缀为 git.xxx.comgithub.com/eddycjy/mquote 的模块都会被认为是私有模块如果不想每次都重新设置。我们也可以利用通配符,例如:

shell 复制代码
go env -W GOPRIVATE="*.example.com"

这样子设置的话,所有模块路径为example.com的子域名(例如:git.example.com)都将不经过 Gomodule proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。

GoModule的优势

go mod 不再依靠 $GOPATH,使得它可以脱离 GOPATH 来创建项目,你可以在你电脑的任意位置创建一个文件夹。

GoModules 常用命令

  • go mod init 项目名称:生成 go.mod 文件,这里的项目名称是自定义的。
  • go mod tidy:添加缺少的包,且删除无用的包。
  • go mod download:下载 go.mod 文件中指明的所有依赖go mod tidy 整理现有的依赖。
  • go mod graph:查看现有的依赖结构。
  • go mod edit:编辑 go.mod 文件。
  • go mod vendor:导出项目所有的依赖到vendor目录go mod verify 校验一个模块是否被篡改过go mod why 查看为什么需要依赖某模块。
  • go mod why: 查看为什么需要依赖。

初始化GOMODULE 项目

  1. 创建一个文件夹,并进入该目录中。
  2. 执行go mod init 项目名称,初始化项目,此时会生成一个 go.mod 文件.
  3. 下载第三方库:go get 第三方库地址
  4. 当执行包含第三方库的程序之后,会生成 go.sum 文件。
go.mod 与 go.sum
go.mod 文件

功能

  • 定义模块:go.mod 文件定义了项目的模块名称,即模块的路径(通常是代码仓库的路径)。

  • 记录依赖:go.mod 文件列出了当前项目所依赖的所有模块及其版本号。它确保了项目在不同环境下能够使用相同的依赖版本进行构建。

  • 管理版本:在 go.mod 文件中,可以通过指定版本号来管理依赖的版本,确保项目的可重复构建。

go.sum 文件

功能

  • 校验和信息go.sum 文件记录了所有依赖模块及其版本的校验和信息。这个文件用于确保依赖模块在不同环境下的一致性和完整性,防止模块内容被篡改或不一致。
  • 保护项目完整性 :在构建项目时,Go工具链会根据 go.sum 文件中的校验和信息来验证下载的依赖模块是否完整和正确。
  • 模块版本校验go.sum 中的每一行记录了模块及其版本的校验和(h1: 开头的部分)。
  • go.mod 文件校验go.sum 还记录了依赖模块自身的 go.mod 文件的校验和,确保依赖模块的元数据也是一致的。
区别:

功能不同

  • go.mod:主要用于定义模块和记录项目的依赖关系,指定项目使用的依赖版本。

  • go.sum:主要用于记录依赖模块的校验和信息,确保模块的内容在不同环境下的一致性。

内容不同

  • go.mod:内容相对简洁,主要是模块路径和版本号。

  • go.sum:内容更详细,包含每个依赖模块和其 go.mod 文件的校验和。

致谢:

相关推荐
Linux520小飞鱼2 分钟前
F#语言的网络编程
开发语言·后端·golang
weixin_399264297 分钟前
QT c++ 样式 设置 标签(QLabel)的渐变色美化
开发语言·c++·qt
吾当每日三饮五升3 小时前
C++单例模式跨DLL调用问题梳理
开发语言·c++·单例模式
猫武士水星4 小时前
C++ scanf
开发语言·c++
BinaryBardC4 小时前
Bash语言的数据类型
开发语言·后端·golang
Lang_xi_4 小时前
Bash Shell的操作环境
linux·开发语言·bash
Pandaconda4 小时前
【Golang 面试题】每日 3 题(二十一)
开发语言·笔记·后端·面试·职场和发展·golang·go
捕鲸叉5 小时前
QT自定义工具条渐变背景颜色一例
开发语言·前端·c++·qt
想要入门的程序猿5 小时前
Qt菜单栏、工具栏、状态栏(右键)
开发语言·数据库·qt
_院长大人_5 小时前
使用 Spring Boot 实现钉钉消息发送消息
spring boot·后端·钉钉