跳到主要内容

91 篇博文 含有标签「后端」

后端开发技术文章

查看所有标签

Go 切片底层数组的重新分配情况与示例

· 阅读需 2 分钟
素明诚
Full stack development

扩容操作

当使用 append 向切片追加元素,且追加后的总元素数量超出了切片的原始容量时,Go 会自动重新分配一个更大的底层数组。

slice := []int{1, 2, 3}
slice = append(slice, 4, 5, 6, 7) // 原始容量可能不足以存放所有元素,触发重新分配

从 nil 切片

nil 切片进行 appendnil 切片进行 append 操作时,由于 nil 切片没有底层数组,因此会分配一个新的数组。

var slice []int
slice = append(slice, 1) // 从 nil 切片开始,分配新的底层数组

切片截取后再扩容

如果对一个切片进行截取操作,然后基于这个截取的结果进行 append 操作,并且所需容量超过了截取切片的容量,这同样会触发重新分配。

original := []int{1, 2, 3, 4, 5}
subSlice := original[:2] // 容量仍然是 5
subSlice = append(subSlice, 6, 7, 8, 9, 10, 11) // 容量超载,需要重新分配

显式使用 make 指定初始容量

当使用 make 创建切片时,可以指定一个初始容量,这种方式创建的切片将拥有独立的底层数组。

slice := make([]int, 0, 10)  // 创建一个初始容量为 10 的空切片

通过 copy 创建新切片

使用 copy 函数可以创建一个完全独立的切片副本,该副本将拥有自己的底层数组。

original := []int{1, 2, 3}
clone := make([]int, len(original))
copy(clone, original) // clone 现在有了一个完全独立的底层数组

求赞~

Go 命名返回值和非命名返回值在使用 defer 时的不同表现

· 阅读需 2 分钟
素明诚
Full stack development

使用命名返回值

通过 defer 修改这个返回值。

package main

import "fmt"

func modifyWithNamedReturn() (result int) {
defer func() {
result += 5 // 修改命名返回值
}()

return 10 // 初始设置返回值为 10
}

func main() {
fmt.Println(modifyWithNamedReturn()) // 输出 15
}

解释

  • 这个函数 modifyWithNamedReturn 中有一个命名返回值 result
  • 函数返回时,首先设置 result10
  • 在函数实际返回前,defer 语句执行,将 result 的值增加 5
  • 因此,虽然 return 语句最初将返回值设置为 10defer 修改后,最终返回值变为 15

使用非命名返回值

defer 如何无法修改返回值。

package main

import "fmt"

func modifyWithoutNamedReturn() int {
var result int

defer func() {
result += 5 // 尝试修改,但不影响返回值
}()
result = 10 // 设置 result 的值

return result // 返回 result 的值
}

func main() {
fmt.Println(modifyWithoutNamedReturn()) // 输出 10
}

解释

  • 函数 modifyWithoutNamedReturn 返回一个非命名返回值。
  • result 被设置为 10,并准备用作返回值。
  • 尽管 defer 函数将 result 增加了 5,但这种修改发生在返回值被确定(已复制)后,因此对实际返回给调用者的值没有影响。
  • 最终输出为 10,展示了即使 defer 修改了局部变量 result,也不会改变返回值。

defer 使用总结

defer 在命名返回值情况下可以修改最终的返回值,因为命名返回值的作用域延伸至整个函数,包括 defer 执行的时间。而在非命名返回值的情况下,return 语句实际上是将值复制到返回位置,此后的 defer 修改不再影响已经设置的返回值。

简单点可以这样理解,命名返回值,说明返回值很重要,要都处理完再返回~

Go 语言中的作用域

· 阅读需 2 分钟
素明诚
Full stack development

宇宙块(Universe block)

宇宙块是 Go 语言中最顶层的作用域,包含了所有内置的类型和函数。

package main

import "fmt"

func main() {
var num int = len("Hello") // 使用内置函数 len
fmt.Println(num) // 使用内置包 fmt
}

在这个例子中,lenfmt 都是宇宙块的一部分,它们在任何 Go 程序中都是可用的。

包块(Package block)

包块包含了一个包内的所有 Go 源代码文本,其内定义的变量在整个包内可见。

// 文件 1: math.go
package math

var Factor int = 2

// 文件 2: double.go
package math

func Double(n int) int {
return n * Factor // 可以访问同一个包内的其他文件中定义的变量 Factor
}

