Go语言的函数和defer用法

news2024/11/25 18:25:36

目录

函数的基本用法

函数中的变长参数

递归函数(recursion)

函数是“一等公民”

函数中defer的用法

defer的底层原理

使用 defer 跟踪函数的执行过程

defer的注意事项

(1)Go语言内置的函数中哪些可以作为deferred 函数

(2)注意 defer 关键字后面表达式的求值时机

(3)注意defer 带来的性能损耗


函数的基本用法

Go 语言中的函数可以使用和其它编程语言类似的方式定义,也可以将函数设置为一个变量,具体用法如下面的代码所示:

package main

import "fmt"

func main() {
	/* 函数的定义 */
	caclResult1 := cacl1(1, 2) // 3
	fmt.Println(caclResult1)

	//声明一个类型为函数类型的变量,直接写在一个已有的函数里面
	var cacl2 = func(a int, b int) int {
		return a + b
	}
	fmt.Println(cacl2(1, 2)) // 3

	//多返回值
	if caclResult3, errMsg := cacl3(-1, 2); errMsg == "" {
		fmt.Println(caclResult3)
	} else {
		fmt.Println(errMsg) //a或者b不能小于0
	}

    /* 指针运算 */
	var num = 1
	ptr(&num)
	fmt.Println(num) //2
}

// 定义函数的传统方式,指定一个int类型的返回值
func cacl1(a int, b int) int {
	return a + b
}

// 函数支持多返回值
func cacl3(a int, b int) (int, string) {
	if a < 0 || b < 0 {
		return -1, "a或者b不能小于0"
	}
	return a + b, ""
}

// 为每个返回值声明变量名,这种带有名字的返回值被称为具名返回值
func cacl4(a int, b int) (result int, errMsg string) {
	if a < 0 || b < 0 {
		errMsg = "a或者b不能小于0"
		return
	}
	result = a + b
	return
}

// 指针运算
func ptr(n *int) {
	*n++
}

多个参数具有相同类型放在一起,可以只写一次类型: 

package main

import "fmt"

func main() {
    fmt.Println(Fun("A", "B", "C", -1)) //15 16 你好
}

func Fun(a, b, c string, d int) (x, y int, z string) {
	x = 15
	y = 16
	z = "你好"
	return
	//return 15, 16, "你好" // 这样也可以
}

Go 语言中的函数参数不管使用哪种数据类型都是值传递,也就是把实际参数的内存表示拷贝到了形式参数中。对于整型、数组、结构体 类型作为实参时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。但是 string、切片、map 这些类型作为实参时,值传递拷贝的是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符” 而不拷贝实际数据内容的拷贝过程就是 “浅拷贝”。 

package main

import "fmt"

func main() {
	/* Go 语言中的函数参数不管使用哪种数据类型都是值传递,是对数据进行一个拷贝, 但是具体分为引用类型和非引用类型 */
	arr1 := [4]int{1, 2, 3, 4}
	fmt.Println(arr1) //[1 2 3 4]
	fun1(arr1)
	fmt.Println(arr1) //[1 2 3 4]
	println()

	s1 := []int{1, 2, 3, 4}
	fmt.Println(s1) // [1 2 3 4]
	fun2(s1)
	fmt.Println(s1) //[100 2 3 4]
}

func fun1(arr [4]int) {
	arr[0] = 100
}

func fun2(sli []int) {
	sli[0] = 100
}

函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型。而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。

因此,函数类型也可以看成是由 func 关键字与函数签名组合而成的。 如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型。比如下面两个函数类型都是func (int, string) ([]string, error),因此它们是相同的函数类型。

func (a int, b string) (results []string, err error)
func (c int, d string) (sl []string, err error)

函数中的变长参数

先来一个简单的例子感受一下可变长参数:

package main

import "fmt"

func main() {
	fmt.Println(Sum(1, 2))                // 3
	fmt.Println(Sum(1, 2, 3, 4))          // 10
	fmt.Println(Sum(1, 2, 3, 4, 5, 6, 7)) // 28

	//向可变长函数中传入一个切片,需要加上...
	sli := []int{1, 2, 3, 4, 5}
	fmt.Println(Sum(sli...)) //15
}

