go/golang 入门学习笔记(Java/Python/C++转Go快速上手)

文章目录


注意:go 语言命名规范中首字母大小写问题

原则:默认小写(封装),需要公开时再改为大写。这不是"都要首字母大写",而是 "按需大写" (但包名必须小写)。

一、go 语言特性

go 即 golang,是由 Google 在 2009 年发布的一种静态强类型、编译型、并发型编程语言。它结合了 C 语言的高性能和 Python/JavaScript 的开发效率,特别适合云计算、微服务、分布式系统等领域。被誉为 "21世纪的C语言"

特点:

  1. 简洁高效:编写起来 python 语言一样简单(编译/运行时间和 c/c++ 差不多)
  2. 并发支持(Goroutine):可以非常方便的执行并发任务
  3. 跨平台支持:可以跨平台编译(0/1机器码),比如用 win 电脑编译出 linux 系统可执行文件,且目标系统不用安装 go 环境
  4. 标准库强大:内置 http 库,可以非常方便的开发 web 服务(还有加密、json 等等这些好用的库)
  5. 静态类型:编辑器编辑时,静态语言开发体验方便,方便后期维护(编译时可以检测出大多数错误)

go 语言项目:

  • docker
  • k8s
  • prometheus(监控告警)
  • cockroachDB(分布式关系型数据库系统)
  • tiDB

当前在用go的公司

  • Google
  • 字节跳动
  • 腾讯
  • 哔哩哔哩
  • 京东
  • 小米
  • 小红书
  • 百度
  • ...

二、go 环境安装

下载:

下载完配置环境变量:

bash 复制代码
vim ~/.zshrc

在文件最后面加入

bash 复制代码
export GOROOT=$(go env GOROOT)  # 设置安装路径
export GOPATH=$HOME/go          # 设置工作区路径
export PATH=$GOPATH/bin:$PATH   # 将工作区 bin 目录加入路径

刷新环境变量

bash 复制代码
source ~/.zshrc

如果是 mac系统,可以直接使用 brew 一键安装:

bash 复制代码
brew install go

优化网络环境(推荐国内用户):设置 Go 模块代理以加速依赖下载:

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

验证安装:

go version

Ide 推荐:

  • goland(需要激活)
  • Vscode(安装插件)

三、go 变量定义

go 对于变量的生命有多种方式:

  • 先声明,再赋值
    *

    // 先声明

    var name string

    // 再赋值

    name = "zhangsan"

  • 声明并赋值
    *

    var name1 string = "zhangsan"

  • 声明并赋值(省略类型)
    *

    var name2 = "zhangsan"

  • 声明并赋值 短声明符号
    *

    name3 := "zhangsan"

  • 同时声明赋值多个值
    *

    var a1, a2 = 1, 2

并且注意全局变量和局部变量声明时,会有一些小的区别

  • 全局变量需要用 var 声明
    *

    var age = 13

  • 全局同时声明赋值多个变量,要用 var + 括号包起来
    *

    var (

    s1 string = "1"

    s2 string = "2"

    )

  • 对于常量声明,使用 const 关键字(赋值后不能改变)
    *

    const Version1 = "1.0.0"

注意:对于包外需要调用的变量(一般是全局变量),声明时变量名必须首字母大写,不然编译会失败

go 复制代码
package main // 如果要能 run,则必须声明 main 包

import (
	"fmt"
	"studyProject/go_study/version"
)

// 声明函数
func hello() {
	fmt.Println("Hello World")
}

// 全局变量需要用 var 声明
var age = 13

// 全局同时声明赋值多个变量
var (
	s1 string = "1"
	s2 string = "2"
)

// Version1 其他包调用需要首字母大写
const Version1 = "1.0.0"

func main() {
	// 先声明
	var name string
	// 再赋值
	name = "zhangsan"
	fmt.Println(name)

	// 声明并赋值
	var name1 string = "zhangsan"
	fmt.Println(name1)

	// 省略类型
	var name2 = "zhangsan"
	fmt.Println(name2)

	// 声明并赋值 短声明符号
	name3 := "zhangsan"
	fmt.Println(name3)

	// 调用函数
	hello()

	// 数字类型
	var a = 1
	fmt.Println(a)

	// 同时声明赋值多个值
	var a1, a2 = 1, 2
	fmt.Println(a1, a2)

	// 调用外包的变量
	fmt.Println(version.Version)
}

如果需要运行当前文件,则必须声明为 main 包

四、输入输出

输出:

  • fmt.Println() // 输出带换行
  • fmt.Print() // 输出不带换行
  • fmt.Printf() // 格式化输出
  • fmt.Sprintf() // 将格式化后的内容赋值给一个变量

输入:

  • fmt.Scan(&name) // 读取键盘输入
  • // n 代表成功读入的个数,err 代表异常

    n, err := fmt.Scan(&age, &age2)

注意:#%v,打印字符串的时候用的比较多,可以看出来是否是空字符串/没有打印

%v 可以输出任意类型

go 复制代码
package main

import "fmt"

func main() {
	fmt.Println("hello", "你好")
	fmt.Println("你好", "张三")
	fmt.Printf("你好,%s!\n", "李四")
	fmt.Printf("%d\n", 3)
	fmt.Printf("%f\n", 3.1415)
	fmt.Printf("%.2f\n", 3.1415)
	fmt.Printf("%T %T\n", "你好", 1)
	// %v 任意类型都可以显示
	fmt.Printf("%v\n", "你好")
	fmt.Printf("%v\n", 123)
	// 打印字符串的时候用的比较多,可以看出来是否是空字符串/没有打印
	fmt.Printf("%#v\n", "")
	// 将格式化后的内容赋值给一个变量
	var f = fmt.Sprintf("%.2f\n", 3.1415)
	fmt.Println(f)

	// 输入
	fmt.Print("请输入你的名字:")
	var name string
	fmt.Scan(&name)
	fmt.Println(name)

	var age, age2 int
	fmt.Print("请输入你的年龄:")
	// n 代表成功读入的个数,err 代表异常
	n, err := fmt.Scan(&age, &age2)
	fmt.Println(n, err, age, age2)
}

五、基本数据类型

go 语言的基本数据类型有

  1. 整数
  2. 浮点
  3. 复数
  4. 布尔
  5. 字符串

整数

  • 默认整数类型是 int 类型
  • 带个 u 代表无符号类型,只能存非负整数
  • 后面的数字就是2进制的位数(uint64,代表64位/8字节, 2 64 2^{64} 264)
  • uint8 还有一个别名 byte,一个字节=8个bit位
  • int 类型的大小取决于所使用的平台(64位系统的int最大值是 2 63 − 1 2^{63} - 1 263−1)
go 复制代码
var age = 123
var x1 uint8 = 255
var x2 uint16 = 2
var x3 uint32 = 2
var x4 uint64 = 2
var x5 uint = 2
var x6 int8 = 2
var x7 int16 = 2
var x8 int32 = 2
var x9 int64 = 2
var x10 int = 2

浮点型

go 语言支持两种浮点类型:float32 和 float64

  1. float32 的浮点型最大范围为 3.4e38,常量使用:math.MaxFloat32
  2. float64 的浮点数最大范围为 1.8e308,常量使用:math.MaxFloat64

如果没有显示声明,默认是 float64

字符型

比较重要的两个类型是 byte(单字节字符)和 rune(多字节字符)

go 复制代码
// byte = uint8
	var a byte = 'a'
	fmt.Printf("%c %d\n", a, a)
	var a1 uint8 = 'a'
	fmt.Printf("%c %d\n", a1, a1)
	var a2 uint8 = 97
	fmt.Printf("%c %d\n", a2, a2)

	// rune=int32 多字节字符(中文、韩文等)
	var z rune = '中'
	fmt.Printf("%c %d\n", z, z)
  1. go 中,字符的本质是一个整数,直接输出时,是该字符对应的 UTF-8 编码的码值
  2. 可以直接给某个变量赋值一个数字,然后按格式输出时 %c,会输出该数字对应的 unicode 的字符
  3. 字符类型是可以进行运算的,相当于一个整数,因为它都对应有 unicode 码

字符串类型

和字符不一样的是,字符的赋值是单引号,字符串的赋值是双引号

go 复制代码
var s string = "张三"
fmt.PrintLn(s)

转义字符

go 复制代码
fmt.Println("张三\t你好")
fmt.Println("'张三'你好")
fmt.Println("\"张三\"你好")
fmt.Println("\\张三\\你好")
fmt.Println("张三\n你好")

多行字符

go 复制代码
fmt.Println(`hello
	World \n
`)

布尔类型

布尔类型只有 true 和 false 两个值

  1. 布尔类型默认是 false
  2. go 不允许将整型强转为布尔型
  3. 布尔类型无法参与数值运算,也无法与其他类型进行转换

零值问题

如果一个基本数据类型只声明不赋值

那么这个变量的值就是对应类型的零值,例如 int 就是 0,bool 就是 false,字符串就是 ""

go 复制代码
var age int
var b bool
var s string
fmt.Printf("%#v %#v %#v\n", age, b, s)

六、数组、切片、map

数组:

数组(Array)是一种非常常见的数据类型,几乎所有的计算机编程语言中都会用到

  1. 数组中的元素必须为同一类型,要么全部字符串,要么全部是整数...
  2. 声明数组时,必须指定其长度或者大小

索引

go 复制代码
var nameList [3]string = [3]string{"zhangsan", "lisi", "wangwu"}
fmt.Println(nameList)
// zhangsan lisi wangwu
// 0        1     2
fmt.Println(len(nameList))
fmt.Println(nameList[0])
fmt.Println(nameList[1])
fmt.Println(nameList[len(nameList)-1])

nameList[0] = "ww"
fmt.Println(nameList[0])

切片/列表

go 中的数组定义后是固定大小

slice(切片) 相对更灵活,声明切片后长度是可变的

go 复制代码
// 切片
var names []string
names = append(names, "zhangsan", "lisi", "wangwu")
names = append(names, "bc")
fmt.Println(names[0])

// 定义空切片
var arrs []string
fmt.Println(arrs == nil)

// 定义长度为0 的切片
var arrs1 []string = []string{}
var arrs2 = []string{}
arrs3 := []string{}
arrs4 := make([]string, 0)
fmt.Println(arrs1, arrs2, arrs3, arrs4)