在这个例子中,Factormath 包的包块中定义,可以在同一个包内的其他文件中使用。

文件块(File block)

文件块是仅限于单个文件的作用域。

// 文件: util.go
package util

var helper = "hidden" // 只在 util.go 文件中可见

func Help() string {
return helper // 只有在同一个文件中定义的函数可以访问 helper
}

这里的变量 helper 仅在定义它的文件 util.go 内可见。

"if", "for", 和 "switch" 语句块(Implicit block)

在这些控制流语句中定义的变量仅在语句块内部有效。

package main

import "fmt"

func main() {
if x := 10; x > 5 {
fmt.Println(x) // x 在这个 if 块中有效
}
// fmt.Println(x) // 这里访问 x 会报错,因为 x 的作用域仅在 if 块内
}

"switch" 或 "select" 语句中的每个子句(Implicit block)

在每个 switchselect 的子句中定义的变量仅在该子句内有效。

 package main

import "fmt"

func main() {
switch num := 5; {
case num < 10:
fmt.Println(num) // num 在这个 case 块中有效
}
// fmt.Println(num) // 这里访问 num 会报错,因为 num 的作用域仅在 switch 的 case 块内
}

Go make 和 new 的区别

· 阅读需 2 分钟
素明诚
Full stack development

对比 new 和 make

对比项newmake
函数功能分配内存并返回指向零值的指针分配内存并返回初始化的引用类型实例
返回类型指针引用类型实例(非指针)
使用类型所有类型切片、映射、通道
初始化内存初始化为零值内存初始化为特定的非零值(如切片的结构信息)
场景需要类型零值的指针时使用需要立即使用的切片、映射或通道

new 函数适用于所有类型,它分配内存并返回一个指向零值的指针。它适用于你只需要一个指向某类型零值的指针的场景。

make 函数专用于切片、映射和通道这些内建的引用类型,它不仅分配内存,还负责初始化这些类型的内部数据结构,使得实例可以立即使用。

理解零值

比如说,如果你执行 ptr := new(int),ptr 是一个指向 int 类型的指针

  • ptr 指向的内存区域包含的 int 值被初始化为 0(int 类型的零值)。
  • 因此,*ptr == 0 将返回 true,因为 ptr 指向的 int 值是其零值。

如果是 ptrStruct := new(MyStruct),其中 MyStruct 是一个包含多个字段的结构体

  • 每个字段都会被初始化为相应类型的零值。如果有整型字段,它们会被设置为 0;如果有字符串字段,它们会被设置为空字符串 "";如果有指针字段,它们会被设置为 nil。
  • 所以,ptrStruct 指向的 MyStruct 实例在所有字段上都体现了各自类型的零值。

注意事项

new 的使用

  • 零值误解:新手可能会误解 new 创建的是已初始化的对象,尤其是复杂的结构体。
  • 引用类型字段:new 创建的结构体中的切片、映射或通道字段默认为 nil,需要额外的初始化步骤。

make 的使用

  • 类型限制:make 仅适用于切片、映射和通道。
  • 初始化细节:容易混淆切片的容量和长度,可能导致内存浪费或逻辑错误。

Go println 和 fmtPrintln 对比

· 阅读需 2 分钟
素明诚
Full stack development
特性println()fmt.Println()
来源Go 语言的内建函数,无需导入需要导入 fmt 包
用途主要用于开发阶段的快速调试适用于生产环境,支持复杂的输出需求
格式化能力无法自定义输出格式支持广泛的格式化选项,如数字格式、宽度、精度等
输出控制自动在输出元素之间添加空格,结尾自动换行自动在输出元素之间添加空格,结尾自动换行,格式化更灵活
性能通常较快但差异通常不显著性能优秀,适用于高频输出场景
依赖性不依赖任何外部包,始终可用依赖于 fmt 包,需要导入使用
输出示例println("Hello", "world") 输出:Hello worldfmt.Println("Hello", "world") 输出:Hello world
适用场景简单脚本、学习阶段、快速调试生产代码、需要格式化输出的场景、日志记录等
控制台交互较为简单,无法处理复杂的用户输出交互可以与 fmt.Scanln() 等输入函数配合使用,适合复杂的控制台交互
推荐使用开发早期、调试、教学示例生产环境、需要格式化或多样化输出的应用

格式化输出

print

func main() {
name := "Alice"
age := 30
println("Name:", name, "Age:", age)
}

Println