// 可变长参数入门,对多个数求和运算
func Sum(ops ...int) int {
	ret := 0
	for _, op := range ops {
		ret += op
	}
	return ret
}

Go语言支持形参传递变长参数,在具体的数据类型前面加上 ... 表示,比如:...int,...string 等。变长参数实际上是通过切片来实现的,所以在函数体中就可以使用切片支持的所有操作来操作变长参数,这会大大简化了变长参数的使用复杂度。需要注意,如果函数中既有普通参数又有可变参数,那么可变参数需要放在最后面,并且一个函数的参数列表中最多只能有一个可变参数。

package main

import "fmt"

func main() {
	//myAppend
	sl := []int{1, 2, 3}
	sl = myAppend(sl) // no elems to append
	fmt.Println(sl)   // [1 2 3]
	sl = myAppend(sl, 4, 5, 6)
	fmt.Println(sl) // [1 2 3 4 5 6]
}

// 对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参
func myAppend(sl []int, elems ...int) []int {
	fmt.Printf("%T\n", elems) // 输出变长参数的类型: []int, //变长参数实际上是通过切片来实现的
	if len(elems) == 0 {    //可以用切片的len()获取变长参数的个数
		fmt.Println("no elems to append")
		return sl
	}
	sl = append(sl, elems...)
	return sl
}

递归函数(recursion)

一个函数自己调用自己,就叫做递归函数,递归函数要有一个退出的条件,否则就是死循环。

package main

import "fmt"

func main() {
	/* 递归函数:计算斐波那契数列 */
	fmt.Println(getFibonacci(12)) //144
}

func getFibonacci(n int) int {
	if n == 1 || n == 2 {
		return 1
	}
	return getFibonacci(n-1) + getFibonacci(n-2)
}

函数是“一等公民”

为什么这么说?因为在Go语言中,函数可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回,因此可以被称为是“一等公民”。

(1)Go 函数可以存储在变量中,这种函数就是匿名函数;此时这个变量可以作为参数传递给其他函数,也可以拥有自己的类型。

package main

import (
	"fmt"
)

// 把函数存储到一个变量中
var (
	userId   = 100
	userData = func(username string, age int32) string {  //这个函数就是个匿名函数,顾名思义,就是没有名字的函数
		return fmt.Sprintf("用户名:%v,年龄:%d", username, age)
	}
)

func main() {
    fmt.Printf("%T \n", userData) //获取类型: func(string, int32) string
	userInfo := fmt.Sprintf("用户ID: %v, 其它信息: %v", userId, userData("zhangsan", 18))
	fmt.Println(userInfo) //用户ID: 100, 其它信息: 用户名:zhangsan,年龄:18
}

 (2)可以基于函数创建一个自定义类型,创建后的类型就是函数类型

type MyUserData func(float64, bool)

(3)Go 函数不仅可以在函数外创建,还可以在函数内创建,函数也可以作为函数的参数和返回值(有点绕?细品!)

package main

import (
	"fmt"
)

func main() {
    getUserFunc("test") //getUserFunc: test ---
	println()
	getUserFunc("test")() //getUserFunc: test --- getUserFunc里面的func: test
	println()
}

// 在函数内创建函数
func getUserFunc(username string) func() {
	fmt.Print("getUserFunc: ", username, " --- ")
	return func() {
		fmt.Print("getUserFunc里面的func: ", username)
	}
}

再举个例子,比如有一个函数,要计算它的执行耗时,可以这么写:

package main

import (
	"fmt"
)

func main() {
    fmt.Println(timeCost(slowFun)(10)) 
}

func timeCost(myFunc func(num int) int) func(num int) int {
	return func(n int) int {
		start := time.Now()
		ret := myFunc(n)
		fmt.Println("time cost: ", time.Since(start).Seconds(), " 秒")
		return ret
	}
}
func slowFun(num int) int {
	time.Sleep(time.Second * 1)
	return num * num
}

怎么样,看到这里是不是对Go语言中的函数有点兴趣了?是不是有点渐入佳境的感觉?或者是更迷惑了?没关系,多看几次就清晰了。

(4)Go 语言中的闭包是在函数内部创建的匿名函数,这个匿名函数可以访问创建它的函数的参数与局部变量。