// 创建指定长度的切片
ageList := make([]int, 3)
fmt.Println(ageList)

// 进行切片
array := [3]int{1, 2, 3}
fmt.Println(array)
sliceArray1 := array[:]
fmt.Println(sliceArray1)

// 左闭右开
fmt.Println(sliceArray1[0:2])

切片排序

go 复制代码
// 切片排序
var sourceArray = []int{3, 1, 8, 10, 2}
sort.Ints(sourceArray) // 默认升序
fmt.Println(sourceArray)

// 降序
// sort.IntSlice() 将 []int 类型转换为 IntSlice 接口类型
// sort.Reverse() 重写 IntSlice 类型的 Less 比较方法
// sort.Sort() 实际的排序方法,对相应类型按 Less 比较器进行排序
sort.Sort(sort.Reverse(sort.IntSlice(sourceArray)))
fmt.Println(sourceArray)

map

go 语言中的 map(映射、字典)是一种内置的数据结构,它是一个无序的 key-value 对的集合

map 的 key 必须是基本数据类型,value 可以是任意类型

注意:map 使用之前一定要初始化

go 复制代码
var userMap map[int]string = map[int]string{
		1: "zhangsan",
		2: "lisi",
		4: "",
	}
	fmt.Println(userMap)
	fmt.Println(userMap[1])
	fmt.Printf("%#v", userMap[3])
	fmt.Printf("%#v\n", userMap[4])

	v := userMap[4]
	fmt.Println(v)
	// v 是 value值,ok 是代表在字典里有没有(:=左边多个值时,如果都声明过,就会报错)
	//var ok bool(不能声明,不然 v 和 ok 都声明,:= 左边报错,可以使用 = 代替)
	//var v string
	v, ok := userMap[3]
	fmt.Println(v, ok)

	// 修改 map 值
	userMap[1] = "休学"
	fmt.Println(userMap)
	// 删除 map 中对应 key 的元素
	delete(userMap, 4)
	fmt.Println(userMap)

	// map 创建的时候一定要初始化
	// 错误写法(未初始化,编译报错)
	var aMap map[string]string
	aMap["1"] = "1"

	// 正确写法
	var aMap1 map[string]string = map[string]string{}
	var aMap2 map[string]string = make(map[string]string)
	aMap1["2"] = "2"
	aMap2["2"] = "2"
	fmt.Println(aMap1, aMap2)

map 取值

  • 如果只有一个参数接,那这个参数就是值,如果没有,这个值就是对应类型的零值
  • 如果两个参数接,那第二个参数就是布尔值,表示是否存在这个 key

map 在并发下会有问题

七、判断语句

if 语句

中断式 卫语句(推荐!)

go 复制代码
var age int
	fmt.Printf("请输入你的年龄:")
	fmt.Scan(&age)

	if age <= 0 {
		fmt.Println("未出生")
		return
	}
	if age <= 18 {
		fmt.Println("未成年")
		return
	}
	if age <= 35 {
		fmt.Println("青年")
		return
	}
	fmt.Println("中年")

嵌套式

go 复制代码
if age <= 18 {
	if age <= 0 {
		fmt.Println("未出生")
	} else {
		fmt.Println("未成年")
	}
} else {
	if age <= 35 {
		fmt.Println("青年")
	} else {
		fmt.Println("中年")
	}
}

多条件

go 复制代码
if age <= 0 {
	fmt.Println("未出生")
}
if age <= 18 && age > 0 {
	fmt.Println("未成年")
}
if age > 18 && age <= 35 {
	fmt.Println("青年")
}
if age > 35 {
	fmt.Println("中年")
}
// &&(逻辑短路)
// ||
// !

多行拷贝对应位置进行粘贴快捷键:按ALT键不放选中四条复制,回到当前页面按住ALT再选四行空位置粘贴

switch 语句

go 复制代码
var age int
fmt.Print("请输入你的年龄:")
fmt.Scan(&age)

switch {
case age <= 0:
	fmt.Println("未出生")
case age <= 18:
	fmt.Println("未成年")
case age <= 35:
	fmt.Println("青年")
default:
	fmt.Println("中年")
}

go 的 switch 不用 break

如何让其继续往下走

go 复制代码
fmt.Println("未成年")
fallthrough
go 复制代码
var week int
fmt.Print("请输入星期:")
fmt.Scan(&week)

switch week {
case 1:
	fmt.Print("周一")
case 2:
	fmt.Print("周二")
case 3:
	fmt.Print("周三")
case 4:
	fmt.Print("周四")
case 5:
	fmt.Print("周五")
case 6, 7:
	fmt.Print("周末")
default:
	fmt.Print("输错了")
}

八、for 循环

go 复制代码
//var sum int = 0
sum:= 0
for i := 1; i <= 100; i += 2 {
	sum += i
}
fmt.Println(sum)

死循环

go 复制代码
// 死循环
for true{
	....
}
for {
	....
}

while 模式

go 没有 while 循环,通过 for 来替代

go 复制代码
var sum int
var i int = 1
for i <= 100 {
	sum += i
	i++
}
fmt.Println(sum)

do while 模式

go 复制代码
var sum int
i:= 1
for {
	sum += i
	i++
	if i > 100 {
		break
	}
}

遍历切片, map

遍历切片

第一个参数是索引,第二个参数是元素

go 复制代码
var list = []string{"张三", "李四"}
for i := 0; i < len(list); i++ {
	fmt.Println(i, list[i])
}
for index, item := range list {
	fmt.Println(index, item, ", ")
}

遍历 map

第一个参数是 key,第二个参数是 value

go 复制代码
var userMap = map[string]string{"name": "zhangsan"}
for key, value := range userMap {
	fmt.Println(key, value)
}

break 和 continue

go 复制代码
for i := 0; i < 10; i++ {
if i%2 == 0 {
		continue
	}
	fmt.Printf("%d ", i)
}

九、函数(指针)

函数是一段封装了特定功能的可重用代码块,用于执行特定的任务或计算

函数接受输入(参数)并产生输出(返回值)

函数定义

go 复制代码
package main

import "fmt"

func sayHello() {
	fmt.Println("hello")
}

func main() {
	sayHello()
}

函数参数

go 复制代码
func param1(id string) {
	fmt.Println(id)
}

func param2(id string, name string) {
	fmt.Print(id)
}

func add(numberList ...int) {
	sum := 0
	for _, number := range numberList {
		sum += number
	}
	fmt.Println("sum:", sum)
}

// 多个参数的类型相同时,前面的类型可以省略
func param3(id, name string) {
	fmt.Print(id)
}

func main() {
	param1("123")
	add(1, 3, 5)
	add(1, 3, 2, 9)
}

函数返回值

go 复制代码
// 无返回值
func r1() {
	return // 也可以不写
}

// 带返回值
func r2() int {
	return 1
}

// 返回多个值
func r3() (int, string) {
	return 1, "123"
}

// 命名返回值
func r4() (age int, name string) {
	age = 19
	name = "zhangsan"
	return // 相当于 return age, name
}

func main() {
	age, name := r4()
	fmt.Println(name, age)
}

匿名函数

go 复制代码
// 匿名函数(变量是函数类型)
getName := func() string {
	return "zhangsan"
}
fmt.Println(getName())

setName := func(name string) {
	fmt.Println(name)
}
setName("lisi")

高阶函数

将函数作为参数,传到另一个函数里面

go 复制代码
fmt.Println("请输入你要执行的操作")
fmt.Println("1 登录")
fmt.Println("2 注册")
fmt.Println("3 个人中心")
var index int
fmt.Scan(&index)

switch index {
case 1:
	login()
case 2:
	register()
case 3:
	userCenter()
}

提取到 map 中,key 是数字,value 是 func 类型

go 复制代码
// map
var funMap = map[int]func(){
	1: login,
	2: register,
	3: userCenter,
}
// value, ok
fun, ok := funMap[index]
// 如果当前key存在,执行对应方法
if ok {
	fun()
}

闭包

返回值是函数(嵌套),内部函数用到了外部函数的变量,叫闭包

(类比py: 函数嵌套 内部函数引用了外部函数的参数或变量 并将内部函数返回 就叫做闭包)

  • 设计一个函数,先传一个参数表示延时,后面再次传参数就是将参数求和
go 复制代码
package main

import (
	"fmt"
	"time"
)

// 闭包:返回值是函数(内部函数用到外部函数的变量)
func awaitAdd(awaitTime int) func(...int) int {
	return func(numberList ...int) (sum int) {
	time.Sleep(time.Duration(awaitTime) * time.Second)
		for _, number := range numberList {
			sum += number
		}
		return sum
	}
}

func main() {
	
	t1 := time.Now()
	// 延迟2s
	sum := awaitAdd(2)(1, 2, 3, 4, 5)
	fmt.Println("sum =", sum)
	subTime := time.Since(t1)
	fmt.Println(subTime)

	fmt.Println("--------")
	
	// 此处延时2s
	addFunc := awaitAdd(2)
	t2 := time.Now()
	sum2 := addFunc(1, 2, 3, 4, 5)
	fmt.Println("sum2 =", sum2)
	subTime2 := time.Since(t2)
	fmt.Println(subTime2)
}

值传递和引用传递

  • Go 所有参数都是 值传递 ,但值的内容 可以是 引用(如 slice 描述符、指针、map 引用等)。
  • 修改能否影响外部,取决于你通过 值传递副本 是否能够 访问到原始数据
  • 结构体默认是 值传递 ,想要修改外部结构体,需传递 指针 或返回新值。
  • 切片不是"引用类型"而是"值类型(描述符)+ 引用底层数组",这常造成误解。

正常情况下来说,函数的参数是将之前的内存存储的东西复制了一份出来

go 复制代码
package main

import "fmt"

func copyValue(name string) {
	fmt.Printf("copyValue = %p \n", &name)
}

func main() {

	name := "zhangsan"
	fmt.Printf("sourceValue = %p \n", &name)
	copyValue(name)

}

上面的方式传的是值(值传递),对应地址是不一样的

go 复制代码
func setName(name *string) {
	fmt.Printf("copyValue = %p \n", name)
}

func main() {

	name := "zhangsan"
	fmt.Printf("sourceValue = %p \n", &name)
	setName(&name)

}