func main() {
name := "Alice"
age := 30
fmt.Printf("Name: %s, Age: %d\n", name, age)
}

使用切片和复杂数据结构

package main

import "fmt"

func main() {
numbers := []int{1, 2, 3}
// 快速检查切片的地址或确认切片非空。
println("Numbers:", numbers) // Numbers: [3/3]0xc000010108
// 能够直接输出切片内容及其结构,适合在处理数据和调试复杂结构时使用。
fmt.Println("Numbers:", numbers) // Numbers: [1 2 3]
}

Go 五种字符串连接方式

· 阅读需 1 分钟
素明诚
Full stack development
package main

import (
"bytes"
"fmt"
"strings"
)

// 使用 + 连接字符串
func joinWithPlus(s1, s2 string) string {
return s1 + s2
}

// 使用 fmt.Sprintf 连接字符串
func joinWithSprintf(s1, s2 string) string {
return fmt.Sprintf("%s%s", s1, s2)
}

// 使用 strings.Builder 连接字符串
func joinWithBuilder(s1, s2 string) string {
var builder strings.Builder
builder.WriteString(s1)
builder.WriteString(s2)
return builder.String()
}

// 使用 strings.Join 连接字符串切片
func joinWithJoin(
elements []string,
sep string,
) string {
return strings.Join(elements, sep)
}

// 使用 bytes.Buffer 连接字符串
func joinWithBuffer(s1, s2 string) string {
var buffer bytes.Buffer
buffer.WriteString(s1)
buffer.WriteString(s2)
return buffer.String()
}

func main() {
s1 := "Hello, "
s2 := "world!"
elements := []string{s1, s2}

// 使用 + 连接
// 小需求场景下使用 + 连接字符串
fmt.Println("Join with +:", joinWithPlus(s1, s2))

// 使用 fmt.Sprintf 连接
fmt.Println("Join with fmt.Sprintf:", joinWithSprintf(s1, s2))

// 使用 strings.Builder 连接
// 此方法最高效 但是需要 Go 1.10 以上版本 是专门为高效构建和连接大量字符串而设计的
fmt.Println("Join with strings.Builder:", joinWithBuilder(s1, s2))

// 使用 strings.Join 连接
fmt.Println("Join with strings.Join:", joinWithJoin(elements, ""))

// 使用 bytes.Buffer 连接
fmt.Println("Join with bytes.Buffer:", joinWithBuffer(s1, s2))
}

Go 内建函数

· 阅读需 1 分钟
素明诚
Full stack development

内建函数(built-in functions)是指在编程语言中预定义的、可以直接使用而不需要额外导入库或模块的函数。这些函数是编程语言的一部分,通常提供基本的或最常用的功能,以方便开发者在不引入外部依赖的情况下完成基础或核心的任务。

常见内建函数

类型操作:比如 int()float64() 用于类型转换,type 用于类型断言。

处理数据len() 可获取切片、映射、字符串、数组或通道的长度,cap() 查看切片或数组的容量。另外,append()copy() 分别用于向切片添加元素和复制切片内容。

创建和初始化make()new() 分别用于初始化切片、映射、通道,以及分配内存。

复杂数据操作delete() 可从映射中删除键值对。

并发控制go 关键字用来启动新的协程,而 select 处理多个通道的接收操作。

错误处理panic() 用于触发运行时错误,而 recover() 可以从这种错误中恢复。

Go 切片操作

· 阅读需 3 分钟
素明诚
Full stack development

切片的基本概念

切片是 Go 语言中一个动态数组的引用类型,提供了一种灵活、高效的方式来处理数据集合。它不存储任何数据,只是对底层数组的一个连续片段的描述,包括三个部分 指向数组的指针、切片的长度和容量。

切片创建和索引

两个参数的切片(arr[start:end])

当使用两个参数对数组或已存在的切片进行切片操作时

  • 长度(len)end - start 计算得出,表示切片覆盖的元素数量。
  • 容量(cap) 从切片的起始索引到原数组或原切片末尾的元素数量。计算方式是原数组或切片的长度减去切片的起始索引。
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3]
// s 包含元素 [2, 3]
// len(s) == 2
// cap(s) == 4 (从索引1到数组末尾)

三个参数的切片(arr[start:end:cap])

使用三个参数进行切片操作可以更精确地控制切片的容量

  • 第三个参数(容量上限) 定义了切片从起始索引开始能扩展到的数组或切片的最远边界的索引。
  • 切片的实际容量由 cap - start 计算得出,但不能超过从起始索引到原数组或原切片末尾的长度。