假设要实现一个监控告警的例子:根据不同的告警规则,触发不同渠道的不同紧急程度的告警。
告警支持多种通知渠道,包括:邮件、微信、语音电话。
通知的紧急程度有多种类型,包括:严重、紧急、普通。

package main

import "fmt"

func main() {
	//传统的定义函数调用
	fmt.Println(alarm("邮件", "严重"))
	fmt.Println(alarm("邮件", "紧急"))
	fmt.Println(alarm("邮件", "普通"))
	fmt.Println(alarm("微信", "严重"))
	fmt.Println(alarm("微信", "紧急"))
	fmt.Println(alarm("微信", "普通"))

	println("------------")

	//使用闭包函数调用
	email := myAlarm("邮件")
	fmt.Println(email("严重"))
	fmt.Println(email("紧急"))
	fmt.Println(email("普通"))
	weixin := myAlarm("微信")
	fmt.Println(weixin("严重"))
	fmt.Println(weixin("紧急"))
	fmt.Println(weixin("普通"))
}

func alarm(messageType string, messageDegree string) string {
	return fmt.Sprintf("消息类型:%v, 重要程度:%v", messageType, messageDegree)
}

func myAlarm(messageType string) func(string) string {
	return func(messageDegree string) string {
		return alarm(messageType, messageDegree)
	}
}

可以看到,两种方式输出的结果是一样的,第二种方式可以减少参数的重复输入。但是,你是否会觉得这样的简化程度意义不大?实际上这只是个简单的易于理解的例子,在很多复杂的场景下这种方式能起到很好的优化作用。这里的案例场景借鉴了我之前写的一篇文章:PHP设计模式之桥接模式_浮尘笔记的博客-CSDN博客 

函数中defer的用法

一般在连接数据库并操作数据之后都会释放连接,比如在PHP中可能会写在 析构函数(__destruct)中销毁资源;或者又比如PHP中的 try...catch语句后面的 finally语句最终都会执行。类似的,Go 语言提供的 defer 是一种延迟调用机制,只有在函数和方法内部才能使用 defer。无论是执行到函数尾部返回,还是在某个错误处理分支显式 return,又或是出现 panic,已经存储到 deferred 函数栈中的函数都会被调度执行。简单地说:defer函数就是用来“收尾”的(例如清理资源、释放锁等)。举个例子:

package main

import "fmt"

func main() {
	fmt.Println("数据库已连接")
	defer closeDB()
	fmt.Println("执行数据库查询...")
	panic("不小心写错了SQL语句...")
	fmt.Println("Unreachable code")
}

func closeDB() {
	fmt.Println("数据库连接已关闭")
}

这里释放数据库函数的 defer 紧邻着数据库连接成功的动作,这样成对出现就可以很大程度降低遗漏释放资源的情况,代码的可读性也提高了。

defer的底层原理

defer 将函数或方法注册到其所在的 Goroutine 中,这些 deferred 函数将在执行 defer 的函数退出前按后进先出(LIFO)的顺序被调度执行,也就是“栈”,如下图所示:

使用 defer 跟踪函数的执行过程

按照上面defer的底层原理是个“栈”的逻辑,可以使用defer实现一个函数调用栈,跟踪函数的执行过程,代码如下:

package main

// 使用 defer 跟踪函数的执行过程
func main() {
	defer Trace("main")()
	foo()
}
func Trace(name string) func() {
	println("enter:", name)
	return func() {
		println("exit:", name)
	}
}

func foo() {
	defer Trace("foo")()
	bar()
}
func bar() {
	defer Trace("bar")()
}

可以看到, 程序按 main -> foo -> bar 的函数调用次序执行,并且逐层返回。但是仔细看还是有一些“瑕疵”的,如下:

  • 调用 Trace 时需手动显式传入要跟踪的函数名;
  • 如果是并发应用,不同 Goroutine 中函数链跟踪混在一起无法分辨;
  • 输出的跟踪结果缺少层次感,调用关系不易识别;
  • 对要跟踪的函数,需手动调用 Trace 函数。

因此可以做一些优化,参考:https://www.cnblogs.com/rxbook/p/17397888.html

defer的注意事项

(1)Go语言内置的函数中哪些可以作为deferred 函数

扩展:Go 语言内置的函数如下 append cap close complex copy delete imag len make new panic print println real recover