当前这种通过 & 和 * 传递的方式,传递的是指针(引用),所以对应指向的是同一个内存区域(地址一样)。

可以通过 *变量 的方式修改对应的值

go 复制代码
func setName(name *string) {
	fmt.Printf("copyValue = %p \n", name)
	// 通过解引用修改对应的值
	*name = "李四"
}

func main() {
	name := "zhangsan"
	fmt.Printf("sourceValue = %p \n", &name)
	setName(&name)
	// 打印发现修改成功
	fmt.Println(name)
}

指针

上面这个案例搞清楚后,指针也就不难了

我们只需知道

&是取地址,* 是解引用,取这个地址指向的值

十、init 函数和 defer 函数

init 函数

init() 函数是一个特殊的函数,存在以下特性:

  1. 不能被其他函数调用,而是在 main 函数执行之前,自动被调用
  2. init函数不能作为参数传入
  3. 不能有传入参数和返回值

一个go文件可以有多个 init 函数,谁在前面谁就先执行(按顺序执行)

go 复制代码
package main

import "fmt"

var db int

func init() {
	// 初始化加载
	db = 10
	fmt.Println("连接数据库成功")
	fmt.Println("init1")
}
func init() {
	fmt.Println("init2")
}
func init() {
	fmt.Println("init3")
}

func main() {
	fmt.Println("main", db)
}

多个包时的加载顺序(谁先加载就先执行谁)

defer 函数

在某个函数结束前执行(所有语句之后)

  1. 关键字 defer 用于注册延迟调用
  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理
  3. 多个defer语句,按先进后出的方式执行,谁离 return 近谁先执行
  4. defer语句中的变量,在 defer 声明时就决定了
go 复制代码
package main

import "fmt"

func main() {
	
	// 类似栈的调用顺序(前面的后执行)
	var age int = 10
	defer func() {
		age = -1
		fmt.Println("清理defer1", age)
	}()
	defer fmt.Println("清理defer2")
	// 如果变量定义在defer函数后面,defer中拿不到这个变量
	//name := "zhangsan"

	return
}

十一、结构体

结构体定义

go 复制代码
package main

import "fmt"

type Student struct {
	name string
}

func (stu Student) study() {
	fmt.Printf("%s在study", stu.name)
}

func main() {
	//s1 := new(Student)
	//s1.name = "zhangsan"

	s1 := Student{name: "zhangsan"}
	fmt.Println(s1)
	s1.study()
}

继承/组合

go 复制代码
package main

import "fmt"

type Class struct {
	name string
}

type Student struct {
	Class // 继承/组合
	name  string
}

func (stu Student) study() {
	fmt.Printf("%s在study\n", stu.name)
}

func (stu Student) info() {
	fmt.Printf("学生:%s班-%s在学习\n", stu.Class.name, stu.name)
}

func main() {
	//s1 := new(Student)
	//s1.name = "zhangsan"
	c1 := Class{name: "c1"}
	s1 := Student{name: "zhangsan", Class: c1}
	fmt.Println(s1)
	s1.study()
	s1.info()
}

结构体指针

之前了解的值传递和引用传递,如果想在函数里面或者方法里面修改结构体里面的属性,只能使用结构体指针或者指针方法

go 复制代码
// 值传递修改不成功
func (stu Student) setName(name string) {
	stu.name = name
}

// 引用传递修改成功
func (stu *Student) setNameTwo(name string) {
	stu.name = name
}

func main() {
	//s1 := new(Student)
	//s1.name = "zhangsan"
	c1 := Class{name: "c1"}
	s1 := Student{name: "zhangsan", Class: c1}
	fmt.Println(s1)
	s1.study()
	s1.info()
	// 修改失败
	s1.setName("王五")
	s1.info()
	// 修改成功
	s1.setNameTwo("王五")
	s1.info()
}

结构体tag(类比Java中的字段注解)

在 Go 语言中,结构体标签(Struct Tag) 是附加在结构体字段后面的字符串元数据,用于在运行时通过反射读取,用来指导包(如 encoding/json、gorm、validate 等)如何处理该字段。

标签是反引号包裹的字符串,通常采用 key:"value" 的形式,多个键值对用空格分隔(但不能有逗号分隔,用空格即可)。

go 复制代码
type User struct {
    Name string `json:"name" db:"username" validate:"required"`
    Age  int    `json:"age,omitempty"`
}
  • json:"name":告诉 encoding/json 包,字段在 JSON 中对应键名为 "name"。
  • db:"username":供数据库映射库(如 sqlx)使用,字段对应数据库列 username。
  • validate:"required":供校验库(如 go-playground/validator)使用,字段不能为零值。

注意事项:

标签内不能有空格,除非在值中需要。键值对之间用空格分隔。

常用库标签参考:

  • json:序列化/反序列化控制。
  • xml:XML 处理。
  • yaml:YAML 处理。
  • gorm:ORM 操作(列名、约束等)。
  • form:表单解析(如 gin、echo)。
  • binding:参数绑定校验。
go 复制代码
package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	// json:"xx",其中xx代表转json后当前字段名
	Name string `json:"name"`
	// omitempty 代表当前字段是空值/默认值时,转json忽略
	Age int `json:"age,omitempty"`
	// - 代表转json时,忽略这个字段
	Password string `json:"-"`
}

func main() {
	user := User{Name: "zhangsan", Age: 10, Password: "123456"}
	jsonByte, _ := json.Marshal(user)
	fmt.Println(string(jsonByte))
}

json tag

  1. 这个不写json标签转换为json的话,字段名就是属性的名字
  2. 小写的属性也不会转换空

如果再转 json 的时候,我不希望某个字段被转出来,我可以写一个-

十二、自定义数据类型

在Go语言中,自定义类型指的是使用type关键字定义的新类型 ,它可以是基本类型的别名 ,也可以是结构体函数等组合而成的新类型。自定义类型可以帮助我们更好地抽象和封装数据,让代码更加易读、易懂、易维护。

结构体就是自定义类型中的一种

除此之外我们使用自定义类型,还可以让代码组合更加规范

例如,响应给客户端的想要码,我给他一个自定义类型

go 复制代码
package main

// 给 int 自定义类型,之后可以对自定义类型绑定方法
type Code int

const (
	SuccessCode    Code = 0
	ValidErrCode   Code = 1001
	ServiceErrCode Code = 1002
)

func (code Code) getMsg() string {
	switch code {
	case SuccessCode:
		return "成功"
	case ValidErrCode:
		return "验证失败"
	case ServiceErrCode:
		return "服务错误"
	}
	return ""
}

func (code Code) getCodeMsg() (Code, string) {
	return code, code.getMsg()
}

func webServer(name string) (code Code, msg string) {
	if name == "1" {
		return SuccessCode.getCodeMsg()
	}
	if name == "2" {
		return ServiceErrCode.getCodeMsg()
	}
	return ValidErrCode.getCodeMsg()
}

func main() {

}

类型别名

自定义类型很像,但是有一些地方和自定义类型有很大差异

  1. 不能绑定方法
  2. 打印类型还是原始类型
  3. 和原始类型比较,类型别名不用转换
go 复制代码
package main

import "fmt"

type MyCode int        // 自定义类型(可以绑定方法)
type MyAliasCode = int // 类型别名(不能绑定方法)

// 正确
func (code MyCode) info() {

}

// 错误
func (alisa MyAliasCode) info1() {

}

const myCode MyCode = 1
const myAliasCode MyAliasCode = 1

func main() {

	fmt.Printf("%v, %T\n", myCode, myCode)
	fmt.Printf("%v, %T\n", myAliasCode, myAliasCode) // 打印出来时原始类型

	var age = 1
	// 不能直接判断,age 是 int 类型
	if myCode == age {
		fmt.Printf("123")
	}
	// 除非进行类型转换
	// 1. MyCode 转 int 类型
	if int(myCode) == age {
		fmt.Printf("123")
	}
	// 2. int 转 MyCode 类型
	if myCode == MyCode(age) {
		fmt.Printf("123")
	}
	// 可以进行判断(是int别名,等同关系)
	if myAliasCode == age {
		fmt.Printf("123")
	}
}

十三、接口

接口是一组仅包含方法名、参数、返回值的未具体实现的方法的集合

注意:对于 go,如果有结构体实现了当前接口的所有方法,则视为其实现当前接口(不需要显示 implments)

go 复制代码
package main

import "fmt"

type AnimalInterface interface {
	// Sing 如果有结构体实现了当前接口的所有方法,则视为其实现当前接口
	Sing()
	GetName() string
}

type Cat struct {
	Name string
}

func (cat Cat) Sing() {
	fmt.Println("咪咪叫")
}

func (cat Cat) GetName() string {
	return cat.Name
}

type Dog struct {
	Name string
}

func (dog Dog) Sing() {
	fmt.Println("汪汪")
}

func (dog Dog) GetName() string {
	return dog.Name
}

func sing(animal AnimalInterface) {
	fmt.Print(animal.GetName())
	animal.Sing()
}

func main() {
	cat := Cat{Name: "小花猫"}
	sing(cat)
	dog := Dog{Name: "柴犬"}
	sing(dog)
}

接口本身不能绑定方法

接口是值传递,保存的是:值 + 原始类型

实现接口:

一个类型实现了接口的所有方法

即实现了该接口

类型断言

在使用接口时,如果希望知道具体的类型是什么,可以通过断言来获取

在 Go 语言中,类型断言是一种从接口值中提取其底层具体值的操作。它的语法是 x.(T),其中 x 是一个接口类型的表达式,T 是一个具体类型或另一个接口类型。

  • 接口值内部存储了一对 (具体类型, 具体值),类型断言就是用来访问这个具体值的。
  • 如果 x 是 nil 接口,类型断言会 panic。
  • 类型断言可返回一个值(不安全形式),或返回两个值(安全形式,第二个为 bool 表示成功)。
go 复制代码
func sing(animal AnimalInterface) {
	switch animalType := animal.(type) {
	case Cat:
		fmt.Println("当前对象:", animalType)
	case Dog:
		fmt.Println("当前对象:", animalType)
	default:
		fmt.Println("其他")
	}

}

注意:x.(type) 只能在 switch 语句中使用,这里的 type 是关键字。

  • 在类型开关(type switch)的语法 switch v := x.(type) 中,x.(type) 本身并不直接返回一个值或一个类型 ,它是 Go 语言的一个特殊语法结构,只能出现在类型开关的 switch 语句中,用来表示 "提取 x 的动态类型信息,并依次与每个 case 指定的类型进行比较"。

