- Published on
基本问题
defer执行顺序
后进先出(LIFO)的顺序执行,即最后一个defer语句会首先执行,第一个defer语句最后执行。
func main() { defer fmt.Println("first") defer fmt.Println("second") defer fmt.Println("third") }
依次打印
third second first
defer不会执行的情况
在Go语言中,defer语句会将其后的函数调用推迟到包含该defer语句的函数返回之前执行。如果一个函数中有多个defer语句,它们会以后进先出(LIFO)的顺序执行,即最后一个defer语句会首先执行,第一个defer语句最后执行。
多个defer的执行顺序
考虑以下示例:
func main() { defer fmt.Println("first") defer fmt.Println("second") defer fmt.Println("third") }
在这个例子中,输出将会是:
third second first
这展示了defer语句的LIFO执行顺序。
defer不会执行的情况
虽然defer语句是非常可靠的,但在某些情况下,defer可能不会执行:
程序异常退出:如果Go程序因为一个未被捕获的panic而异常退出,那么在panic发生后声明的
defer语句将不会被执行。然而,已经声明的defer语句在panic向上传递到它们的函数时仍然会执行。使用
os.Exit退出:调用os.Exit会立即终止程序,不会执行任何defer语句。所在的协程(goroutine)发生panic且未恢复(recover):如果一个协程发生panic,并且这个panic没有被恢复(即没有使用
recover),那么这个协程中的defer语句可能不会全部执行。defer语句后的函数调用发生panic:如果defer后跟随的函数调用本身发生panic,且未被捕获,那么后续的defer语句也不会执行。
defer和panic
func main1() { // 这个defer不会被执行,因为它在panic之后声明 panic("something bad happened") defer fmt.Println("This defer won't be executed.") } func main2() { // defer会执行 因为在panic之前 defer fmt.Println("This defer will be executed, even after a panic.") panic("something bad happened") } func main() { // 只有在defer函数内部调用recover才能捕获到panic defer func() { if r := recover(); r != nil { fmt.Println("Recovered in defer:", r) } }() fmt.Println("Calling panic...") panic("something bad happened") // 注意,这里的defer不会执行,因为它在panic之后声明。 defer fmt.Println("This defer won't be executed.") }
执行结果如下:
Calling panic... Recovered in defer: something bad happened
这表明,通过在defer函数中恢复panic,程序可以避免因为panic而直接崩溃。然而,如果在panic发生之后再声明defer,这个新的defer是不会被执行的,因为程序的控制流在遇到panic时已经被改变了。
**要注意的是,recover只有在defer函数中调用时才有效。如果你在没有发生panic的正常执行路径中调用recover,它将不会有任何作用。此外,一旦使用recover处理了panic,程序将继续在panic调用之后的点继续执行,但这是指在相同的函数内,如果panic发生在defer之后,则那些defer仍然不会执行。 **
buf[:]的意思
func process(conn net.Conn) { defer conn.Close() // 关闭连接 for { reader := bufio.NewReader(conn) // 自己定义的固定缓冲区 var buf [128]byte n, err := reader.Read(buf[:]) // 读取数据 if err != nil { fmt.Println("read from client failed, err:", err) break } recvStr := string(buf[:n]) fmt.Println("收到client端发来的数据:", recvStr) conn.Write([]byte(recvStr)) // 发送数据 } } func main() { listen, err := net.Listen("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("listen failed, err:", err) return } for { conn, err := listen.Accept() // 建立连接 if err != nil { fmt.Println("accept failed, err:", err) continue } go process(conn) // 启动一个goroutine处理连接 } }。
buf[:]的解释
buf[:]用于创建一个从buf的首字节到最后一个字节的切片。buf是一个固定大小的数组,这里是[128]byte类型,表示它可以容纳 128 个字节。buf[:]实际上是一个切片操作。它从数组buf创建一个新的切片,这个切片引用了相同的底层数组。这意味着通过这个切片对底层数组所做的任何更改都会反映在原始数组buf上。- 在
n, err := reader.Read(buf[:])行,buf[:]被传递给Read方法。这意味着Read方法会从连接中读取数据,最多填充这个切片的长度,即 128 字节。
超过 128 字节的情况
- 如果从
conn读取的数据超过 128 字节,Read方法只会读取前 128 字节的数据到buf数组中。剩余的数据仍然保留在内部的网络缓冲区中,等待下一次读取。 - 在每次循环迭代中,最多只能读取 128 字节。如果有更多的数据,它们将在后续的迭代中被读取。
- 这意味着如果客户端发送一个超过 128 字节的消息,该消息将被分割成多个部分,每部分最多 128 字节,在多次迭代中被逐渐读取并处理。
自定义固定缓存区和系统输入输出网络缓存区
上面的buf数组即是自定义固定缓存区
输出网络缓存区
调用 conn.Write([]byte(recvStr)) 时,数据首先被复制到操作系统的发送缓冲区。这个缓冲区是由操作系统管理的,用于处理网络I/O。这个缓冲区的存在意味着数据不是立即发送到网络的。操作系统会根据网络状况、缓冲区的填充情况以及其他因素来决定何时实际发送数据。
网络延迟和吞吐量会影响数据包的发送。例如,如果网络延迟较高,操作系统可能会倾向于等待更多数据到达缓冲区后再发送,以减少总体延迟。
输入网络缓存区
当数据到达您的服务器时,它们首先被存放在接收缓冲区中,然后您的程序可以从这个缓冲区中读取数据。
数据的即时性
在某些情况下,如果发送缓冲区不是满的,数据可能会被相对快速地发送出去,但这并不是保证的。 在高性能或实时性要求较高的应用中,开发人员可能需要调用特定的系统调用(如在某些系统中的tcp_nodelay选项),来影响数据包的发送策略,例如减少延迟。
io.EOF
在 Go 语言中,io.EOF 是一个特殊的错误值,用于表示文件或流的结尾(End Of File)。这个值在 io 包中定义,通常在处理文件读取、网络数据读取等场合遇到。它是用来指示没有更多的数据可以读取了,通常并不是一个真正的“错误”,而是一个正常的信号,表示数据读取已经完成。
在网络编程中的使用
在处理网络连接时,当你从连接中读取数据,io.EOF 会在没有更多数据可读时返回。例如,在你的 process 函数中,如果客户端关闭了连接,reader.Read 方法将返回 io.EOF。这通常是处理网络连接中断或结束的一个重要信号。
示例
n, err := reader.Read(buf) if err != nil { if err == io.EOF { // 处理文件或流的结尾 break // 或者进行其他适当的处理 } else { // 处理其他错误 } }
关键点
io.EOF不是一个表示故障的错误,而是表示没有更多数据可以读取的状态。- 正确处理
io.EOF对于确保程序正确理解输入结束非常重要,尤其是在文件读取或网络通信中。 - 在很多场合,遇到
io.EOF是循环读取的终止条件,表示数据传输或文件读取已经完成。
go哪些是引用类型
在 Go 语言中,有几种类型通常被认为是引用类型。这意味着当这些类型的变量被赋值给另一个变量时,新变量实际上是指向原始数据的引用,而不是数据的副本。以下是 Go 语言中的主要引用类型:
切片(Slices):切片是对数组的引用,它提供了一个动态大小的、灵活的视图。对切片的任何修改都会影响其底层数组。
映射(Maps):映射是存储键值对的数据结构,其中键是唯一的。映射内部使用哈希表实现,赋值和传递映射变量实际上是在传递对映射数据结构的引用。
通道(Channels):用于在不同的 Goroutines 之间进行通信。当通道被传递时,传递的是对原始通道的引用。
接口(Interfaces):接口类型是一组方法的集合。一个变量如果是接口类型,那么它实际上是指向满足这个接口的具体类型和值的引用。
函数类型(Function types):在 Go 中,函数也可以被视为引用类型。函数变量实际上是对函数实现的引用。
注意事项
尽管这些类型在技术上是引用类型,但 Go 语言并没有像一些其他语言那样显式使用指针。相反,这些引用类型的行为更像是内置的指针,但在使用上更为简便。
指针(Pointers) 本身不是引用类型,但它们存储了对其他变量的直接引用。在 Go 中,你可以创建任何类型的指针,这使得即使是值类型(如 int 或 struct)也可以通过引用传递。
虽然这些类型被归类为引用类型,但它们的赋值和传递行为在一些情况下仍然需要谨慎处理,特别是涉及到并发和数据共享的时候。
总的来说,理解 Go 中的引用类型和它们的行为对于编写高效且正确的程序非常重要。
make和new的区别
在 Go 语言中,make 和 new 是两个用于分配和初始化内存的内置函数,但它们在使用上有一些重要的区别:
make函数:用于初始化内建的引用类型,即切片(slice)、映射(map)和通道(channel)。当你使用make(chan os.Signal, 1)时,它创建了一个实际的通道实例并返回一个通道类型的引用(即一个指向该通道的指针)。这个通道是已经初始化好的,可以直接使用。new函数:用于分配内存,对于任何类型T,new(T)分配类型T的零值内存并返回一个指向它的指针,即*T类型的值。如果你使用new(chan os.Signal),它会返回一个指向chan os.Signal的指针,这个指针指向的通道是 nil,也就是说它没有指向任何实际的通道实例。你需要在使用之前对它进行初始化。
在处理通道的情况下,通常推荐使用 make 而不是 new,因为 make 会返回一个已经初始化好的、可以直接使用的通道,而 new 返回的是指向 nil 通道的指针,这通常不是你想要的。
例如:
// 使用 make 创建和初始化通道 stopChan := make(chan os.Signal, 1) // 使用 new 会创建一个指向 nil 通道的指针 // 这通常不是你想要的,因为你需要一个已经初始化好的通道 stopChanPtr := new(chan os.Signal)
在上面的第二种情况中,如果你尝试通过 stopChanPtr 发送或接收数据,将会导致运行时错误,因为 stopChanPtr 指向的通道是 nil。因此,对于创建通道,推荐使用 make。
new在什么时候使用
在 Go 语言中,new 函数的使用场景相对较少,主要是因为 Go 提供了更方便的方式来初始化变量。不过,还是有一些情况下 new 会被使用:
当你需要一个指向类型零值的指针时:
new(T)创建一个类型为T的新项,初始化为T的零值,并返回其地址,即一个*T类型的值。这在你需要一个指向某类型零值的指针,但又不需要立即分配或初始化更复杂的数据时是有用的。编写泛型代码时:在某些泛型编程场景中,你可能不知道具体的类型,但需要一个指向该类型的指针。在这种情况下,
new可以用来创建这样的指针。当显式指针操作更清晰时:尽管在 Go 中直接使用值类型(如结构体)是常见的,有时显式地处理指针(如通过
new创建)可以使代码的意图更加明确,尤其是在涉及到共享数据或优化性能(例如减少大型结构体的复制)的情况下。
示例
假设你有一个结构体 MyStruct,以下是使用 new 的一个例子:
type MyStruct struct { Field1 int Field2 string } // 使用 new 创建 MyStruct 的指针 ptr := new(MyStruct) // 此时 ptr 是指向 MyStruct 的指针,MyStruct 的字段都被初始化为零值
在这个例子中,ptr 指向一个新分配的 MyStruct,其字段 Field1 和 Field2 被初始化为它们各自类型的零值(0 和 "")。
对比 new 和复合字面量
在 Go 中,更常见的做法是使用复合字面量(结构体字面量)来创建并初始化结构体,如下所示:
// 直接初始化 var s MyStruct // 使用复合字面量创建结构体的指针 ptr := &MyStruct{}
这种方法更加直观,并且允许你在创建时就初始化结构体的字段。因此,除非你有特定的理由需要使用 new,否则通常推荐使用复合字面量语法。
channel关闭判断方法
关闭channel会自动发送一个零值到channel
使用ok判断
val, ok := <-ch
ok为false时channel关闭,此时val为零值
range
使用Range后会循环到channel关闭,但无法接受到关闭channel后自动发送的零值。
func getC(ch chan struct{}) { // range只循环一次 for v := range ch { fmt.Println(v) wg.Done() } // 关闭后还有一个零值 fmt.Println(<-ch) wg.Done() } func main() { wg.Add(2) ch := make(chan struct{}) go getC(ch) ch <- struct{}{} close(ch) wg.Wait() }
r.ParseMultipartForm内存限制大小
r表示httpFunc的request
r.ParseMultipartForm(10 << 20) 不能直接限制上传文件的大小。这个函数调用设定的是服务器处理multipart/form-data请求时允许存储在内存中的最大数据量。当请求中的文件和表单数据的总大小超过这个限制时,多余的数据会自动被写入到服务器的临时文件系统中,而不是被拒绝或导致错误。
这个“超出写入临时文件”的逻辑是由Go的http包内部实现的。当你调用ParseMultipartForm方法时,这个方法会检查请求体的大小。如果请求体的内容(即上传的文件和其他表单数据)超过你指定的内存限制(在这个例案中是10MB),那么超出部分的数据会被存储到服务器的临时文件中。
这个过程是自动进行的,无需开发者手动编写任何额外的代码来处理超出限制的数据。Go的标准库会负责管理这些临时文件,包括在适当的时候删除它们,以避免占用过多的磁盘空间。