var c chan int
var sl []int
var m = make(map[string]int, 10)
m["item1"] = 1
m["item2"] = 2
var a = complex(1.0, -1.4)
var sl1 []int
defer append(sl, 11)
defer cap(sl)
defer close(c)
defer complex(2, -2)
defer copy(sl1, sl)
defer delete(m, "item2")
defer imag(a)
defer len(sl)
defer make([]int, 10)
defer new(*int)
defer panic(1)
defer print("hello, defer\n")
defer println("hello, defer")
defer real(a)
defer recover()

从这组错误提示中可以看到: append、cap、len、make、new、imag 等内置函数都是不能直接作为 deferred 函数的,而 close、copy、delete、print、recover 等内置函数可以设置为 deferred 函数。 

其实,对于不能直接作为 deferred 函数的内置函数,可以使用一个包裹它的匿名函数来间接实现,以 append 为例:

var slx []int
defer func() {
	_ = append(slx, 11)
}()

(2)注意 defer 关键字后面表达式的求值时机

defer 关键字后面的表达式,是在将 deferred 函数注册到deferred 函数栈的时候进行求值的。什么意思?看下面的例子:

package main

import "fmt"

func main() {
	fmt.Print("testA result:")
	testA()
	println()
	fmt.Print("testB result:")
	testB()
	println()
	fmt.Print("testC result:")
	testC()
	println()
}

func testA() {
	for i := 0; i <= 3; i++ {
		defer fmt.Print(i, " ")
	}
	//输出 testA result:3 2 1 0
	/*每当 defer 将fmt.Print 注册到 deferred 函数栈的时候,都会对 Print 后面的参数进行求值。依次压入 deferred 函数栈的函数是:
	fmt.Print(0)
	fmt.Print(1)
	fmt.Print(2)
	fmt.Print(3)
	因此,当 testA 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以LIFO 次序出栈执行,这时的输出的结果为:3,2,1,0
	*/
}
func testB() {
	for i := 0; i <= 3; i++ {
		defer func(n int) {
			fmt.Print(n, " ")
		}(i)
	}
	//输出 testB result:3 2 1 0
	/*每当 defer 将匿名函数注册到 deferred 函数栈的时候,都会对该匿名函数的参数进行求值。依次压入 deferred 函数栈的函数是:
	func(0)
	func(1)
	func(2)
	func(3)
	因此,当 testB 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以LIFO 次序出栈执行,因此输出的结果为:3,2,1,0
	*/
}
func testC() {
	for i := 0; i <= 3; i++ {
		defer func() {
			fmt.Print(i, " ")
		}()
	}

	//输出 testC result:4 4 4 4
	/* testC 中 defer 后面接的是一个不带参数的匿名函数。依次压入 deferred 函数栈的函数是:
	func()
	func()
	func()
	func()
	所以,当 testC 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以LIFO 次序出栈执行。
	匿名函数会以闭包的方式访问外围函数的变量 i,并通过 Print 输出i 的值,此时 i 的值为 4,因此 testC 的输出结果为:4,4,4,4
	*/
}

因此,得到的结论是,无论以何种形式将函数注册到 defer 中,deferred 函数的参数值都是在注册的时候进行求值的。

(3)注意defer 带来的性能损耗

先写一段测试代码,分别使用defer和不使用defer,对比一下执行开销:

package main

import "testing"

func sum(max int) int {
	total := 0
	for i := 0; i < max; i++ {
		total += i
	}
	return total
}

func fooWithDefer() {
	defer func() {
		sum(10)
	}()
}

func fooWithoutDefer() {
	sum(10)
}

// 测试带有 defer 的函数执行的性能
func BenchmarkFooWithDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fooWithDefer()
	}
}

// 测量不带有 defer 的函数的执行的性能
func BenchmarkFooWithoutDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fooWithoutDefer()
	}
}

使用Benchmark(性能基准测试)可以直观地看到 defer 会带来多少性能损耗,运行命令和结果如下:

go02 $ go test -bench . func_defer_test.go 

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
BenchmarkFooWithDefer-4         143286962                8.390 ns/op
BenchmarkFooWithoutDefer-4      227384844                5.267 ns/op
PASS
ok      command-line-arguments  3.793s