断言某个类型

go 复制代码
// 单类型断言(断言成功时,ok为true,并返回当前类型的对象)
cat, ok :=animal.(Cat)
fmt.Println(ok, cat)

value := x.(T)

如果 x 的动态类型是 T(或实现了 T,如果 T 是接口),则返回 x 中存储的 T 类型的值(也就是返回对象)。

空接口

空接口可以接受任何类型

换一种,任何类型都实现了空接口的定义

go 复制代码
type ObjInterface interface {
}

func PrintObj(obj ObjInterface) {
	fmt.Printf("%#v\n", obj)
}


func Print(obj interface{}) {
	fmt.Printf("%#v\n", obj)
}

func main() {
	cat := Cat{Name: "小花猫"}
	//sing(cat)
	dog := Dog{Name: "柴犬"}
	//sing(dog)

	Print(cat)
	Print(dog)
	Print(123)
	Print("123")

	PrintObj(cat)
	PrintObj(dog)
	PrintObj(123)
	PrintObj("123")
}

go 中为空接口起了别名为 any
type any = interface{}

十四、协程和channel

(天然支持协程)

Goroutine 是 Go 运行时管理的轻量级线程

在 go 中,开启一个协程是非常简单的

go 复制代码
package main

import (
	"fmt"
	"time"
)

func shopping(name string) {
	fmt.Printf("%s开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s购物结束\n", name)

}

// 协程
func main() {
	// 同步(需要3s+)
	//t1 := time.Now()
	//shopping("zhangsan")
	//shopping("lisi")
	//shopping("wangwu")
	//fmt.Println("总共购买时间", time.Since(t1))

	// 异步(主线程结束,协程会跟着结束,主线程不会等协程)
	t2 := time.Now()
	go shopping("zhangsan")
	go shopping("lisi")
	go shopping("wangwu")
	time.Sleep(1 * time.Second)  // 固定等1s
	fmt.Println("总共购买时间", time.Since(t2))
}

如果我把这个主线程中的延时去掉之后,你会发现程序没有任何输出就结束了

这是为什么呢

那是因为主线程结束协程自动结束,主线程不会等待协程的结束

WaitGroup

我们只需要让主线程等待协程就可以了

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

var waitDone sync.WaitGroup

func shopping(name string) {
	fmt.Printf("%s开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s购物结束\n", name)
	waitDone.Done()
}

// 协程
func main() {
	// 通过 wait 来实现协作
	waitDone.Add(3)
	t3 := time.Now()
	go shopping("zhangsan")
	go shopping("lisi")
	go shopping("wangwu")
	waitDone.Wait()
	fmt.Println("总共购买时间", time.Since(t3))
}

上面是全局变量的方式,局部变量以下这种方式:

go 复制代码
func shopping(name string, waitDone *sync.WaitGroup) {
	fmt.Printf("%s开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s购物结束\n", name)
	waitDone.Done()
}

// 协程
func main() {
	// 通过 wait 来实现协作
	var waitDone sync.WaitGroup
	waitDone.Add(3)
	t3 := time.Now()
	go shopping("zhangsan", &waitDone)
	go shopping("lisi", &waitDone)
	go shopping("wangwu", &waitDone)
	waitDone.Wait()
	fmt.Println("总共购买时间", time.Since(t3))
}

Channel

如果在协程里面产生了数据,怎么传递给主线程,或者传递给其他协程呢?

这个时候就可以用 channel(类比 Java 中的阻塞队列,线程安全容器)

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

// 初始化信道,大小为0
var waitChan = make(chan int)
//var waitChan chan int = make(chan int)

func pay(name string, money int, waitDone *sync.WaitGroup) {
	defer waitDone.Done()
	fmt.Printf("%s开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s花了%d块钱\n", name, money)
	waitChan <- money
}

func main() {

	t1 := time.Now()
	var waitDone sync.WaitGroup
	waitDone.Add(3)
	go pay("zhangsan", 2, &waitDone)
	go pay("lisi", 5, &waitDone)
	go pay("wangwu", 8, &waitDone)

	// 新开一个协程,专门在三个协程任务完成后去关闭信道,不然下面取不到值会死锁
	go func() {
		defer close(waitChan)
		waitDone.Wait()
	}()
	
	// 死循环在信道中取值
	//for {
	//	money, ok := <-waitChan
	//	fmt.Println(money)
	//	if !ok {
	//		break
	//	}
	//}
	
  //waitDone.Wait()
	
	var moneyList []int
	// 简化写法
	for money := range waitChan {
		moneyList = append(moneyList, money)
	}
	
	fmt.Println("时间:", time.Since(t1))
	fmt.Println(moneyList)
}

注意:

  1. 在无缓冲通道中,waitChan <- money 这一行本身必须等待一个接收方才能完成。因此发送动作和接收动作是同步配对的,不可分割。
  2. Go 语言的通道关闭规则:关闭一个有缓冲的通道后,仍然可以读取其中尚未被取走的数据,直到缓冲区变空。之后再读会得到零值且 ok == false

select

一个协程函数往多个 channel 里发数据,在主线程如何去接

select 会检查所有 case 列出的通道操作。

  • 如果有多个 case 同时就绪(比如多个通道都有数据可读),Go 会伪随机地选择其中一个执行。
  • 一旦选中并执行完该 case,select 语句就结束了(除非你在循环中使用 select)。
  • 如果没有 case 就绪,select 会阻塞,直到某个 case 变为就绪。

所以,一次 select 执行只处理一个通道操作,但它不是"只读一个通道",而是"从多个被监听的通道中,选择并执行最先就绪(或随机)的那一个"。

注意: 你可以把 select 想象成电梯:它停在多个楼层(通道)前,哪一层的按钮先被按下(通道就绪),它就开一次门并处理那一层,然后这次 select 结束。如果同时多个按钮被按下,它随机选一个开门。

select 是 Go 并发编程的核心语法,它让一个 goroutine 能够同时等待多个通道事件,常用于:

  1. 生产者-消费者模型的多路消费
  2. 超时控制(case <-time.After(...))
  3. 优雅退出(结合 done 通道)

defer close(doneChan) 放最后,第一个执行

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

var nameChan = make(chan string)   // 初始化一个长度为0的信道
var moneyChan = make(chan int)     // 初始化一个长度为0的信道
var doneChan = make(chan struct{}) // 定义一个用于关闭select的信道

func send(name string, money int, waitDone *sync.WaitGroup) {
	defer waitDone.Done()
	fmt.Printf("%s开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s购物完成,花费%d元\n", name, money)
	nameChan <- name
	moneyChan <- money
}

func main() {

	t1 := time.Now()

	var waitDone sync.WaitGroup
	waitDone.Add(3)
	go send("zhangsan", 10, &waitDone)
	go send("li", 20, &waitDone)
	go send("wangwu", 30, &waitDone)

	go func() {
		waitDone.Wait()
		// 先进后出(类似栈的执行顺序)
		defer close(nameChan)
		defer close(moneyChan)
		// 需要先执行,不然先关闭其他两个,会返回0值触发相应case执行
		defer close(doneChan)
	}()

	var nameList []string
	var moneyList []int

	var flag = false
	for !flag {
		select {
		case name := <-nameChan:
			fmt.Printf("%s\n", name)
			nameList = append(nameList, name)
		case money := <-moneyChan:
			fmt.Printf("%d\n", money)
			moneyList = append(moneyList, money)
		case <-doneChan: // 当doneChan被close时,这个case会执行
			flag = true
		}
	}

	//event := func() {
	//	for {
	//		select {
	//		case name := <-nameChan:
	//			fmt.Printf("%s\n", name)
	//			nameList = append(nameList, name)
	//		case money := <-moneyChan:
	//			fmt.Printf("%d\n", money)
	//			moneyList = append(moneyList, money)
	//		case <-doneChan:
	//			return
	//		}
	//	}
	//}
	//event()

	fmt.Println("执行完成")
	fmt.Println(moneyList)
	fmt.Println(nameList)

	fmt.Println(time.Since(t1))
}
  • case <-doneChan: 是 Go 语言中 select 语句的一个分支,它的作用是尝试从 doneChan 通道中接收一个值,但代码中并没有将这个接收到的值赋给任何变量(即丢弃接收的值)。它通常被用作信号检测:检查 doneChan 是否已关闭或有数据到来。
  • doneChan 通常被声明为 chan struct{}(空结构体通道),不传递实际数据,只传递事件信号。
  • doneChan 被关闭后,从该通道接收的操作会立即返回该类型的零值(对于 struct{} 就是空结构体),并且不会阻塞。因此 case <-doneChan: 能够在通道关闭时被立即触发。

也就是说,case <-doneChan 没有任何位置给当前 chan 输入数据,所以不会执行,但是当 close 当前 chan 时,会返回一次 0 值,然后触发当前 case 执行一次

注意: 所以当前 defer 需要放在最后,第一个执行,防止其他chan 由于 close 返回 0 值,而意外执行一次

协程超时执行

go 复制代码
package main

import (
	"fmt"
	"time"
)

var done = make(chan struct{}) // 定义一个用于关闭select的信道

func send2() {
	time.Sleep(2 * time.Second)
	close(done)
}

func main() {

	t1 := time.Now()
	go send2()
	// select 只执行一次,所以会执行先触发的
	select {
	case <-done: // 只有 close done 才会触发0值执行这个case
		fmt.Println("执行完成")
	case <-time.After(1 * time.Second): // 等待3s之后,返回当前时间值, 触发执行
		fmt.Println("超时执行")
	}

	fmt.Println(time.Since(t1))
}

time.After(d Duration) <-chan Time 返回一个只读通道(<-chan Time),该通道会在持续时间 d 之后接收到一个当前时间值(time.Time 类型)。具体行为:

  • 内部会创建一个定时器,当时间到达时,向通道发送一个时间值。
  • 调用 time.After 后立即返回通道,不会阻塞。

也就是说:time.After(1 * time.Second) 会返回一个一次性的信道,到达超时时间信道会读出一个 时间值。

十五、线程安全与sync.Map

线程不安全

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var sum int
var waitDone sync.WaitGroup