arr := [5]int{1, 2, 3, 4, 5}
t := arr[1:3:4]
// t 包含元素 [2, 3]
// len(t) == 2
// cap(t) == 3 (从索引1开始,可以扩展到索引4之前)

切片的行为

  • 不修改原始数组 通过切片操作不会修改底层数组的内容,切片仅作为对数组某部分的引用。
  • 切片的修改影响数组 如果通过切片修改了元素,同样的修改会反映在底层数组上。
  • 切片扩展 切片可以通过 append() 函数进行扩展,但是如果扩展后的长度超过了切片的容量,Go 语言运行时会分配一个新的数组来存储数据,原有数组的内容会被复制到新数组中。

使用场景和注意事项

  • 使用两个参数的切片适用于简单的数据访问和部分修改,而三个参数的切片适用于需要严格控制切片容量和安全性的场景,如并发编程中防止切片操作影响到原数组的其他部分。
  • 切片的容量控制尤其重要,因为它影响着切片的扩展能力和性能表现。

数据存储的基本单位

· 阅读需 2 分钟
素明诚
Full stack development

使用 Markdown 表格模拟的示例,更直观地理解这些单位之间的关系

  • 位(bit): 是计算机内存中的最小数据单位。每个位的值只能是 0 或 1。
  • 字节(Byte): 1 字节 = 8 位。字节是计算机处理数据的基本单位之一,通常用来表示一个字符,例如,'A' 或 '0'。
  • 千字节(KB,Kilobyte): 1KB = 1024 字节。在数据存储中,KB 通常用来衡量文件大小或者数据传输的量。
  • 位(b): 与“位(bit)”相同,只是表示方法不同,都是指计算机数据的最小单位。

假设每个格子代表一个位(bit/b),那么 8 个格子组成一个字节(Byte)。这里是一个简化的视图

| bit  | bit  | bit  | bit  | bit  | bit  | bit  | bit  |
|------|------|------|------|------|------|------|------|
| 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | <- 一个字节(Byte)的示例

这个例子中,一个字节(Byte)由 8 个位(bit)组成,每个位的值可以是 0 或 1。因此,一个字节可以表示 2^8=256 种不同的状态,从 00000000 到 11111111。

进一步扩展,如果我们将这样的 1024 个字节串联起来,就构成了 1 千字节(KB)。因为直接在表格中展示 1024 个字节(即 8,192 个位)非常不实际,所以这里用文字描述

常见字符大小

类型示例编码格式大致占用字节数
英语字符AASCII1 字节
汉字UTF-83 字节
Emoji 表情UTF-84 字节

如果您喜欢这篇文章,不妨给它点个赞并收藏,感谢您的支持!

认证和授权的区别

· 阅读需 2 分钟
素明诚
Full stack development

认证(Authentication)

认证是指验证用户或系统的身份。这是一个确认“你是谁”的过程,确保用户或系统是他们声称的那个人或那个实体。常见的认证方式包括用户名和密码、数字证书、双因素认证(2FA)等。简单来说,认证过程就是确定用户的身份。

授权(Authorization)

授权发生在认证之后,是指一旦用户的身份得到确认,系统决定用户可以访问的资源或可以执行的操作的过程。授权回答的问题是“你能做什么”或“你有权限访问哪些资源”。例如,一个系统可能允许某些用户访问敏感信息,而禁止其他用户访问。

认证与授权的区别

目的不同,认证的目的是验证用户身份,而授权的目的是确定用户能做什么。

时机不同,认证通常是用户交互的首个步骤,只有通过认证后,系统才会考虑授权问题。

方法不同,认证方法关注于证明身份(如密码、指纹、令牌),授权方法关注于权限检查(如访问控制列表、角色、策略)。

在开发中

认证和授权通常需要分开处理。这是因为它们分别涉及到安全机制的不同方面。通常,应用程序会首先通过认证流程确认用户身份,然后根据授权策略来确定用户可以访问的资源或执行的操作。

认证,可能涉及到登录表单、会话管理、用户数据库以及可能的第三方认证服务(如 OAuth、OpenID Connect)。

授权,通常需要实现访问控制逻辑,可能包括角色基础的访问控制(RBAC)、属性基础的访问控制(ABAC)或更复杂的策略决策点(PDP)。