可以看到 带有 defer 的函数执行开销大概是不带 defer 的函数的执行开销的 1.6 倍左右(不同的机器运行的效果也不一样),因此可以放心使用。

需要注意的是,在 Go 1.13 前的版本中,使用 defer 的函数的执行时间是没有使用 defer 函数的 8 倍左右,如果你用的是Go 1.13 之前的版本一定要留意。

源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/basic/go02

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/534467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试code(1)—— 排序算法

算法动画 从小到大排序 1 冒泡排序 被动的将最大值送到最右边 1、比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。 2、对每一对相邻元素作同样的工作&#xff0c;从开始第一对到结尾的最后一对。这步做完后&#xff0c;最后的元素会是最大的数。 3、针对…

Redis修炼 (17. redis主从集群的数据同步原理 全量同步/增量同步)

数据同步 在一个集群之中 数据同步是很重要的。 redis的集群有多种。 我们这次主要说 主从集群。 全量同步 既然是主从集群 那么肯定是 1个master节点 多个从节点。redis的集群中的第一次同步 我们叫 全量同步&#xff1a; 为什么要强调第一次&#xff1f; 因为如果你是第一…

独家策略大放送:最高年化150%的策略,谁不感兴趣?(含免费版)

上一节我们在沪深300中回测了550中均线交叉策略,有朋友想看看这些策略在沪深300以外的中小市场表现如何,同时大家都非常好奇表现抢眼的老Q自研指标WMA_Q系列到底是怎么计算的。 于是老Q又选择了中证500和创业板指来验证下这些策略是否能有同样的表现(PART 1),顺便在今天的…

流水线中的握手信号笔记

1.《握手信号的打拍(一)》 解释了&#xff0c;为什么在流水线中&#xff0c;握手信号不能简单得加一级寄存器 业界关于流水线级握手信号的标准答案是 skid buffer&#xff0c;此外还有人提到了 Register slice 2.《握手信号的打拍(二)》 为什么简单加一级寄存器会握手失败 &…

抖音SEO系统源码开发搭建/MVC二次开发定制

首先&#xff0c;抖音SEO矩阵系统源码开发&#xff0c;如何做独立部署&#xff0c;首先我们需要深刻理解这个系统的开发逻辑是什么&#xff1f;开发的前言是在抖音平台做流量新增&#xff0c;现在抖音及各大主流短视频平台&#xff0c;流量新增&#xff0c;各大企业需要在短视频…

Redis BigKey

Redis BigKey 一 面试题引入二 MoreKey案例2.1 大批量往redis里面插入2000W测试数据key2.1.1 Linux Bash下执行&#xff0c;插入100W2.1.2 通过redis提供的管道 --pipe命令插入100W大批量数据 2.2 key *2.3 生产上如何限制keys*/flushdb/flushall等危险命令以防止误删误用&…

我们在操作自动化测如何实现用例设计实例

在编写用例之间&#xff0c;笔者再次强调几点编写自动化测试用例的原则&#xff1a; 1、一个脚本是一个完整的场景&#xff0c;从用户登陆操作到用户退出系统关闭浏览器。 2、一个脚本脚本只验证一个功能点&#xff0c;不要试图用户登陆系统后把所有的功能都进行验证再退出系统…

【结构体-位段】

位段 在结构体中&#xff0c;以位为单位的成员&#xff0c;咱们称之为位段(位域)。 struct packed_data{unsigned int a:2;unsigned int b:6;unsigned int c:4;unsigned int d:4;unsigned int i; } data;注意&#xff1a;不能对位段成员取地址。 #include<stdio.h>str…

5.名词复数、动词规则、代词、形容词、副词(不包含不规则)

目录 一、 名词、动词、代词、形容词、副词五种的规则变化。 &#xff08;1&#xff09;名词。 &#xff08;1.1&#xff09;名词复数变化。 &#xff08;1.2&#xff09;名词所有格。 &#xff08;2&#xff09; 动词变化规则。 &#xff08;3&#xff09;代词。 &…

C语言-double和float在内存中的存储方式

本文主要介绍double和float数据类型在C语言中的存储方式 文章目录 double和float存储方式介绍如何存储&#xff1f; double和float存储方式介绍 从存储结构和算法上来讲&#xff0c;double和float是一样的&#xff0c;不一样的地方仅仅是float是32位的&#xff0c;double是64位…