func add() {
	for i := 0; i < 100000; i++ {
		sum += i
	}
	waitDone.Done()
}

func sub() {
	for i := 0; i < 100000; i++ {
		sum -= i
	}
	waitDone.Done()
}

func main() {
	waitDone.Add(2)
	go add()
	go sub()
	waitDone.Wait()
	fmt.Println("sum =", sum)
}

线程安全写法

加锁:sync.Mutex 类型的锁

  • Lock()
  • TryLock()
  • Unlock()
go 复制代码
package main

import (
	"fmt"
	"sync"
)

var sum1 int
var waitDone1 sync.WaitGroup
var lock sync.Mutex

func add1() {
	lock.Lock()
	for i := 0; i < 100000; i++ {
		sum1 += i
	}
	lock.Unlock()
	waitDone1.Done()
}

func sub1() {
	lock.Lock()
	for i := 0; i < 100000; i++ {
		sum1 -= i
	}
	lock.Unlock()
	waitDone1.Done()
}

func main() {
	waitDone1.Add(2)
	go add1()
	go sub1()
	waitDone1.Wait()
	fmt.Println("sum1 =", sum1)
}

注意:这里的 select 只其一个一直阻塞的作用,不然主线程直接结束。

线程不安全map

go 复制代码
package main

import "fmt"

func main() {

	var mp = map[string]int{}
	go func() {
		for i := 0; i < 100000; i++ {
			mp["123"] = 1
		}
	}()
	go func() {
		for i := 0; i < 100000; i++ {
			fmt.Println(mp["123"])
		}
	}()
	select {}
}

线程安全map
sync.Map{} 内部通过加锁实现,支持的常用方法:

  • mp.Store(key, value),存值
  • map.Load(key),取值
go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {

	var mp = sync.Map{}
	go func() {
		for {
			mp.Store("123", 1)
		}
	}()
	go func() {
		for {
			value, ok := mp.Load("123")
			if ok {
				fmt.Println(value)
			}
		}
	}()
	select {}
}

十六、异常处理

go 的异常处理可能是这门语言唯一的一个诟病了。

由于 go 语言 没有捕获异常的机制,导致每调一个函数都要接一下这个函数的 error

网上有个梗,叫做 error 是 go 的一等公民

常见的异常处理
1)向上抛

将错误交给上一级处理。

一般是用于框架层,有些错误框架层面不能擅做决定,将错误向上抛不失为一个好的办法。

go 复制代码
package main

import (
	"errors"
	"fmt"
)

func div(a, b int) (res int, err error) {
	if b == 0 {
		err = errors.New("division by zero")
		return
	}
	res = a / b
	return
}

func server() (res int, err error) {
	res, err = div(10, 2)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 正常返回时的逻辑
	res++
	return
}

func main() {
	res, err := server()
	if err != nil {
		fmt.Println("错误原因:", err)
		return
	}
	fmt.Println("正确返回:", res)
}

这里可以类比为 java 的 throw/throws

2)中断程序

遇到错误直接停止程序这种一般是用于 初始化,一旦初始化出现错误,程序继续走下去也意义不大了,还不如中断掉。

(一般是配置文件等操作中)

非初始化不要使用

  • panic(xx)
  • log.Fatalln(xx) // 内部会调用 os.Exit(1)

这两个方法调用后,都会直接退出当前程序,后续代码不会执行

像 c++ 的 exit(0),和 java 的 System.exit(1);

go 复制代码
package main

import (
	"fmt"
	"os"
)

func init() {
	_, err := os.ReadFile("123")
	if err != nil {
		panic("异常读取") // 抛错误(程序直接退出,后续代码不会执行)
		//log.Fatalln("错误了")  // 内部会调用os.Exit(1)(程序直接退出,后续代码不会执行)
	}
}

func main() {
	fmt.Println("main执行")
}

3)恢复程序

我们可以在一个函数里面,使用一个 defer,可以实现对 panic 的捕获

以至于出现错误不至于让程序直接崩溃

这种一般也是框架层的异常处理所做的

注意:触发 panic 错误,当前所在的函数会中断结束运行,并触发 defer 逻辑

类比 java 理解:这里的 defer 有点像一个 try 块,相当于当前函数内所有代码被 defer try 住了,defer 内部相当于 catch 块的逻辑,这样的话,当前方法的某一行出错,后续逻辑不会执行(try 块的某一行出错),然后触发 defercatch 逻辑进行处理)。

  • err := recover() // 类似于 catch(Exception e)
  • debug.PrintStack() // 类似于 e.printStackTrace();
  • fmt.Println(err) // 类似于 System.out.println(e);
go 复制代码
package main

import (
	"fmt"
	"runtime/debug"
)

// 这里的运行时异常相当于直接 throw 了
func read() {
	var arr = []int{1, 2}
	fmt.Println(arr[3])
	// try 中的正常逻辑
	fmt.Println("read函数中的正常逻辑....")
}

// 这里相当于用了 try-catch
func operatorFun() {
	defer func() {
		err := recover() // 类似于 catch(Exception e)
		if err != nil {
			// 打印错误信息
			fmt.Println(err)
			// 打印错误堆栈信息
			debug.PrintStack()
		}
	}()
	read()
	fmt.Println("operatorFun函数中的正常逻辑....")
}

func main() {
	operatorFun()
	fmt.Println("main中的正常逻辑....")
}

十七、泛型

从1.18版本开始,Go添加了对泛型的支持,即类型参数

  • 感觉和 Java 类比,多了一个在定义当前泛型时,就需要指定对应泛型的约束(即类似 Java 的 T extends XT super X 这种,但是 java 不是强制的)

泛型函数

go 复制代码
package main

import (
	"fmt"
)

type Number interface {
	int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}

// 泛型函数
func add[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](a, b T) T {
	sum := a + b
	return sum
}

// 泛型函数(add的简化写法,通过接口实现)
func plus[T Number](a, b T) T {
	sum := a + b
	return sum
}

// Print 函数多个泛型参数
func Print[T int | uint, K string](age T, name K) {
	fmt.Printf("%s的年龄是%d\n", name, age)
}

func main() {
	fmt.Println(add(3, 100))
	fmt.Println(plus(3, 100))
	Print(18, "zhangsan")
}

这里注意下 Number 接口的用法:

Go 的类型集(Type Set)

从 Go 1.18 开始,接口(interface)的能力被扩展了。除了可以包含方法,还可以直接嵌入类型列表 。这种接口称为类型约束接口 ,它定义了一个类型集

  • 传统接口:类型集 = 所有实现了这些方法的类型。
  • 带类型列表的接口 :类型集 = 列表中列出的所有类型(且同时满足方法要求的类型【交集】)。

在 Go 1.18 引入泛型后,接口(interface)的能力得到了扩展:除了可以包含方法集合,还可以包含类型列表 (通过 | 分隔)。这种包含类型列表的接口被称为类型约束接口,它的作用是限定泛型类型参数(type parameter)必须属于某个类型集合。

与普通接口的区别:

1)定义类型约束接口并使用

go 复制代码
// 定义类型集:所有整型 + 浮点型
type Number interface {
    int | int64 | float64
}

// 泛型函数,求两个数的和
func Add[T Number](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(Add(10, 20))       // int -> 30
    fmt.Println(Add(int64(5), 6))  // int64 -> 11
    fmt.Println(Add(3.14, 2.72))   // float64 -> 5.86
    // fmt.Println(Add("hello", "world")) // 编译错误:string 不在 Number 类型集中
}

2)类型约束接口与方法的组合

go 复制代码
type MyNumber interface {
    int | float64
    String() string   // 要求类型同时实现 String() 方法(注意:int/float64 本身没有该方法,所以此接口无用)
}

这种混合写法要求类型既在类型列表中,又实现了 String() 方法。通常不会这样写,因为像 int 这样内置类型没有方法。更常见的做法是用另一个接口来约束方法,例如:

go 复制代码
type MyInterface interface {
    ~int | ~float64   // ~int 表示底层类型是 int 的类型(如 type MyInt int)
    String() string
}

但实际使用中,类型约束接口通常只包含类型列表,而方法约束则用另外的接口。

3)类型约束接口中使用的近似元素 ~

Go 允许在类型列表中使用 ~ 前缀,表示所有底层类型为该类型的集合。

go 复制代码
type Integer interface {
    ~int | ~int64   // 包括 int, int64,以及任何底层类型为 int 或 int64 的自定义类型
}

type MyInt int
func Add[T Integer](a, b T) T { return a + b }

func main() {
    fmt.Println(Add(MyInt(5), MyInt(3))) // 可以,因为 MyInt 底层是 int
}

泛型结构体

go 复制代码
package main

import (
	"encoding/json"
	"fmt"
)

// Response 定义泛型结构体
type Response[T any] struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data T      `json:"data"`
}

func main() {

	type User struct {
		Id int64
	}

	type UserInfo struct {
		Id   int64
		Name string
		Age  int32
	}

	user := Response[User]{
		Code: 200,
		Msg:  "success",
		Data: User{
			Id: 1,
		},
	}
	userInfo := Response[UserInfo]{
		Code: 200,
		Msg:  "success",
		Data: UserInfo{
			Id:   1,
			Name: "zhangsan",
			Age:  22,
		},
	}

	// 序列化
	byteData1, _ := json.Marshal(user)
	fmt.Println(string(byteData1))  // {"code":200,"msg":"success","data":{"Id":1}}

	byteData2, _ := json.Marshal(userInfo)  // {"code":200,"msg":"success","data":{"Id":1,"Name":"zhangsan","Age":22}}
	fmt.Println(string(byteData2))

	// 反序列化
	var userResponse Response[User]  // 指定泛型
	var userInfoResponse Response[UserInfo] // 指定泛型
	
	json.Unmarshal([]byte(`{"code":200,"msg":"success","data":{"Id":1}}`), &userResponse)
	fmt.Println(userResponse)

	json.Unmarshal([]byte(`{"code":200,"msg":"success","data":{"Id":1,"Name":"zhangsan","Age":22}}`), &userInfoResponse)
	fmt.Println(userInfoResponse)
}

泛型切片

go 复制代码
package main

import "fmt"

func main() {
	
	// 自定义泛型切片
	type MySlice[T int | string] []T

	var arr = MySlice[int]{}
	arr = append(arr, 1)
	fmt.Println(arr)
}

泛型map

泛型map 的 key 只能是简单类型,value 可以是任意类型

go 复制代码
package main

import "fmt"

func main() {
	// 泛型 map
	type MyMap[T int | string, K any] map[T]K
	mp := MyMap[int, string]{
		1: "1",
	}
	mp[123] = "123"
	fmt.Println(mp)
}

十八、文件操作

文件读取

一次性读取

go 复制代码
package main

import (
	"fmt"
	"os"
)

func main() {
	// 一次性读取文件
	// 读取绝对路径
	//byteData, err := os.ReadFile("/Users/mybook/GolandProjects/studyProject/go_study/文件操作/1.txt")
	// 相对路径(终端在当前文件夹下可以执行成功)
	//byteData, err := os.ReadFile("./1.txt")
	// 项目根路径下可以执行
	byteData, err := os.ReadFile("./go_study/文件操作/1.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(byteData))
}

分片读

go 复制代码
package main

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

func main() {
	// 分片读取
	// 打开文件生成File
	file, err := os.Open("./go_study/文件操作/1.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()

	// 读取到byte数组中
	var bytes = make([]byte, 13)

	// 循环读取所有的字节数据
	for {
		n, err := file.Read(bytes)
		// 读完标记
		if err == io.EOF {
			break
		}
		fmt.Println(string(bytes[:n]))
	}
}

带缓冲读

1)按行读

go 复制代码
//带缓冲读------按行读
file, err := os.Open("./go_study/文件操作/1.txt")
if err != nil {
	fmt.Println(err)
	return
}
defer file.Close()
// 获取装饰后带缓冲的 Reader
buf := bufio.NewReader(file)
for {
	line, _, err := buf.ReadLine()
	if err == io.EOF {
		break
	}
	fmt.Println(string(line))
}

2)指定分隔符

go 复制代码
//带缓冲读------按分隔符
file, err := os.Open("./go_study/文件操作/1.txt")
if err != nil {
	fmt.Println(err)
	return
}
defer file.Close()
buf := bufio.NewScanner(file)
// 指定按照单词分割(这里也可以实现对应的函数来自定义分割逻辑)
buf.Split(bufio.ScanWords)
for buf.Scan() {
	fmt.Println(buf.Text())
}

函数类型:

在 Go 中,type SplitFunc func(...)... 这种方式定义的是一个 函数类型(Function Type)。它创建了一个新的命名类型,其底层类型为一个具有指定参数和返回值的函数签名。

Go 语言中,函数也是 第一类公民(first-class value),可以像其他类型一样被声明、赋值、传递、作为参数和返回值。

  • type SplitFunc func(...)... 告诉编译器:SplitFunc 是一种新的命名函数类型
  • 底层仍然是函数,但这个新类型可以附加方法(例如 func (f SplitFunc) SomeMethod()),也可以用于实现接口。
  • 与类型别名(type A = B)不同,这里创建的是新类型 ,与原始函数类型不能直接赋值(需要显式转换),但实际使用时由于兼容性,大多数情况下可以直接赋值(因为底层类型相同,Go 允许可赋值性:如果两个函数类型签名相同,即使不同名,也可以直接赋值,但需要检查是否满足赋值规则------实际上,对于函数类型,无论是否命名,只要签名相同,就可以互相赋值。但如果是新命名的类型,直接赋值也是允许的,因为它们是底层类型相同且都是函数类型。我们可以认为是等价的。精确地说:Go 规范中,如果两个函数的参数和结果列表完全相同,且结果名称可忽略,那么它们是"相同类型",所以命名函数类型和未命名函数类型可以直接赋值。

是否所有满足入参出参要求的函数都可以用当前自定义函数类型来接?

是的。任何具有完全相同签名(参数类型、顺序、返回值类型、顺序、可变参数等)的函数,都可以:

  • 赋值给 SplitFunc 类型的变量。
  • 作为实参传递给期望 SplitFunc 类型的形参。
  • 被类型转换为 SplitFunc(虽然通常不需要显式转换)。

注意:必须完全匹配,包括参数名称(其实参数名不重要,重要的是类型和顺序)。以下情况不能匹配:

  • 参数或返回值数量不同。
  • 参数或返回值类型不同(即使可以隐式转换,比如 int 和 int64,也不允许)。
  • 返回值命名不同不影响(Go 允许返回值命名,但签名比较时忽略名称)。

另外,如果是方法 (如 func (r *Reader) Split(...)),不能直接赋值给 SplitFunc 类型变量,因为方法是绑定到接收者的函数,签名不同。但可以将方法转换为普通函数(通过闭包捕获接收者)来适配。

文件写入

一次性写入

go 复制代码
package main

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

func main() {
	// 按指定约束打开文件(文件不存在就创建|只写)
	//file, err := os.OpenFile("w.txt", os.O_CREATE|os.O_WRONLY, 0777)
	file, err := os.OpenFile("w.txt", os.O_CREATE|os.O_RDWR, 0777)
	if err != nil {
		fmt.Println(err)
		return
	}

	defer file.Close()

	// 向文件中写入数据
	//file.Write([]byte("你好"))

	// 尝试读取内容
	byteData, err := io.ReadAll(file)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(byteData))

	// 一次性写入(内部封装了 OpenFile 以及对应的 file.Write操作(和上面一样))
	err = os.WriteFile("w1.txt", []byte("你好好"), 0777)
	fmt.Println(err)
}

文件的打开方式

常见的一些打开模式

go 复制代码
// 如果文件不存在就创建
os.O_CREATE|os.O_WRONLY
// 追加写
os.O_APPEND|os.O_WRONLY
// 可读可写
os.O_RDWR

完整的模式

go 复制代码
const (
	// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
	O_RDONLY int = syscall.O_RDONLY // open the file read-only.
	O_WRONLY int = syscall.O_WRONLY // open the file write-only.
	O_RDWR   int = syscall.O_RDWR   // open the file read-write.
	// The remaining values may be or'ed in to control behavior.
	O_APPEND int = syscall.O_APPEND // append data to the file when writing.
	O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
	O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
	O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
	O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
)

文件的权限


文件复制

go 复制代码
// 文件复制
file, err := os.Open("/Users/mybook/GolandProjects/studyProject/go_study/文件操作/1.jpg")
if err != nil {
	fmt.Println(err)
	return
}
defer file.Close()
wFile, err := os.OpenFile("./2.jpg", os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
	fmt.Println(err)
	return
}
defer wFile.Close()

io.Copy(wFile, file)

目录操作

go 复制代码
// 目录操作
dir, err := os.ReadDir("go_study/文件操作")
if err != nil {
	fmt.Println(err)
	return
}
// 读取当前文件夹下的文件
for _, entry := range dir {
	info, _ := entry.Info()
	fmt.Println(entry.IsDir(), entry.Name(), info.Size())
}

十九、单元测试

单元测试

Go语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言的测试框架相似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决:

  1. 确保每个函数是可运行,并且运行结果是正确的
  2. 确保写出来的代码性能是好的
  3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

calc.go

go 复制代码
package main

func Add(a, b int) int {
	return a + b
}

calc_test.go(测试文件以 xxx_test 命名,xxx 是原来的文件名)

测试方法以 TestXXX 命名,XXX 是原来的方法名

go 复制代码
package main

import "testing"

func TestAdd(t *testing.T) {
	res := Add(1, 2)
	if res != 3 {
		t.Errorf("测试失败")
		return
	}
	t.Logf("测试通过")
}

可以idea中一键执行,也可以终端

终端执行:go test calc_test.go calc.go -v

子测试

如果需要给一个函数,调用不同的测试用例,可以使用子测试

子测试里面的 Fatal,是不会终止程序的

go 复制代码
package main

import "testing"

func TestAdd(t *testing.T) {
	t.Run("add1", func(t *testing.T) {
		if Add(1, 2) != 3 {
			t.Errorf("测试失败")
			return
		}
	})
	t.Run("add2", func(t *testing.T) {
		if Add(1, -1) != 0 {
			t.Errorf("测试失败")
			return
		}
	})
}

如果测试用例很多,还可以用一个类似表格去表示

go 复制代码
package main

import "testing"

func TestAdd(t *testing.T) {

	testCase := []struct {
		Name           string
		A, B, Excepted int
	}{
		{"test1", 1, 2, 3},
		{"test2", 1, -1, 0},
		{"test2", 30, 20, 50},
	}

	for _, v := range testCase {
		t.Run(v.Name, func(t *testing.T) {
			if Add(v.A, v.B) != v.Excepted {
				t.Errorf("测试失败")
				return
			}
		})
	}
}

[]struct { ... } 是 Go 语言中匿名结构体切片的语法。它用于需要临时组合数据,但又不想专门定义一个新类型(命名结构体)的场景。

  • []struct { ... }:声明一个切片,其元素类型是一个匿名结构体。
  • struct { Name string; A, B, Excepted int }:定义了结构体的字段布局,但没有为这个结构体起名字(所以是匿名)。
  • 后面的大括号 {...} 是切片字面量,内部每个 {...} 是结构体字面量,按字段顺序初始化。

等价于:如果先用 type TestCase struct { ... } 定义一个命名结构体,然后 testCase := []TestCase{...}。但匿名方式省去了定义类型的步骤,使代码更紧凑,尤其适合测试用例这种局部使用、不重复的场景。

[]struct { ... } 是一种 Go 特有的语法糖,它让你在定义切片的同时定义元素的字段类型,非常适合编写表格驱动测试、临时数据集和一次性聚合逻辑。理解这个语法后,你会发现它非常直观,也是 Go 语言强调"简单直接"思想的体现。

为什么用匿名结构体?

  • 一次性:这个结构体类型只在当前作用域内使用一次,不需要在其他地方引用。
  • 简洁:避免为单纯的数据容器创建全局类型声明。
  • 灵活:可以直接修改字段定义,而不用去找类型定义。

TestMain 函数

它是测试的入口

我们可以在 TestMain里面实现测试流程的生命周期

(像 Java 的环绕通知 around())

go 复制代码
package main

import (
	"fmt"
	"os"
	"testing"
)

func TestAdd1(t *testing.T) {
	fmt.Println("测试中...")
}

func setup() {
	fmt.Println("before...")
}

func teardown() {
	fmt.Println("after...")
}