【CAN卡通信的下位机-STM32cubeIDE-hal库+STMF1xx和STMF4xx+数据发送和接收+轮询接收方式+基础样例(1)】

【CAN卡通信的下位机-STM32cubeIDE-hal库数据发送和接收轮询接收方式基础样例1】 1、概述2、实验环境3、自我总结与提升(1)道理学习了一堆&#xff0c;如何使用STM32进行can的收发的话&#xff0c;配置还是挺简单。(2)自己实现了can的收发后&#xff0c;要反过来&#xff0c;补…

shell编程——Here Document免交互与Expect(免交互,高效率)

shell编程——Here Document免交互与Expect&#xff08;免交互&#xff0c;高效率&#xff09; 一、Here Document免交互概述二、Here Document常规用法1、免交互方式实现对行数地统计2、通过read/tee命令接受输入并打印3、通过passwd给用户设置密码4、支持变量替换5、整体赋值…

K8S基础操作之命令篇

目录 第一章.陈述式资源管理 1.1陈述式资源管理方法 1.2.基本命令查看信息 1.3.K8S管理操作分为2大类 1.4.数据网络端口访问流程 第二章.基本信息查看 2.1.命令格式 2.2.命令 2.3.项目的生命周期 第三章.service 3.1.概述 3.2.service 的 type 类型 3.3 headless …

神级指标DMI魔改免费公开!在宽基指数上也可以收获40倍收益,每年都在创新高!

一、写在前头 今天,我们要讲的DMI实际上是一组指标,它由表示多空方向的PDI、MDI以及表示趋势强度的ADX、ADXR共四条线组成。在正式开讲之前,我们先聊几句近期的行情。 上周我们根据量化策略提示了一些板块的机会,其中有一些已经开始有所表现。比如今天涨幅前十的板块中,…

【python笔记】可变对象和不可变对象

前言 在python中&#xff0c;一切事物皆是对象&#xff0c;变量是对象在内存中的存储和地址的抽象。类型也是属于对象的&#xff0c;而不是变量。变量和对象是分离的&#xff0c;对象是内存中储存数据的实体&#xff0c;变量则是指向对象的指针。 “”(赋值号)是将右侧对象的内…

网络编程与netty

目录 NIO 网络编程Buffer&#xff08;缓冲区&#xff09;Channel&#xff08;通道&#xff09;Selector&#xff08;选择器&#xff09;SelectionKey 零拷贝原生NIO存在的问题 线程模型传统阻塞 I/O 服务模型Reactor 模式单 Reactor 单线程单 Reactor 多线程主从 Reactor 多线程…

RabbitMQ养成记 (3.MQ的简单工作模式 和 Pub/sub 订阅模式)

上一篇是一个简单的helloworld。 我们直接发直接收 这种是最简单的。 下面我们再来接触更加复杂一点&#xff1a; 简单工作模式 work queues 工作队列模式&#xff1a; 这里注意 这里的消息 对两个消费者 c1 c2来说是竞争关系 而不是等份分发关系&#xff0c; 就像两个线程…

[山海关crypto 训练营 day10]

日常鼓励自己&#xff1a;别抱怨努力的苦&#xff0c;那是你去看世界的路。 最近几天一直忙着项目的结项答辩&#xff0c;今天终于是搞完了&#xff0c;得到了老师们的一致好评&#xff0c;最近几天的努力也没白费&#xff01;现在可以愉快刷题了&#xff0c;先复现下LiteCtf的…

c++核心知识—文件操作

目录 一、文件操作 1、文本文件 2、二进制文件 一、文件操作 文件操作头文件&#xff1a;<fstream> 操作文件的三大流&#xff1a; 1、ofstream&#xff1a;写操作 2、ifstream&#xff1a;读操作 3、fstream&#xff1a;读写操作 1、文本文件 写文件 步骤&…

【数据结构】-学习链表所需要的预备知识

知识点收集于网络&#xff0c;我会加以总结&#xff0c;如果把预备知识学好了。那么后面的操作就不难了 用节或者结都可以&#xff0c;不要在意字的差别 目录 一、头指针与头结点的概念 二、链表带头结点和不带头节点的区别 三、八大链表类型&#xff1a; 四、链表节点为…