func TestMain(m *testing.M) {
	fmt.Println("TestMain...")
	setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}

二十、反射

类型判断

判断一个变量是否是结构体,切片,map

  • reflect.TypeOf:从接口值中提取类型元数据。
  • t.Kind():返回该元数据中的"底层类别"(如 Int、Struct、Slice 等),用于分类处理。

Go 中的空接口 any(或 interface{})在内存中存储两部分信息:值的类型(type)值的实际数据(data)。reflect.TypeOf 会从这个接口值中提取出类型元数据,并封装成一个 reflect.Type 对象返回。

  • 我理解就是反射 字段/方法 属性(TypeOf) + 字段/方法 实际值(ValueOf)
go 复制代码
package main

import (
	"fmt"
	"reflect"
)

func getType(obj any) {
	// 获取类型
	t := reflect.TypeOf(obj)
	// t.Kind() 获取类型实际的值
	switch t.Kind() {
	case reflect.Int:
		fmt.Println("Int")
	case reflect.String:
		fmt.Println("String")
	case reflect.Struct:
		fmt.Println("Struct")
	}
}

func main() {
	getType("123")
	getType(123)
	getType(struct {
		Name string
	}{})
}

Kind() 是 reflect.Type 接口的一个方法,返回一个 reflect.Kind 类型的常量。

  • reflect.Kind 是一个枚举类型(如上面你看到的 reflect.Int、reflect.String、reflect.Struct 等),它表示该类型的底层种类(underlying kind),而不是具体的类型名。

  • 为什么需要 Kind? 因为很多用户自定义类型(如 type MyInt int)的底层种类是 reflect.Int,而 Name() 返回的是 "MyInt"。Kind() 能让我们忽略具体的命名,对一组行为相似的底层类型做统一处理(例如所有整数类型、所有切片类型)。

通过反射获取值

go 复制代码
package main

import (
	"fmt"
	"reflect"
)

func getValue(obj any) {
	v := reflect.ValueOf(obj)
	switch v.Kind() {
	case reflect.Int:
		fmt.Println("Int", v.Int())
	case reflect.String:
		fmt.Println("String", v.String())
	case reflect.Struct:
		fmt.Println("Struct")
	}
}

func main() {
	getValue("123")
	getValue(123)
	getValue(struct {
		Name string
	}{})
}

通过反射修改值

注意,如果需要通过反射修改值,必须要传指针,在反射中使用 Elem 取指针对应的值

Elem 理解成 *星号 会好一些

  • 在 Go 的反射包中,Elem 方法用于获取指针、数组、切片、映射或通道等引用类型所指向的底层值。通俗地讲,它相当于反射世界里的"解引用"操作。
go 复制代码
 package main

import (
	"fmt"
	"reflect"
)

func setValue(obj any, value any) {
	// obj 是指针类型的值
	// v1 获取到的是指针类型元数据
	v1 := reflect.ValueOf(obj)
	v2 := reflect.ValueOf(value)
	// v1.Elem 获取到的是指针指向的变量的类型
	if v1.Elem().Type() != v2.Type() {
		return
	}
	switch v1.Elem().Kind() {
	case reflect.Int:
		// 将对应值进行替换成已知的值a
		v1.Elem().SetInt(v2.Int())
	case reflect.String:
		//v1.Elem().SetString(v2.String())
		v1.Elem().SetString(value.(string))
	}
}

func main() {
	var name string = "1"
	var age int = 1
	setValue(&name, "123")
	setValue(&age, 123)
	fmt.Println(name, age)
}

Elem 的作用

  • 当 reflect.Value 持有一个指针(reflect.Ptr)时,v.Elem() 返回指针指向的值的 reflect.Value。
  • 当 reflect.Value 持有一个接口(reflect.Interface)时,v.Elem() 返回接口内动态值的 reflect.Value。
  • 对于其他类型(如 reflect.Int、reflect.Struct),调用 Elem() 会 panic,因为它没有"指向"的概念。

结构体反射

读取 json 标签对应的值,如果没有就用属性的名称

go 复制代码
 package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name  string `json:"name"`
	Age   int
	isMan bool
}

func ParseJson(obj any) {
	// 值反射
	v := reflect.ValueOf(obj)
	// 类型反射
	t := reflect.TypeOf(obj)
	for i := 0; i < v.NumField(); i++ {
		// 获取每个字段,对应的字段属性(t.Field) + 字段值(v.Filed)
		tf := t.Field(i)
		tag := tf.Tag.Get("json")
		if tag == "" {
			tag = tf.Name
		}
		fmt.Println(tag, v.Field(i))
	}
}

func main() {
	s := Student{Name: "zhangsan", Age: 18, isMan: true}
	ParseJson(s)
}

修改结构体中的字段值

例如,结构体 tag 中有big标签,就将值大写

go 复制代码
package main

import (
	"fmt"
	"reflect"
	"strings"
)

type User struct {
	Name1 string `big:"-"`
	Name2 string
}

func SetStruct(obj any) {
	// 这里通过 Elem 解引用获取到的是 u 对象本身,所以是可以修改的
	v := reflect.ValueOf(obj).Elem()
	t := reflect.TypeOf(obj).Elem()
	for i := 0; i < v.NumField(); i++ {
		// 获取到字段属性和字段值
		tf := t.Field(i)
		vf := v.Field(i)
		// 互殴去字段属性的 tag 值
		tag := tf.Tag.Get("big")
		if tag == "" {
			continue
		}
		// 修改字段值
		vf.SetString(strings.ToUpper(vf.String()))
	}
}

func main() {
	u := User{Name1: "zhangsan", Name2: "lisi"}
	SetStruct(&u)
	fmt.Println(u)
}

能修改。这是因为:

  • ptrVal 是通过 reflect.ValueOf(&x) 获得的,它的 Kind 是 reflect.Ptr,持有一个指向变量 x 的指针。
  • v := ptrVal.Elem() 返回的 reflect.Value 代表指针指向的变量 x 本身而不是它的副本)。
  • 这样的 v 是可设置的(v.CanSet() 为 true),因为它代表一个实际的内存位置,并且该位置是可寻址的。
  • 调用 v.Set(newValue) 会将 newValue 写入 x 的内存地址,从而修改原始变量。

调用结构体方法

如果结构体有 call 这个名字的方法,就执行它

注意:通过反射是拿不到首字母小写方法

go 复制代码
package main

import (
	"fmt"
	"reflect"
)

type User1 struct {
	Name string
}

func (u User1) Call(name string) {
	fmt.Println("结构体方法被调用", name)
}

func Call(obj any) {
	v := reflect.ValueOf(obj).Elem()
	t := reflect.TypeOf(obj).Elem()
	for i := 0; i < v.NumMethod(); i++ {
		tf := t.Method(i)
		if tf.Name != "Call" {
			continue
		}
		method := v.Method(i)
		// 反射调用 + 参数是Value结构体切片(Call里是切片初始化)
		method.Call([]reflect.Value{
			reflect.ValueOf("lisi"),
		})
	}
}

func main() {
	u := User1{Name: "zhangsan"}
	Call(&u)
}

orm 的一个小案例

go 复制代码
package main

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
)

type Class struct {
	Name string `whb-orm:"name"`
	age  int    `whb-orm:"age"`
}

func Find(obj any, query ...any) (sql string, err error) {
	t := reflect.TypeOf(obj)
	// 验证结构体类型
	if t.Kind() != reflect.Struct {
		err = errors.New("结构体类型不匹配")
		return
	}
	// 拼接 where
	var where string
	if len(query) > 0 {
		s, ok := query[0].(string)
		if !ok {
			err = errors.New("query模板需要传字符串")
			return
		}
		if len(query)-1 != strings.Count(s, "?") {
			err = errors.New("where 语句参数数量不匹配")
			return
		}
		// 替换 where 的问号
		for _, q := range query[1:] {
			switch v := q.(type) {
			case string:
				//s = strings.Replace(s, "?", v, 1)
				s = strings.Replace(s, "?", fmt.Sprintf("%s", v), 1)
			case int:
				//s = strings.Replace(s, "?", strconv.Itoa(v), 1)
				s = strings.Replace(s, "?", fmt.Sprintf("%d", v), 1)
			}
		}
		where = "where " + s
	}
	// 拼接 select
	var fieldNames []string
	for i := 0; i < t.NumField(); i++ {
		tf := t.Field(i)
		tag := tf.Tag.Get("whb-orm")
		if tag == "" {
			continue
		}
		fieldNames = append(fieldNames, tag)
	}

	// sql拼接
	sql = fmt.Sprintf("select %s from %s %s", strings.Join(fieldNames, ","), strings.ToLower(t.Name()), where)
	return
}

func main() {
	// select name,age from class
	sql, err := Find(Class{})
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(sql)
	// select name,age from class where name='zhangsan'
	sql, err = Find(Class{}, "name=?", "zhangsan")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(sql)
	// select name,age from class where name='zhangsan' and age=18
	sql, err = Find(Class{}, "name=? and age=?", "zhangsan", 18)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(sql)
}

对反射的一些建议

二十一、网络编程

TCP

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议

如何保证连接可靠呢? (面试常考题)

  • 三次握手
  • 四次挥手

1)服务端

go 复制代码
package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	// 获取TCPAddr对象
	addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
	// 获取对应tcp的监听器
	listener, err := net.ListenTCP("tcp", addr)
	if err != nil {
		fmt.Println(err)
		return
	}
	for {
		// 等待客户端连接
		conn, err := listener.Accept()
		if err != nil {
			break
		}
		// 客户端ip
		fmt.Println(conn.RemoteAddr().String() + " connect success")
		// 接收客户端发的内容
		go func() {
			var buf = make([]byte, 1024)
			for {
				n, err := conn.Read(buf)
				// 客户端错误
				if err == io.EOF {
					fmt.Println(conn.RemoteAddr().String() + "disconnect")
					break
				}
				fmt.Println(string(buf[:n]))
				conn.Write([]byte(conn.RemoteAddr().String() + "说了:" + string(buf[:n])))
			}
			defer conn.Close()
		}()
	}
}

2)客户端

go 复制代码
package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	// 发起tcp请求,建立连接
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println(err)
		return
	}
	// 接收服务端发送的消息
	go func() {
		var buf = make([]byte, 1024)
		for {
			n, err := conn.Read(buf)
			// 服务端错误
			if err == io.EOF {
				break
			}
			fmt.Println(string(buf[:n]))
		}
	}()
	// 给服务端发送消息
	for {
		var text string
		fmt.Print("请输入你要发送的内容:")
		fmt.Scan(&text)
		if text == "q" {
			break
		}
		conn.Write([]byte(text))
	}
	defer conn.Close()
}

HTTP

1)服务端

go 复制代码
package main

import (
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
)

// 获取当前源文件的绝对路径
func getCurrentDir() string {
	_, file, _, _ := runtime.Caller(0)
	return filepath.Dir(file)
}

func Index(writer http.ResponseWriter, request *http.Request) {
	fmt.Println(request.URL.Path, request.UserAgent())
	// 响应文本
	//writer.Write([]byte("hello world"))
	// 响应页面
	byteData, err := os.ReadFile(filepath.Join(getCurrentDir(), "index.html"))
	if err != nil {
		fmt.Println(err.Error())
		return
	}
	writer.Write(byteData)
}

func main() {

	// 路由回调
	http.HandleFunc("/", Index)

	fmt.Println("访问地址:http://127.0.0.1:8080")
	// 绑定并监听
	http.ListenAndServe("127.0.0.1:8080", nil)

}

2)客户端

go 复制代码
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {

	resp, err := http.Get("http://127.0.0.1:8080")
	if err != nil {
		fmt.Println(err)
		return
	}
	// 通过io流读取响应数据
	byteData, _ := io.ReadAll(resp.Body)
	fmt.Println(string(byteData))
}

RPC

远程过程调用的协议

1)服务端

go 复制代码
package main

import (
	"fmt"
	"net"
	"net/http"
	"net/rpc"
)

type Server struct {
}

type Req struct {
	Num1 int
	Num2 int
}

type Resp struct {
	Num int
}

func (s Server) Add(req Req, res *Resp) error {
	res.Num = req.Num1 + req.Num2
	return nil
}

func main() {

	// 注册 Rpc 服务
	rpc.Register(new(Server))
	rpc.HandleHTTP()
	listen, err := net.Listen("tcp", ":8081")
	if err != nil {
		fmt.Println(err)
		return
	}
	http.Serve(listen, nil)
}

new(Server) 的含义

  • new(Server) 是 Go 的内建函数,它:
    • 分配一个 Server 类型的零值实例,并返回指向该实例的指针(*Server)。
  • 等价于 &Server{}。
  • 所以 new(Server) 的类型是 *Server

rpc.Register 的作用:

  • 接收一个任意类型的参数 rcvr(通常是一个指针)。
  • 它会分析 rcvr 类型的方法集,将符合 RPC 规则的方法公开为远程可调用的过程。
  • 如果注册成功,返回 nil;否则返回错误(例如没有符合条件的方法)。

要使 Server 类型的方法能被 RPC 调用,必须满足以下规则:

  • 方法必须是可导出的(首字母大写)。
  • 方法必须接收两个参数,且第二个参数必须是指针类型(用于返回值)。
  • 方法必须返回一个 error 类型的值。

标准方法签名:

go 复制代码
func (t *T) MethodName(args *ArgsType, reply *ReplyType) error
  • args:客户端传入的参数(通常是指针,但也可以是值类型;推荐指针)。
  • reply:返回值必须是指针类型,服务端会填充该指针指向的变量。
  • 返回值 error:如果方法执行成功,返回 nil;否则返回错误信息。
go 复制代码
type Server struct{}

type AddArgs struct {
    A, B int
}

type AddReply struct {
    Sum int
}

func (s *Server) Add(args *AddArgs, reply *AddReply) error {
    reply.Sum = args.A + args.B
    return nil
}

rpc.Register(new(Server)) 完整流程

  • new(Server) 创建 Server 类型的零值指针。
  • rpc.Register 接收该指针,通过反射获取它的类型和方法。
  • 遍历所有符合 RPC 规则的方法(如上面的 Add),将它们注册到内部方法映射表中。
  • 之后启动 RPC 服务(例如 rpc.Accept 或配合 HTTP 的 rpc.HandleHTTP),客户端就可以通过 Add 方法名调用这个远程服务。

为什么要传指针而不是值?

  • RPC 框架通常需要修改接收者的状态(虽然多数服务是无状态的),但更为重要的是:

    • 反射要求方法的接收者类型与注册时的类型匹配。
      Register(new(Server)) 注册的是 *Server,因此方法必须定义在指针接收者上(如 func (s *Server) Add(...))。
    • 如果尝试注册 Server{} 值类型,则只能看到值接收者的方法(func (s Server) Add(...)),但通常建议使用指针接收者,因为这样可以避免复制整个结构体,且能统一修改内部状态。

2)客户端

go 复制代码
package main

import (
	"fmt"
	"net/rpc"
)

type Req struct {
	Num1 int
	Num2 int
}

type Resp struct {
	Num int
}

func main() {
	req := Req{1, 2}
	client, err := rpc.DialHTTP("tcp", ":8081")
	if err != nil {
		fmt.Println(err)
		return
	}
	var res Resp
	client.Call("Server.Add", req, &res)
	fmt.Println(res)
}

websocket

websocket 是 socket 连接和 http 协议的结合体,实现网页和服务端的长连接

go get github.com/gorilla/websocket

1)服务端

go 复制代码
package main

import (
	"fmt"
	"github.com/gorilla/websocket"
	"net/http"
)

var UP = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

func handler(res http.ResponseWriter, req *http.Request) {
	// 服务升级
	conn, err := UP.Upgrade(res, req, nil)
	if err != nil {
		fmt.Println(err)
		return
	}

	for {
		// 消息类型,消息,错误
		t, p, err := conn.ReadMessage()
		if err != nil {
			break
		}

		conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗?", string(p))))
		fmt.Println(t, string(p))
	}

	defer conn.Close()
	fmt.Println("服务关闭")
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

服务端给多个客户端发

go 复制代码
package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/websocket"
)

var UP = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

var connLis []*websocket.Conn

func handler(res http.ResponseWriter, req *http.Request) {
	// 服务升级
	conn, err := UP.Upgrade(res, req, nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	connLis = append(connLis, conn)
	for {
		// 消息类型、消息、错误
		t, p, err := conn.ReadMessage()
		if err != nil {
			break
		}
		for index := range connLis {
			connLis[index].WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗?", string(p))))
		}
		fmt.Println(t, string(p))
	}
	defer conn.Close()
	fmt.Println("服务关闭")
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

2)客户端

go 复制代码
package main

import (
	"bufio"
	"fmt"
	"os"

	"github.com/gorilla/websocket"
)

func main() {
	dl := websocket.Dialer{}
	conn, _, err := dl.Dial("ws://127.0.0.1:8080", nil)
	if err != nil {
		fmt.Println(err)
		return
	}

	go send(conn)
	for {
		t, p, err := conn.ReadMessage()
		if err != nil {
			break
		}
		fmt.Println(t, string(p))
	}
}

func send(conn *websocket.Conn) {
	for {
		reader := bufio.NewReader(os.Stdin)
		l, _, _ := reader.ReadLine()
		conn.WriteMessage(websocket.TextMessage, l)
	}
}

3)postman 作为客户端

二十二、go 部署

go 项目的部署特别简单,编写完成之后,只需要执行 go build 即可打包为可执行文件

注意,这个操作是不同平台不一样的

windows 下打包就是 exe文件,linux 下打包就是二进制文件

打包命令

go 复制代码
go build

打当前目录下的 main 包,注意,只能有一个 main 函数的包

go 复制代码
go build xxx.go

打当前目录下,xxx.go 的包,这个包必须是一个 main 包,不然没有效果

go 复制代码
go build -o main xxx.go

强制对输出的文件进行重命名
-o 参数必须得在文件的前面

交叉编译

什么是交叉编译呢,就是在 windows 上,我开发的 go 程序,我也能打包为 linux 上的可执行程序

例如在 windows 平台,打 linux 的包

注意,执行 set 这个命令,一定要是在 cmd 的命令行下,powershell 是无效的

go 复制代码
set CGO_ENABLED=0
set GOOS=linux
set GOARCH=amd64
go build -o main main.go

mac 打 windows 包

go 复制代码
export CGO_ENABLED=0
export GOOS=windows
export GOARCH=amd64
go build -o myapp.exe main.go

CGO_ENABLED: CGO 表示 golang 中的工具,CGO_ENABLED=0 表示 CGO 禁用,交叉编译中不能使用 CGO

GOOS: 环境变量用于指定目标操作系统,mac 对应 darwin,linux 对应 linux,windows 对应 windows ,还有其它的 freebsd、android 等

GOARCH: 环境变量用于指定处理器的类型,386 也称 x86 对应 32 位操作系统,amd64 也称 x64 对应 64 位操作系统,arm 这种架构一般用于嵌入式开发。比如 Android 、ios 、Win mobile 等

为了方便呢,可以在项目的根目录下写一个 bat 文件

这样就能快速构建了

然后放到 linux 服务器下,设置文件权限就可以直接运行了

go 复制代码
chmod +x main
./main
相关推荐
技术小结-李爽1 小时前
【学习】怎样把“提问题”推荐给别人
学习
꧁꫞꯭零꯭点꯭꫞꧂1 小时前
FastAPI入门学习
学习·fastapi
Byron__1 小时前
Java JVM核心知识点复习笔记
java·jvm·笔记
爱莉希雅&&&2 小时前
MySQL MGR + MySQL Router 高可用集群完整笔记(含手动配置 + Shell 接管双路线)
linux·数据库·笔记·mysql·mysqlrouter·mysqlshell
凉、介2 小时前
Armv8-A virtualization 笔记 (一)
c语言·笔记·学习·嵌入式·虚拟化·hypervisor
楼田莉子2 小时前
仿Muduo的高并发服务器:LoopThread模块及其ThreadPool模块
linux·服务器·c++·后端·学习
菜鸟的日志3 小时前
【嵌入系统】嵌入式学习笔记(一)
windows·笔记·嵌入式硬件·学习·ubuntu·操作系统
暗夜猎手-大魔王3 小时前
OpenCode提示词工程学习
学习
Slow菜鸟3 小时前
Docker 学习篇(七)| 实战 — 用 Docker 构建 SpringBoot + Vue 全栈项目
spring boot·学习·docker