2025/3/19
This commit is contained in:
commit
abb37c49a7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.obsidian
|
7
技术/Go/八股.md
Normal file
7
技术/Go/八股.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
## Go语言中如何访问私有成员
|
||||||
|
在Go语言中,以小写字母开头的标识符是私有成员,私有成员(字段、方法、函数等)遵循语言的可见性规则,仅在定义它的包内可见,包外无法访问这些私有成员。如果想要访问私有成员,主要包括以下三种方式:
|
||||||
|
- 在同一个包内,可以直接访问**小写字母**开头的私有成员。
|
||||||
|
- 在其他包中,无法直接访问私有成员,但可以通过公开的**接口**来间接访问私有成员。
|
||||||
|
- 使用**反射**来绕过Go语言的封装机制访问和修改私有字段。(**不建议使用**)
|
||||||
|
|
||||||
|
---
|
142
技术/Go/常用包/fmt.md
Normal file
142
技术/Go/常用包/fmt.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
在 Go 语言中,`fmt` 包是用于格式化输入和输出的核心工具包,提供了许多常用的函数来处理标准输入、输出以及字符串的格式化操作。以下是一些常用的 `fmt` 包函数及其使用方法:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1. 格式化输出**
|
||||||
|
- **`Print(a ...interface{}) (n int, err error)`**
|
||||||
|
将参数直接输出到标准输出(通常是终端),非字符串参数之间会添加空格,但不会自动换行。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
fmt.Print("Hello", "World") // 输出: HelloWorld
|
||||||
|
fmt.Print("Hello", " ", "World") // 输出: Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`Printf(format string, a ...interface{}) (n int, err error)`**
|
||||||
|
根据格式化字符串 `format` 输出内容,支持丰富的格式化占位符(如 `%d`、`%s` 等)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
name := "Alice"
|
||||||
|
age := 30
|
||||||
|
fmt.Printf("Name: %s, Age: %d\n", name, age) // 输出: Name: Alice, Age: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`Println(a ...interface{}) (n int, err error)`**
|
||||||
|
类似于 `Print`,但在输出末尾自动添加换行符,并且每个参数之间会自动加空格。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
fmt.Println("Hello", "World") // 输出: Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. 格式化生成字符串**
|
||||||
|
- **`Sprint(a ...interface{}) string`**
|
||||||
|
返回格式化后的字符串,但不直接输出到标准输出。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := fmt.Sprint("Hello", " ", "World")
|
||||||
|
fmt.Println(str) // 输出: Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Sprintf(format string, a ...interface{}) string`**
|
||||||
|
根据格式化字符串生成一个新的字符串,但不直接输出到标准输出。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
name := "Bob"
|
||||||
|
age := 25
|
||||||
|
str := fmt.Sprintf("Name: %s, Age: %d", name, age)
|
||||||
|
fmt.Println(str) // 输出: Name: Bob, Age: 25
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Sprintln(a ...interface{}) string`**
|
||||||
|
类似于 `Sprint`,但会在末尾添加换行符。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := fmt.Sprintln("Hello", "World")
|
||||||
|
fmt.Print(str) // 输出: Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. 格式化写入文件或缓冲区**
|
||||||
|
- **`Fprint(w io.Writer, a ...interface{}) (n int, err error)`**
|
||||||
|
将参数写入到指定的 `io.Writer`(如文件或缓冲区)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprint(&buf, "Hello", " ", "World")
|
||||||
|
fmt.Println(buf.String()) // 输出: Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)`**
|
||||||
|
根据格式化字符串将内容写入到指定的 `io.Writer`。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "Name: %s, Age: %d", "Alice", 30)
|
||||||
|
fmt.Println(buf.String()) // 输出: Name: Alice, Age: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Fprintln(w io.Writer, a ...interface{}) (n int, err error)`**
|
||||||
|
类似于 `Fprint`,但在末尾添加换行符。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintln(&buf, "Hello", "World")
|
||||||
|
fmt.Println(buf.String()) // 输出: Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. 格式化输入**
|
||||||
|
- **`Scan(a ...interface{}) (n int, err error)`**
|
||||||
|
从标准输入读取数据并存储到变量中,按空格分隔输入值。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
var name string
|
||||||
|
var age int
|
||||||
|
fmt.Print("请输入姓名和年龄:")
|
||||||
|
fmt.Scan(&name, &age)
|
||||||
|
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Scanf(format string, a ...interface{}) (n int, err error)`**
|
||||||
|
根据格式化字符串从标准输入读取数据。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
var name string
|
||||||
|
var age int
|
||||||
|
fmt.Print("请输入姓名和年龄(格式:姓名 年龄):")
|
||||||
|
fmt.Scanf("%s %d", &name, &age)
|
||||||
|
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Scanln(a ...interface{}) (n int, err error)`**
|
||||||
|
类似于 `Scan`,但会以换行符结束输入。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
var name string
|
||||||
|
var age int
|
||||||
|
fmt.Print("请输入姓名和年龄:")
|
||||||
|
fmt.Scanln(&name, &age)
|
||||||
|
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **5. 常用格式化占位符**
|
||||||
|
`fmt` 包中的格式化字符串支持多种占位符,以下是一些常用示例:
|
||||||
|
- **`%v`**:打印值的默认格式。
|
||||||
|
- **`%T`**:打印值的类型。
|
||||||
|
- **`%d`**:打印整数(十进制)。
|
||||||
|
- **`%f`**:打印浮点数。
|
||||||
|
- **`%s`**:打印字符串。
|
||||||
|
- **`%t`**:打印布尔值(`true` 或 `false`)。
|
||||||
|
- **`%p`**:打印指针地址。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
fmt.Printf("Value: %v, Type: %T\n", 42, 42) // 输出: Value: 42, Type: int
|
||||||
|
```
|
149
技术/Go/常用包/sort.md
Normal file
149
技术/Go/常用包/sort.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
在 Go 语言中,`sort` 包提供了用于对切片和用户自定义数据集进行排序的功能。以下是一些常用的 `sort` 包函数及其使用方法:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1. 基本类型切片排序**
|
||||||
|
- **`Ints(a []int)`**
|
||||||
|
对整数切片进行升序排序。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
nums := []int{3, 1, 4, 2}
|
||||||
|
sort.Ints(nums)
|
||||||
|
fmt.Println(nums) // 输出: [1 2 3 4]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Float64s(a []float64)`**
|
||||||
|
对浮点数切片进行升序排序。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
nums := []float64{3.5, 1.2, 4.8, 2.1}
|
||||||
|
sort.Float64s(nums)
|
||||||
|
fmt.Println(nums) // 输出: [1.2 2.1 3.5 4.8]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Strings(a []string)`**
|
||||||
|
对字符串切片进行升序排序(按字典顺序)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
strs := []string{"banana", "apple", "cherry"}
|
||||||
|
sort.Strings(strs)
|
||||||
|
fmt.Println(strs) // 输出: [apple banana cherry]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. 判断是否已排序**
|
||||||
|
- **`IntsAreSorted(a []int) bool`**
|
||||||
|
判断整数切片是否已经按升序排序。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
result := sort.IntsAreSorted(nums)
|
||||||
|
fmt.Println(result) // 输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Float64sAreSorted(a []float64) bool`**
|
||||||
|
判断浮点数切片是否已经按升序排序。
|
||||||
|
|
||||||
|
- **`StringsAreSorted(a []string) bool`**
|
||||||
|
判断字符串切片是否已经按字典顺序排序。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. 自定义排序**
|
||||||
|
对于自定义结构体或复杂数据类型的排序,需要实现 `sort.Interface` 接口,该接口包含以下三个方法:
|
||||||
|
- **`Len() int`**:返回集合的长度。
|
||||||
|
- **`Less(i, j int) bool`**:定义排序规则,通常是比较索引 `i` 和 `j` 的元素。
|
||||||
|
- **`Swap(i, j int)`**:交换索引 `i` 和 `j` 的元素。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByAge []Person
|
||||||
|
|
||||||
|
func (a ByAge) Len() int { return len(a) }
|
||||||
|
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
|
||||||
|
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
people := []Person{
|
||||||
|
{"Alice", 30},
|
||||||
|
{"Bob", 25},
|
||||||
|
{"Charlie", 35},
|
||||||
|
}
|
||||||
|
sort.Sort(ByAge(people))
|
||||||
|
fmt.Println(people) // 输出: [{Bob 25} {Alice 30} {Charlie 35}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. 稳定排序**
|
||||||
|
- **`Stable(data Interface)`**
|
||||||
|
使用稳定的排序算法对数据进行排序。稳定排序保证相等元素的相对顺序不变。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
people := []Person{
|
||||||
|
{"Alice", 30},
|
||||||
|
{"Bob", 30},
|
||||||
|
{"Charlie", 25},
|
||||||
|
}
|
||||||
|
sort.Stable(ByAge(people))
|
||||||
|
fmt.Println(people) // 输出: [{Charlie 25} {Alice 30} {Bob 30}]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **5. 查找操作**
|
||||||
|
- **`SearchInts(a []int, x int) int`**
|
||||||
|
在已排序的整数切片中查找值 `x`,返回其索引。如果未找到,则返回应该插入的位置以保持排序。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
nums := []int{1, 3, 5, 7}
|
||||||
|
index := sort.SearchInts(nums, 4)
|
||||||
|
fmt.Println(index) // 输出: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`SearchStrings(a []string, x string) int`**
|
||||||
|
在已排序的字符串切片中查找值 `x`。
|
||||||
|
|
||||||
|
- **`SearchFloat64s(a []float64, x float64) int`**
|
||||||
|
在已排序的浮点数切片中查找值 `x`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **6. 自定义切片排序**
|
||||||
|
- **`Slice(slice interface{}, less func(i, j int) bool)`**
|
||||||
|
对任意类型的切片进行排序,通过传入一个比较函数 `less` 来定义排序规则。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
nums := []int{3, 1, 4, 2}
|
||||||
|
sort.Slice(nums, func(i, j int) bool {
|
||||||
|
return nums[i] > nums[j] // 按降序排序
|
||||||
|
})
|
||||||
|
fmt.Println(nums) // 输出: [4 3 2 1]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **7. 其他功能**
|
||||||
|
- **`Reverse(data Interface)`**
|
||||||
|
对已排序的数据进行逆序排列。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
nums := []int{1, 2, 3, 4}
|
||||||
|
sort.Sort(sort.Reverse(sort.IntSlice(nums)))
|
||||||
|
fmt.Println(nums) // 输出: [4 3 2 1]
|
||||||
|
```
|
129
技术/Go/常用包/strconv.md
Normal file
129
技术/Go/常用包/strconv.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
|
||||||
|
在 Go 语言中,`strconv` 包提供了许多用于基本数据类型和字符串之间相互转换的函数。以下是一些常用的函数及其功能:
|
||||||
|
|
||||||
|
### **1. 字符串与整数之间的转换**
|
||||||
|
- **`Atoi(s string) (int, error)`**
|
||||||
|
将字符串转换为整数(十进制)。如果转换失败,会返回错误。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
num, err := strconv.Atoi("123")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("转换错误:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("转换结果:", num) // 输出: 转换结果: 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`Itoa(i int) string`**
|
||||||
|
将整数转换为字符串。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strconv.Itoa(123)
|
||||||
|
fmt.Println("转换结果:", str) // 输出: 转换结果: "123"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`ParseInt(s string, base int, bitSize int) (i int64, err error)`**
|
||||||
|
将字符串按指定进制和位数转换为整数。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
num, err := strconv.ParseInt("FF", 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("转换错误:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("转换结果:", num) // 输出: 转换结果: 255
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`FormatInt(i int64, base int) string`**
|
||||||
|
将整数按指定进制格式化为字符串。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strconv.FormatInt(255, 16)
|
||||||
|
fmt.Println("转换结果:", str) // 输出: 转换结果: "ff"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **字符串与浮点数之间的转换**
|
||||||
|
- **`ParseFloat(s string, bitSize int) (float64, error)`**
|
||||||
|
将字符串转换为浮点数。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
num, err := strconv.ParseFloat("3.14", 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("转换错误:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("转换结果:", num) // 输出: 转换结果: 3.14
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`FormatFloat(f float64, fmt byte, prec, bitSize int) string`**
|
||||||
|
将浮点数格式化为字符串。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strconv.FormatFloat(3.14159, 'f', 2, 64)
|
||||||
|
fmt.Println("转换结果:", str) // 输出: 转换结果: "3.14"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **字符串与布尔值之间的转换**
|
||||||
|
- **`ParseBool(str string) (bool, error)`**
|
||||||
|
将字符串转换为布尔值。支持的真值包括 `"true"`、`"True"`、`"TRUE"` 等,假值包括 `"false"`、`"False"`、`"FALSE"` 等。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
b, err := strconv.ParseBool("True")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("转换错误:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("转换结果:", b) // 输出: 转换结果: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`FormatBool(b bool) string`**
|
||||||
|
将布尔值转换为字符串(`"true"` 或 `"false"`)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strconv.FormatBool(true)
|
||||||
|
fmt.Println("转换结果:", str) // 输出: 转换结果: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **其他常用函数**
|
||||||
|
- **`Quote(s string) string`**
|
||||||
|
返回一个带有双引号的字符串字面量表示形式。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strconv.Quote("Hello, World!")
|
||||||
|
fmt.Println("转换结果:", str) // 输出: 转换结果: "\"Hello, World!\""
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Unquote(s string) (string, error)`**
|
||||||
|
移除字符串中的双引号,并还原转义字符。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str, err := strconv.Unquote("\"Hello, World!\"")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("转换错误:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("转换结果:", str) // 输出: 转换结果: Hello, World!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Append` 系列函数**
|
||||||
|
这些函数将基本数据类型直接追加到字节切片中,常用于高效的字符串拼接操作。例如:`AppendInt`、`AppendBool`、`AppendFloat` 等。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
b := []byte("数字是: ")
|
||||||
|
b = strconv.AppendInt(b, 123, 10)
|
||||||
|
fmt.Println(string(b)) // 输出: 数字是: 123
|
||||||
|
```
|
||||||
|
> 在 `strconv.AppendInt(b, 123, 10)` 中,第三个参数表示数字的进制(base)。具体来说:
|
||||||
|
> **`10`** 表示将整数 `123` 按照十进制格式追加到字节切片 `b` 中。如果改为其他值,例如 `16`则表示将整数以十六进制格式追加;如果是 `2`,则表示以二进制格式追加。
|
||||||
|
|
169
技术/Go/常用包/strings.md
Normal file
169
技术/Go/常用包/strings.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
在 Go 语言中,`strings` 包提供了许多用于字符串操作的函数,这些函数非常高效且易于使用。以下是一些常用的 `strings` 包函数及其使用方法:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1. 字符串查询**
|
||||||
|
- **`Contains(s, substr string) bool`**
|
||||||
|
判断字符串 `s` 是否包含子串 `substr`。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
result := strings.Contains("Hello, World!", "World")
|
||||||
|
fmt.Println(result) // 输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`ContainsAny(s, chars string) bool`**
|
||||||
|
判断字符串 `s` 是否包含 `chars` 中的任意字符。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
result := strings.ContainsAny("Hello, World!", "aeiou")
|
||||||
|
fmt.Println(result) // 输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Count(s, substr string) int`**
|
||||||
|
统计子串 `substr` 在字符串 `s` 中出现的次数。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
count := strings.Count("banana", "a")
|
||||||
|
fmt.Println(count) // 输出: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. 字符串前缀和后缀**
|
||||||
|
- **`HasPrefix(s, prefix string) bool`**
|
||||||
|
判断字符串 `s` 是否以指定前缀开头。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
result := strings.HasPrefix("Hello, World!", "Hello")
|
||||||
|
fmt.Println(result) // 输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`HasSuffix(s, suffix string) bool`**
|
||||||
|
判断字符串 `s` 是否以指定后缀结尾。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
result := strings.HasSuffix("Hello, World!", "World!")
|
||||||
|
fmt.Println(result) // 输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. 字符串索引**
|
||||||
|
- **`Index(s, substr string) int`**
|
||||||
|
返回子串 `substr` 在字符串 `s` 中首次出现的位置。如果未找到,返回 `-1`。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
index := strings.Index("Hello, World!", "World")
|
||||||
|
fmt.Println(index) // 输出: 7
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`LastIndex(s, substr string) int`**
|
||||||
|
返回子串 `substr` 在字符串 `s` 中最后一次出现的位置。如果未找到,返回 `-1`。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
index := strings.LastIndex("Hello, World, World!", "World")
|
||||||
|
fmt.Println(index) // 输出: 13
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. 字符串替换**
|
||||||
|
- **`Replace(s, old, new string, n int) string`**
|
||||||
|
将字符串 `s` 中的 `old` 替换为 `new`,最多替换 `n` 次。如果 `n` 为 `-1`,则替换所有匹配项。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.Replace("banana", "a", "o", 2)
|
||||||
|
fmt.Println(str) // 输出: bonona
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`ReplaceAll(s, old, new string) string`**
|
||||||
|
替换字符串 `s` 中所有的 `old` 为 `new`。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.ReplaceAll("banana", "a", "o")
|
||||||
|
fmt.Println(str) // 输出: bonono
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **5. 字符串分割与拼接**
|
||||||
|
- **`Split(s, sep string) []string`**
|
||||||
|
按照分隔符 `sep` 将字符串 `s` 分割成一个字符串切片。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
parts := strings.Split("a,b,c", ",")
|
||||||
|
fmt.Println(parts) // 输出: [a b c]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **`Join(elems []string, sep string) string`**
|
||||||
|
将字符串切片 `elems` 使用分隔符 `sep` 拼接成一个字符串。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.Join([]string{"a", "b", "c"}, ",")
|
||||||
|
fmt.Println(str) // 输出: a,b,c
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Fields(s string) []string`**
|
||||||
|
按空白字符(如空格、制表符等)将字符串 `s` 分割成一个字符串切片。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
parts := strings.Fields("a b c")
|
||||||
|
fmt.Println(parts) // 输出: [a b c]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **6. 字符串大小写转换**
|
||||||
|
- **`ToLower(s string) string`**
|
||||||
|
将字符串 `s` 转换为小写形式。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.ToLower("HELLO")
|
||||||
|
fmt.Println(str) // 输出: hello
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`ToUpper(s string) string`**
|
||||||
|
将字符串 `s` 转换为大写形式。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.ToUpper("hello")
|
||||||
|
fmt.Println(str) // 输出: HELLO
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **7. 字符串修剪**
|
||||||
|
- **`Trim(s, cutset string) string`**
|
||||||
|
移除字符串 `s` 开头和结尾的所有出现在 `cutset` 中的字符。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.Trim("!!!Hello, World!!!", "!")
|
||||||
|
fmt.Println(str) // 输出: Hello, World
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`TrimSpace(s string) string`**
|
||||||
|
移除字符串 `s` 开头和结尾的所有空白字符(包括空格、制表符、换行符等)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
str := strings.TrimSpace(" Hello, World! ")
|
||||||
|
fmt.Println(str) // 输出: Hello, World!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **8. 字符串比较**
|
||||||
|
- **`EqualFold(s1, s2 string) bool`**
|
||||||
|
比较两个字符串是否相等(忽略大小写)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
result := strings.EqualFold("GoLang", "golang")
|
||||||
|
fmt.Println(result) // 输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
以上是 `strings` 包中一些常用的函数及其使用方法。这些函数可以帮助开发者轻松处理各种字符串操作需求 .
|
137
技术/Go/常用包/time.md
Normal file
137
技术/Go/常用包/time.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
在 Go 语言中,`time` 包提供了丰富的时间和日期处理功能,广泛应用于时间的获取、格式化、解析以及时间间隔的操作。以下是一些常用的 `time` 包函数及其使用方法:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1. 获取当前时间**
|
||||||
|
- **`Now() Time`**
|
||||||
|
返回当前的本地时间。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
now := time.Now()
|
||||||
|
fmt.Println("当前时间:", now) // 输出: 当前时间: 2025-03-17 14:30:00.xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. 时间的创建**
|
||||||
|
- **`Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time`**
|
||||||
|
根据指定的年、月、日、时、分、秒、纳秒和时区创建一个时间对象。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t := time.Date(2025, time.March, 17, 14, 30, 0, 0, time.Local)
|
||||||
|
fmt.Println("指定时间:", t) // 输出: 指定时间: 2025-03-17 14:30:00 +0800 CST
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Unix(sec int64, nsec int64) Time`**
|
||||||
|
根据 Unix 时间戳(秒数和纳秒数)创建时间对象。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t := time.Unix(1710651000, 0)
|
||||||
|
fmt.Println("Unix时间对应时间:", t) // 输出: Unix时间对应时间: 2025-03-17 14:30:00 +0800 CST
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. 时间格式化与解析**
|
||||||
|
- **`Format(layout string) string`**
|
||||||
|
将时间对象格式化为字符串,`layout` 是格式模板,必须使用固定的参考时间 `2006-01-02 15:04:05` 的形式。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t := time.Now()
|
||||||
|
formatted := t.Format("2006-01-02 15:04:05")
|
||||||
|
fmt.Println("格式化时间:", formatted) // 输出: 格式化时间: 2025-03-17 14:30:00
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Parse(layout, value string) (Time, error)`**
|
||||||
|
将字符串解析为时间对象,`layout` 是格式模板。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t, err := time.Parse("2006-01-02 15:04:05", "2025-03-17 14:30:00")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("解析错误:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("解析时间:", t) // 输出: 解析时间: 2025-03-17 14:30:00 +0000 UTC
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`ParseInLocation(layout, value string, loc *Location) (Time, error)`**
|
||||||
|
类似于 `Parse`,但允许指定时区。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2025-03-17 14:30:00", loc)
|
||||||
|
fmt.Println("解析时间:", t) // 输出: 解析时间: 2025-03-17 14:30:00 +0800 CST
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. 时间计算**
|
||||||
|
- **`Add(d Duration) Time`**
|
||||||
|
在当前时间上增加一个时间间隔(`Duration`)。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t := time.Now()
|
||||||
|
future := t.Add(24 * time.Hour)
|
||||||
|
fmt.Println("一天后的时间:", future)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Sub(u Time) Duration`**
|
||||||
|
计算两个时间之间的差值,返回一个 `Duration`。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t1 := time.Now()
|
||||||
|
t2 := t1.Add(2 * time.Hour)
|
||||||
|
duration := t2.Sub(t1)
|
||||||
|
fmt.Println("时间差:", duration) // 输出: 时间差: 2h0m0s
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Since(t Time) Duration`**
|
||||||
|
返回从指定时间到当前时间的时间间隔。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
t := time.Date(2025, time.March, 16, 14, 30, 0, 0, time.Local)
|
||||||
|
duration := time.Since(t)
|
||||||
|
fmt.Println("距离现在的时间:", duration) // 输出: 距离现在的时间: 24h0m0s
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **5. 时间间隔(Duration)**
|
||||||
|
- **`time.Duration`**
|
||||||
|
表示时间间隔,通常以纳秒为单位。可以通过常量如 `time.Second`、`time.Minute` 等表示固定的时间间隔。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
duration := 2*time.Hour + 30*time.Minute
|
||||||
|
fmt.Println("时间间隔:", duration) // 输出: 时间间隔: 2h30m0s
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **6. 定时器与休眠**
|
||||||
|
- **`Sleep(d Duration)`**
|
||||||
|
让当前 Goroutine 进入休眠状态,持续指定的时间间隔。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
fmt.Println("开始休眠...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
fmt.Println("休眠结束!")
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`After(d Duration) <-chan Time`**
|
||||||
|
返回一个通道,在指定的时间间隔后发送当前时间。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
fmt.Println("等待2秒...")
|
||||||
|
<-time.After(2 * time.Second)
|
||||||
|
fmt.Println("2秒已过!")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **7. 时间相关常量**
|
||||||
|
- **`time.Second`、`time.Minute`、`time.Hour` 等**
|
||||||
|
提供了常用的时间单位常量,便于时间间隔的计算。
|
||||||
|
示例:
|
||||||
|
```go
|
||||||
|
fmt.Println("一小时有多少秒:", time.Hour.Seconds()) // 输出: 一小时有多少秒: 3600
|
||||||
|
```
|
42
技术/Go/数据容器.md
Normal file
42
技术/Go/数据容器.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## 切片Slice
|
||||||
|
## 切片容量如何增长
|
||||||
|
在 Go语言中,切片的容量是一种动态增长的机制。当切片的长度达到或超过容量时,Go 语言会自动扩展其底层数组的容量,一般由 append 触发。切片容量增长(growslice)的具体规则在不同版本的规则不同。
|
||||||
|
### 对于 go1.18 之前来说:
|
||||||
|
- 如果期望容量大于当前容量的两倍就会使用期望容量;
|
||||||
|
- 如果当前切片的长度小于1024的话,growslice 时就会将容量翻倍;
|
||||||
|
- 如果当前切片的长度大于1024的话,growslice时会每次增加 **25%** 的容量,直到新容量大于期望容量。
|
||||||
|
### 对于go1.18之后来说:
|
||||||
|
减小了倍增阈值,但是在后续25%的幅度增加的时候把阈值作为基准的一部分,来避免扩容次数过多的问题:
|
||||||
|
- 如果期望容量大于当前容量的两倍就会使用期望容量;
|
||||||
|
- 如果当前切片的长度小于阈值256的话,growslice时就会将容量翻倍;
|
||||||
|
- 如果当前切片的长度大于等于256,就会每次增加`(newcap+3*threshold)/4`的容量,直到新容量大于期望容量。
|
||||||
|
此外,在扩容之后还会进行一步roundupsiz,这一步主要是靠内存对齐的优化,来计算出最终的容量。
|
||||||
|
### 切片与底层数组的关系:
|
||||||
|
- 切片本质上是一个对底层数组的抽象。一个切片包含三个部分:指向底层数组的指针、切片的长度和切片的容量。
|
||||||
|
- 切片是动态数组,它的大小并非固定不变,而是可以根据需要动态扩展。
|
||||||
|
### 切片容量增大的过程:
|
||||||
|
- 当我们向切片中追加元素,如果已有的容量不足以容纳新的元素,Go语言会自动分配一个更大的底层数组,并将旧的元素**复制**到新的数组中。
|
||||||
|
- 在容量小于1024时,新的容量至少是旧容量的两倍。而当容量达到或超过1024时,增长因子减小为1.25。
|
||||||
|
### 什么是内存分配和复制?
|
||||||
|
- 当切片的容量扩展时,Go运行时会新分配一块更大的内存空间,这涉及到内存分配操作。
|
||||||
|
- 然后,运行时将旧的底层数组的内容**复制**到新的内存空间。复制操作虽然是透明的,但是它的开销不容忽视,尤其是在大规模数组操作时。因此,为了**减少频繁的容量扩展**,通常我们可以在**初始化**切片时就指定一个合理的容量。
|
||||||
|
### Go语言优化措施:
|
||||||
|
- Go语言中的切片设计考虑了性能和灵活性,它采用了倍增和低倍增的机制来实现容量的自动成长。
|
||||||
|
- 这种设计使得切片既能在小规模数据处理时保持高效,又能在处理大规模数据时避免频繁的内存分配和数据复制开销。
|
||||||
|
|
||||||
|
## 内建函数 make 和 new 的区别是什么
|
||||||
|
首先 make 和 new 均是 Go 语言的内置的用来分配内存的函数。但是使用的类型不同:前者使用于 `slice,map,channel` 等引用类型;后者适用于 int 型、数组、结构体等值类型。
|
||||||
|
|
||||||
|
其次,两者的函数形式及调用形式不同,函数形式如下:
|
||||||
|
~~~go
|
||||||
|
func make(t Type, size ...IntegerType) Type
|
||||||
|
func new(Type) *Type
|
||||||
|
~~~
|
||||||
|
前者返回一个值,后者返回一个指针。
|
||||||
|
|
||||||
|
使用上, make 返回初始化之后的类型的引用, new 会为类型的新值分配已置零的内存空间,并返回指针。例如:
|
||||||
|
~~~ go
|
||||||
|
s := make([]int, 0, 10) // 使用 make 创建一个长度为0,容量为10的切片
|
||||||
|
a = new(int) // 使用 new 分配一个零值的 int 型
|
||||||
|
*a = 5
|
||||||
|
~~~
|
373
技术/Go/算法相关.md
Normal file
373
技术/Go/算法相关.md
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
## 性能优化运算
|
||||||
|
~~~ go
|
||||||
|
2^x = 1 << x
|
||||||
|
x/2 x >> 1
|
||||||
|
~~~
|
||||||
|
## 加油站
|
||||||
|
在一条环路上有 `n` 个加油站,其中第 `i` 个加油站有汽油 `gas[i]` 升。
|
||||||
|
|
||||||
|
你有一辆油箱容量无限的的汽车,从第 `i` 个加油站开往第 `i+1` 个加油站需要消耗汽油 `cost[i]` 升。你从其中的一个加油站出发,开始时油箱为空。
|
||||||
|
|
||||||
|
给定两个整数数组 `gas` 和 `cost` ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 `-1` 。如果存在解,则 **保证** 它是 **唯一** 的。
|
||||||
|
|
||||||
|
~~~ go
|
||||||
|
func canCompleteCircuit(gas []int, cost []int) int {
|
||||||
|
n := len(gas)
|
||||||
|
totalGas, currentGas,start := 0, 0, 0
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
totalGas += gas[i] - cost[i]
|
||||||
|
currentGas += gas[i] - cost[i]
|
||||||
|
// 如果当前油量小于0,说明从start到i之间不能作为一个有效的起始点
|
||||||
|
if currentGas < 0 {
|
||||||
|
start = i + 1
|
||||||
|
currentGas = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if totalGas >= 0 {
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
## 逆波兰表达式
|
||||||
|
栈
|
||||||
|
~~~ go
|
||||||
|
func evalRPN(tokens []string) int {
|
||||||
|
stk := []int{}
|
||||||
|
for _, token := range tokens {
|
||||||
|
if token == "+" {
|
||||||
|
stk = append(stk[:len(stk)-2], stk[len(stk)-2]+stk[len(stk)-1])
|
||||||
|
} else if token == "-" {
|
||||||
|
stk = append(stk[:len(stk)-2], stk[len(stk)-2]-stk[len(stk)-1])
|
||||||
|
} else if token == "*" {
|
||||||
|
stk = append(stk[:len(stk)-2], stk[len(stk)-2]*stk[len(stk)-1])
|
||||||
|
} else if token == "/" {
|
||||||
|
stk = append(stk[:len(stk)-2], stk[len(stk)-2]/stk[len(stk)-1])
|
||||||
|
} else {
|
||||||
|
i, err := strconv.Atoi(token)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
stk = append(stk, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stk[0]
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## 二叉树的层序遍历
|
||||||
|
DFS算法
|
||||||
|
~~~ go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import . "nc_tools" // 引入工具包,假设其中定义了树节点结构 `TreeNode`
|
||||||
|
|
||||||
|
// BFS迭代法,通法
|
||||||
|
func levelOrder(root *TreeNode) [][]int {
|
||||||
|
// 如果根节点为空,直接返回空的二维数组
|
||||||
|
if root == nil {
|
||||||
|
return [][]int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
queue := []*TreeNode{} // 定义一个队列,用于存储当前层的节点
|
||||||
|
queue = append(queue, root) // 将根节点加入队列
|
||||||
|
levels := [][]int{} // 定义一个二维数组,用于存储每一层的节点值
|
||||||
|
|
||||||
|
// 当队列不为空时,继续遍历
|
||||||
|
for len(queue) > 0 {
|
||||||
|
n := len(queue) // 获取当前层的节点数量
|
||||||
|
level := []int{} // 定义一个一维数组,用于存储当前层的节点值
|
||||||
|
|
||||||
|
// 遍历当前层的所有节点
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
root = queue[0] // 取出队列的第一个节点
|
||||||
|
queue = queue[1:] // 将该节点从队列中移除
|
||||||
|
|
||||||
|
level = append(level, root.Val) // 将当前节点的值加入当前层的结果中
|
||||||
|
|
||||||
|
// 如果左子节点存在,将其加入队列
|
||||||
|
if root.Left != nil {
|
||||||
|
queue = append(queue, root.Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果右子节点存在,将其加入队列
|
||||||
|
if root.Right != nil {
|
||||||
|
queue = append(queue, root.Right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将当前层的结果加入最终结果中
|
||||||
|
levels = append(levels, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回所有层的结果
|
||||||
|
return levels
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## 存在重复元素II
|
||||||
|
哈希表
|
||||||
|
~~~ go
|
||||||
|
func containsNearbyDuplicate(nums []int, k int) bool {
|
||||||
|
m := make(map[int]int)
|
||||||
|
for i, value := range nums{
|
||||||
|
if _, ok := m[value]; ok {
|
||||||
|
if (i - m[value]) <= k {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
m[value] = i
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m[value] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## 移动0
|
||||||
|
快慢指针
|
||||||
|
~~~ go
|
||||||
|
func moveZeroes(nums []int) {
|
||||||
|
left, right := 0, 0
|
||||||
|
for right < len(nums) {
|
||||||
|
if nums[right] != 0 {
|
||||||
|
nums[left], nums[right] = nums[right], nums[left]
|
||||||
|
left++
|
||||||
|
}
|
||||||
|
right++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
## Pow(x, n)
|
||||||
|
~~~ go
|
||||||
|
// myPow 计算x的n次幂,通过快速幂算法实现时间复杂度O(log n)
|
||||||
|
// 当n为负数时,根据数学性质x^(-n) = 1/(x^n)转换为正指数计算
|
||||||
|
func myPow(x float64, n int) float64 {
|
||||||
|
if n >= 0 {
|
||||||
|
return quick(x, n)
|
||||||
|
}
|
||||||
|
return 1.0 / quick(x, -n) // 负指数转为倒数计算
|
||||||
|
}
|
||||||
|
|
||||||
|
// quick 递归实现快速幂算法,采用分治策略降低计算次数
|
||||||
|
// 核心思想:每次将指数折半,底数平方,利用公式x^n = x^(n/2) * x^(n/2)(n为偶数时)
|
||||||
|
func quick(x float64, n int) float64 {
|
||||||
|
if n == 0 {
|
||||||
|
return 1 // 递归终止条件:任何数的0次方均为1
|
||||||
|
}
|
||||||
|
y := quick(x, n/2) // 将问题规模减半,分治策略核心步骤
|
||||||
|
if n%2 == 0 {
|
||||||
|
return y * y // 偶数次幂:直接返回子问题平方
|
||||||
|
}
|
||||||
|
return y * y * x // 奇数次幂:额外乘以一次底数x
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
## 重复的子字符串
|
||||||
|
给定一个非空的字符串 `s` ,检查是否可以通过由它的一个子串重复多次构成。
|
||||||
|
~~~ go
|
||||||
|
func repeatedSubstringPattern(s string) bool {
|
||||||
|
n := len(s)
|
||||||
|
for i := 1; i*2 <= n; i++ {
|
||||||
|
if n%i == 0 {
|
||||||
|
match := true
|
||||||
|
for j := i; j < n; j++ {
|
||||||
|
if s[j] != s[j-i] {
|
||||||
|
match = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 快速排序
|
||||||
|
|
||||||
|
```go
|
||||||
|
func QuickSort(arr []int) {
|
||||||
|
if len(arr) <= 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择最后一个元素作为基准
|
||||||
|
pivot := arr[len(arr)-1]
|
||||||
|
i := 0 // 标记小于基准的边界
|
||||||
|
|
||||||
|
// 分区过程
|
||||||
|
for j := 0; j < len(arr)-1; j++ {
|
||||||
|
if arr[j] <= pivot {
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将基准放到正确的位置
|
||||||
|
arr[i], arr[len(arr)-1] = arr[len(arr)-1], arr[i]
|
||||||
|
|
||||||
|
// 递归排序左右子数组
|
||||||
|
QuickSort(arr[:i])
|
||||||
|
QuickSort(arr[i+1:])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
快速排序采用 **分治法(Divide and Conquer)** 策略:
|
||||||
|
|
||||||
|
1. **选基准**:从数组中选择一个元素作为基准(pivot)。
|
||||||
|
2. **分区**:将数组分为两部分:
|
||||||
|
* 左半部分所有元素 ≤ 基准
|
||||||
|
* 右半部分所有元素 ≥ 基准
|
||||||
|
3. **递归**:对左右子数组重复上述步骤,直到子数组长度为 1 或 0(已有序)。
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## 反转链表
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition for singly-linked list.
|
||||||
|
* type ListNode struct {
|
||||||
|
* Val int
|
||||||
|
* Next *ListNode
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
func reverseList(head *ListNode) *ListNode {
|
||||||
|
var prev *ListNode
|
||||||
|
curr := head
|
||||||
|
for curr != nil {
|
||||||
|
next := curr.Next
|
||||||
|
curr.Next = prev
|
||||||
|
prev = curr
|
||||||
|
curr = next
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## 二分查找
|
||||||
|
|
||||||
|
```go
|
||||||
|
func search(nums []int, target int) int {
|
||||||
|
left, right := 0, len(nums)-1
|
||||||
|
for left <= right {
|
||||||
|
mid := left + (right-left)/2
|
||||||
|
if nums[mid] == target {
|
||||||
|
return mid
|
||||||
|
} else if nums[mid] > target{
|
||||||
|
right = mid - 1
|
||||||
|
} else {
|
||||||
|
left = mid + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 字符串相加
|
||||||
|
|
||||||
|
```go
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func addStrings(num1 string, num2 string) string {
|
||||||
|
buf := make([]byte, max(len(num1), len(num2)) + 1)
|
||||||
|
length := len(buf) - 1
|
||||||
|
add := 0
|
||||||
|
for i, j := len(num1)-1, len(num2)-1; j >= 0 || i >= 0; {
|
||||||
|
num := add
|
||||||
|
if i >= 0 {
|
||||||
|
num += int(num1[i] - '0')
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
if j >= 0 {
|
||||||
|
num += int(num2[j] - '0')
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
if num >= 10 {
|
||||||
|
num %= 10
|
||||||
|
add = 1
|
||||||
|
} else {
|
||||||
|
add = 0
|
||||||
|
}
|
||||||
|
buf[length] = byte(num+'0')
|
||||||
|
length--
|
||||||
|
}
|
||||||
|
if add != 0 {
|
||||||
|
buf[length] = '1'
|
||||||
|
}
|
||||||
|
if buf[0]==0{
|
||||||
|
buf=buf[1:]
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rand7 --> Rand10
|
||||||
|
拒绝采样
|
||||||
|
```go
|
||||||
|
func rand10() int {
|
||||||
|
for {
|
||||||
|
row := rand7()
|
||||||
|
col := rand7()
|
||||||
|
idx := (row-1)*7 + col
|
||||||
|
if idx <= 40 {
|
||||||
|
return 1 + (idx-1)%10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最大子序和
|
||||||
|
|
||||||
|
```go
|
||||||
|
func maxSubArray(nums []int) int {
|
||||||
|
max := nums[0]
|
||||||
|
for i := 1; i < len(nums); i++ {
|
||||||
|
if nums[i] + nums[i-1] > nums[i] {
|
||||||
|
nums[i] += nums[i-1]
|
||||||
|
}
|
||||||
|
if nums[i] > max {
|
||||||
|
max = nums[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 阶乘后的零
|
||||||
|
~~~ go
|
||||||
|
func trailingZeroes(n int) (ans int) {
|
||||||
|
for n > 0 {
|
||||||
|
n /= 5
|
||||||
|
ans += n
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
阶乘末尾的零由因子2和5的乘积(即10)产生。由于2的个数远多于5的个数,因此只需统计因子5的个数即可。具体步骤如下:
|
||||||
|
|
||||||
|
1. **统计5的倍数** :每个能被5整除的数至少贡献一个5。
|
||||||
|
2. **统计25的倍数** :每个能被25整除的数额外贡献一个5(因为25 = 5×5)。
|
||||||
|
3. **统计更高次幂** :类似地,统计125、625等更高次幂的倍数,直到商为0。
|
257
技术/Go/题目.md
Normal file
257
技术/Go/题目.md
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
# Go 语言面试题精选
|
||||||
|
|
||||||
|
**1. Go 语言面试题导论**
|
||||||
|
|
||||||
|
- Go 语言概述及其关键特性:
|
||||||
|
|
||||||
|
Go 语言,通常被称为 Golang,是由 Google 开发的一种现代编程语言,其设计目标是简洁、高效和可靠。它结合了高级语言的易用性与底层语言的性能,使其成为构建可扩展和高并发应用程序的理想选择。Go 语言的关键特性包括其简洁的语法,这使得代码易于阅读和编写;内置的并发支持,通过 goroutine 和 channel 实现,极大地简化了并发编程;高效的内存管理和垃圾回收机制,减轻了开发人员的负担;以及一个强大的标准库,提供了广泛的功能。由于其卓越的性能和并发能力,Go 语言被广泛应用于 Web 开发和云计算领域。
|
||||||
|
|
||||||
|
- 一套全面的面试题的重要性:
|
||||||
|
|
||||||
|
在评估 Go 语言开发人员的技能时,一套全面的面试题至关重要。这样的题库不仅能够考察候选人对 Go 语言基础知识的掌握程度,还能深入评估其解决问题的能力和实际项目经验。通过覆盖从基本语法到高级并发模式,再到实际应用场景的各类问题,面试官可以更准确地判断候选人是否适合特定的职位需求,并识别其在 Go 语言不同方面的优势和不足。对于准备面试的工程师而言,一套全面的面试题也能帮助他们系统地复习和巩固 Go 语言的知识体系,从而更有信心地应对面试挑战。
|
||||||
|
|
||||||
|
|
||||||
|
**2. Go 语言基础面试题**
|
||||||
|
|
||||||
|
- 基本语法和数据类型:
|
||||||
|
|
||||||
|
面试中经常会问到 Go 语言是什么及其关键特性。这通常作为开场问题,旨在了解候选人对 Go 语言的整体认知。Go 语言以其静态类型、编译型特性以及为简洁和性能而设计的特点而闻名。其关键特性包括内置的并发支持、垃圾回收、以及强大的标准库。理解 Go 语言的基本程序语法也是考察的重点。此外,面试还会涉及 Go 语言的基本数据类型,例如整型、浮点型、布尔型和字符串等。了解 Unicode 码点(rune)在 Go 语言中如何表示也可能被问及,这对于处理文本和国际化至关重要。
|
||||||
|
|
||||||
|
- 变量声明和初始化:
|
||||||
|
|
||||||
|
变量的声明和初始化是编程的基础。Go 语言提供了多种声明变量的方式,包括使用 var 关键字和短变量声明语法。理解这些不同的声明方式以及它们的使用场景是很重要的。面试中可能会问到 Go 语言如何处理变量的声明和初始化。此外,区分值类型和引用类型在 Go 语言中至关重要。例如,数组是值类型,而切片是引用类型,这会影响变量在赋值和函数传递过程中的行为。
|
||||||
|
|
||||||
|
- 数组和切片:
|
||||||
|
|
||||||
|
切片是 Go 语言中一种灵活且强大的数据结构。面试中可能会问到什么是切片,以及如何创建切片。理解数组和切片之间的区别是核心考点。数组的大小在声明时是固定的,而切片则可以动态增长和缩小。向切片添加元素是常见的操作。此外,理解切片和结构体在作为参数传递给函数时,是按值传递还是按引用传递也很重要。切片本质上是一个包含指向底层数组的指针、长度和容量的结构体,因此在函数中修改切片可能会影响原始数据。
|
||||||
|
|
||||||
|
- 映射(Maps):
|
||||||
|
|
||||||
|
映射是 Go 语言中用于存储键值对的数据结构。面试中可能会问到什么是映射,以及如何声明和初始化映射。从映射中检索特定键的值以及检查某个键是否存在于映射中也是基本操作。掌握这些操作对于处理各种数据存储和检索任务至关重要。
|
||||||
|
|
||||||
|
- 函数和方法:
|
||||||
|
|
||||||
|
Go 语言支持函数返回多个值,这在处理可能出错的操作时非常有用,通常会将结果和错误信息一起返回。理解方法和函数之间的区别也很重要。方法是与特定类型关联的函数,它允许在特定类型的值上调用该函数。此外,了解函数闭包的概念 对于理解 Go 语言中的函数式编程特性很有帮助。闭包是指可以访问其词法作用域之外变量的函数。
|
||||||
|
|
||||||
|
|
||||||
|
**3. Go 语言并发面试题**
|
||||||
|
|
||||||
|
- Goroutines:
|
||||||
|
|
||||||
|
Goroutine 是 Go 语言实现并发的核心机制。面试中会考察什么是 goroutine,以及如何创建新的 goroutine,通常是通过在函数调用前加上 go 关键字。使用 goroutine 的主要优势在于其轻量级和高效性,与操作系统线程相比,创建和管理的开销要小得多。理解 goroutine 与操作系统线程之间的区别 是关键,例如 goroutine 由 Go 运行时管理,而线程由操作系统管理。面试中还可能涉及 goroutine 的调度和执行方式,以及 Go 运行时调度器的工作原理。了解如何停止 goroutine 对于资源管理很重要。关于可以创建的最大 goroutine 数量,虽然 Go 语言本身没有严格的限制,但实际数量会受到系统资源(如内存和 CPU)的限制。理解 goroutine 可能处于的不同状态,如创建、可运行、运行、阻塞和死亡,有助于理解其生命周期。此外,需要了解什么是 goroutine 泄漏以及如何避免,这对于保证应用程序的稳定性和性能至关重要。
|
||||||
|
|
||||||
|
- Channels:
|
||||||
|
|
||||||
|
Channel 是 goroutine 之间进行安全通信的主要方式。面试中会考察对 channel 概念的理解,以及 channel 在 goroutine 之间进行通信和同步时所扮演的角色。理解缓冲 channel 和无缓冲 channel 之间的区别以及它们各自的使用场景 很重要。无缓冲 channel 在发送和接收操作完成之前会阻塞发送方和接收方,而缓冲 channel 则允许在缓冲区未满之前发送数据而不会立即阻塞。面试中还可能问到只读和只写 channel 的用途,它们可以提高代码的类型安全性和可读性。了解从已关闭的 channel 和未初始化的 channel 进行读写操作时会发生什么情况 对于避免运行时错误至关重要。
|
||||||
|
|
||||||
|
- 同步原语:
|
||||||
|
|
||||||
|
在并发编程中,避免竞态条件至关重要。面试中会考察如何在使用 goroutine 时避免竞态条件,以及 Go 语言提供的同步机制。Mutex 是 Go 语言中最基本的同步原语,用于提供对共享数据的互斥访问。面试中可能会问到什么是 mutex,如何使用它来确保对共享数据的安全访问,以及 Mutex 和 RWMutex 之间的区别。RWMutex 允许多个 reader 同时持有读锁,但只允许一个 writer 持有写锁。熟悉 sync 包及其提供的各种类型(如 Mutex、RWMutex、WaitGroup、Once、Cond、Pool 和 Map)的使用场景 非常重要。WaitGroup 用于等待一组 goroutine 完成执行。面试中可能会问到什么是 WaitGroup,以及如何使用它来同步 goroutine。关于使用 map 加 mutex 和 sync.Map 哪个更好,以及 sync.Map 的缺点,这涉及到对并发数据结构性能和适用性的理解。sync.Map 是 Go 1.9 引入的并发安全的 map,在某些读多写少的场景下性能优于 map 加 mutex。
|
||||||
|
|
||||||
|
- 竞态条件和死锁:
|
||||||
|
|
||||||
|
对竞态条件的理解是并发编程的基础。面试中会问到什么是竞态条件,为什么它很重要,不同类型的竞态条件,竞态条件何时发生,以及如何预防它。死锁是并发编程中另一个常见的问题。面试中可能会问到什么是死锁,它何时发生,如何预防它,以及经典的哲学家就餐问题。了解如何在并发的 Go 程序中预防死锁 至关重要,常见的策略包括以相同的顺序获取锁、使用 defer 释放锁以及避免在持有锁时调用可能获取相同锁的其他函数。
|
||||||
|
|
||||||
|
- select 语句:
|
||||||
|
|
||||||
|
select 语句是 Go 语言中用于处理多个 channel 操作的强大工具。面试中会问到什么是 select 语句,以及如何使用它来处理 channel 和等待 goroutine。select 允许 goroutine 等待多个通信操作,只有当其中的一个操作可以执行时才继续执行。
|
||||||
|
|
||||||
|
- 并发模式:
|
||||||
|
|
||||||
|
熟悉常见的并发模式可以展示候选人在实际并发场景中的经验。面试中可能会问到 Go 语言中的并发模式,例如 worker pool、pipelines 以及 Fan-In 和 Fan-Out 模式。Worker pool 用于限制并发执行的任务数量,pipeline 用于将一个任务分解为一系列顺序执行的阶段,而 Fan-In 和 Fan-Out 则用于处理多个输入或将结果分发给多个消费者。了解 Go 语言中的异步模式、队列的实现、心跳机制、goroutine 的超时和取消、goroutine 之间的错误传播以及工作窃取等概念 也能体现候选人在并发编程方面的深度。此外,理解 "goroutine pool" 模式以及它如何帮助管理创建的 goroutine 数量 对于构建可伸缩的应用程序很重要。
|
||||||
|
|
||||||
|
- 并发问题和调试:
|
||||||
|
|
||||||
|
过度使用 goroutine 可能会导致一些问题。面试中会问到使用过多 goroutine 可能出现哪些问题,以及解决这些问题的方法。例如,过多的 goroutine 会增加上下文切换的开销,从而降低性能。Go 语言提供了一些工具和包来支持并发程序的调试和分析,例如 go tool pprof 可以用于性能分析。了解在 goroutine 中使用 map 时可能遇到的特性也很重要,例如 map 不是并发安全的,需要使用锁或其他并发控制机制来保护。
|
||||||
|
|
||||||
|
|
||||||
|
**4. Go 语言错误处理面试题**
|
||||||
|
|
||||||
|
- error 类型和错误处理约定:
|
||||||
|
|
||||||
|
Go 语言使用 error 类型来表示错误。面试中会问到如何在 Go 语言中处理错误,并要求提供示例。Go 语言的错误处理约定是显式的,函数通常会返回一个 error 类型的值,调用者需要检查该值是否为 nil 来判断操作是否成功。了解 Go 语言中一些好的错误处理实践 非常重要,例如尽早检查错误、返回错误而不是 panic(如果可能)、以及包装错误以提供更多上下文信息。
|
||||||
|
|
||||||
|
- panic 和 recover:
|
||||||
|
|
||||||
|
panic 和 recover 是 Go 语言中用于处理运行时恐慌的机制。面试中可能会问到什么是 panic 以及如何处理它们,并解释 panic 和 recover 在 Go 语言中的作用以及它们应该在何时使用。panic 通常用于表示不可恢复的错误,而 recover 可以在 deferred 函数中捕获 panic,从而阻止程序崩溃。通常建议谨慎使用 panic,并尽可能使用 error 来处理可预见的错误。使用 defer 配合 recover 可以优雅地从 panic 中恢复。
|
||||||
|
|
||||||
|
- 错误处理的最佳实践:
|
||||||
|
|
||||||
|
除了尽早检查错误和返回错误之外,还有一些其他的最佳实践。例如,包装错误可以为错误添加更多的上下文信息,有助于诊断问题。应该谨慎使用 sentinel error,而是倾向于使用自定义错误类型,这可以提供更丰富的信息和更好的类型安全性。使用 errors.Is 和 errors.As 可以更安全地检查错误的类型。在代码中显式地处理错误,避免过度的错误检查,并使用符合语言习惯的错误消息可以提高代码的可读性。
|
||||||
|
|
||||||
|
- 自定义错误类型:
|
||||||
|
|
||||||
|
创建自定义错误类型可以提供更具体的错误信息。虽然 Go 语言没有像其他语言那样的“异常”概念,但创建自定义的 error 类型可以达到类似的目的。这通常通过定义一个新的 struct 类型并实现 error 接口的 Error() 方法来完成。
|
||||||
|
|
||||||
|
- 并发代码中的错误处理:
|
||||||
|
|
||||||
|
在并发的 Go 代码中处理错误需要特别注意。通常会使用 channel 来在 goroutine 之间传递错误信息。select 语句也可以用于处理来自多个 channel 的错误。使用 defer 语句可以确保在发生错误时资源得到正确释放。context.Context 也常用于管理并发代码中的取消和超时,从而间接地处理错误情况。
|
||||||
|
|
||||||
|
- error 和 panic 的区别:
|
||||||
|
|
||||||
|
理解 error 和 panic 之间的关键区别对于编写健壮的 Go 代码至关重要。error 用于处理预期的条件,可以作为函数返回值进行传递和处理。而 panic 则用于处理意外的、无法恢复的错误,它会中断程序的正常执行流程。
|
||||||
|
|
||||||
|
|
||||||
|
**5. Go 语言接口和面向对象编程概念面试题**
|
||||||
|
|
||||||
|
- 理解和实现接口:
|
||||||
|
|
||||||
|
接口是 Go 语言中一个核心的概念,它定义了一组方法签名,但不提供实现。面试中会深入考察对 Go 接口的理解,包括它们是什么以及如何工作。Go 语言的接口实现是隐式的,这意味着如果一个类型实现了接口中定义的所有方法,那么它就自动实现了该接口。面试中可能会问到如何在 Go 语言中实现接口。多态是面向对象编程的一个重要特性,面试中可能会问到如何在 Go 语言中实现多态,这通常是通过接口来实现的。在 Go 语言引入泛型之前,接口是实现类型灵活性的主要方式,面试中可能会问到如何在没有泛型的情况下实现接口。
|
||||||
|
|
||||||
|
- 结构体和方法:
|
||||||
|
|
||||||
|
结构体是 Go 语言中用于定义自定义数据类型的机制。面试中会问到什么是结构体以及如何使用它们,以及如何定义结构体。方法是与特定数据类型关联的函数。面试中可能会问到什么是方法,以及如何在 Go 语言中声明和使用方法。方法通过接收器(receiver)与类型关联。
|
||||||
|
|
||||||
|
- Go 语言中的组合优于继承:
|
||||||
|
|
||||||
|
Go 语言在面向对象编程方面采取了一种独特的策略,倾向于使用组合而不是传统的继承。面试中可能会问到 Go 语言在面向对象编程方面的方法(组合优于继承)。虽然 Go 语言没有像其他语言那样的继承机制,但它提供了通过嵌入(embedding)来实现代码重用的方式。面试中可能会问到如何在 Go 语言中实现“继承”,以及解释 Go 语言中嵌入的概念并提供示例。嵌入允许一个结构体包含另一个结构体的字段,从而实现代码的复用和类型的组合。
|
||||||
|
|
||||||
|
- Go 语言中的多态:
|
||||||
|
|
||||||
|
(此点在上面已强调,此处再次提及以示重要性)面试中可能会再次强调如何在 Go 语言中实现多态。
|
||||||
|
|
||||||
|
|
||||||
|
**6. Go 语言内存管理和垃圾回收面试题**
|
||||||
|
|
||||||
|
- Go 语言中的栈和堆的概念:
|
||||||
|
|
||||||
|
理解栈和堆是理解内存管理的基础。面试中可能会问到栈和堆在 Go 语言的上下文中代表什么,以及它们之间有什么区别。栈通常用于存储局部变量和函数调用的信息,其生命周期与函数调用相关;而堆则用于存储生命周期可能超出函数调用的变量。了解如何判断一个变量是分配在堆上还是栈上,以及相关的 Go 命令,对于优化内存使用很重要。
|
||||||
|
|
||||||
|
- Go 语言的垃圾回收器如何工作:
|
||||||
|
|
||||||
|
Go 语言拥有自动垃圾回收机制,这极大地简化了内存管理。面试中会问到 Go 语言的垃圾回收器是如何工作的。Go 使用并发的三色标记清除(tri-color concurrent mark-sweep)算法。了解 Go 垃圾回收器的工作原理(标记阶段、清除阶段、并发和并行收集) 有助于理解其性能特点。
|
||||||
|
|
||||||
|
- 影响垃圾回收的因素:
|
||||||
|
|
||||||
|
了解哪些因素会影响垃圾回收的性能对于优化 Go 应用程序很重要。面试中可能会问到减少垃圾回收开销的一些策略,例如使用对象池重用内存、减少短生命周期对象的分配以及预分配切片或结构体以避免频繁分配。堆的增长也会触发垃圾回收。此外,不同类型的垃圾回收器对资源消耗有不同的影响。
|
||||||
|
|
||||||
|
- 内存分配和优化:
|
||||||
|
|
||||||
|
内存分配是影响 Go 应用程序性能的关键因素。面试中可能会问到如何在 Go 应用程序中优化性能,这通常包括最小化内存分配、高效地使用 goroutine 和 channel、优化循环和条件语句以及进行代码 profiling。深入讨论 Go 语言的内存分配及其如何影响性能也可能被问到。可以使用 new 关键字和 make 函数在堆上显式分配内存。前面提到的对象池、减少短生命周期对象分配和预分配等策略都是内存优化的一部分。
|
||||||
|
|
||||||
|
- 垃圾回收的资格:
|
||||||
|
|
||||||
|
理解对象何时符合垃圾回收的条件至关重要。面试中可能会问到对象何时变得符合垃圾回收的条件(当它不再能从任何活动线程或静态引用到达时)。了解垃圾回收器如何收集符合条件的对象也很重要。虽然 Go 语言允许通过 runtime 包手动触发垃圾回收,但不建议这样做,因为垃圾回收器通常能够很好地自行管理内存。
|
||||||
|
|
||||||
|
- 延迟调用和资源清理:
|
||||||
|
|
||||||
|
defer 语句是 Go 语言中用于确保函数调用在包含它的函数执行完毕后(但在返回之前)执行的机制。它通常用于资源清理操作。面试中可能会问到 defer 语句的作用,以及哪些使用场景可以帮助确保正确和安全地清理资源,例如释放打开的文件或网络连接、释放在函数执行期间分配的内存以及在从函数返回之前记录信息或处理错误。defer 调用的执行顺序是后进先出(LIFO)。
|
||||||
|
|
||||||
|
|
||||||
|
**7. Go 语言标准库面试题**
|
||||||
|
|
||||||
|
- 关注 fmt 包:
|
||||||
|
|
||||||
|
包是 Go 语言中组织代码的基本单元。面试中可能会问到什么是包以及如何使用它们。fmt 包是 Go 语言标准库中用于格式化输入输出的重要包。面试中可能会问到如何使用 fmt.Sprintf 在 Go 语言中格式化字符串而不打印出来。
|
||||||
|
|
||||||
|
- 关注 net/http 包:
|
||||||
|
|
||||||
|
net/http 包提供了构建网络应用程序所需的基本功能。面试中可能会要求编写一个简单的 Go 程序来实现一个基本的 HTTP 服务器,该服务器响应 "Hello, World!"。
|
||||||
|
|
||||||
|
- 关注 io 包:
|
||||||
|
|
||||||
|
io 包提供了基本的 I/O 接口。在测试中,经常会使用 io.Writer 接口来捕获函数的输出。
|
||||||
|
|
||||||
|
- 关注 os 包:
|
||||||
|
|
||||||
|
os 包提供了与操作系统交互的功能。例如,使用 os.Create 创建文件后,通常会使用 defer file.Close() 来确保文件在使用完毕后被关闭。
|
||||||
|
|
||||||
|
- 通用标准库问题:
|
||||||
|
|
||||||
|
面试中可能会问到对 Go 语言标准库的经验,以及经常使用的特定包。这旨在了解候选人对 Go 语言生态系统的熟悉程度。init 函数在 Go 语言中有特定的用途,它在包被导入时自动执行,通常用于初始化包级别的变量或执行其他必要的启动任务。面试中可能会问到 init 函数的用途。数据序列化和反序列化是常见的任务,Go 语言的标准库提供了 encoding/json、encoding/xml、encoding/binary 等包来处理不同的数据格式。面试中可能会问到如何使用这些包在 Go 项目中处理数据序列化和反序列化。
|
||||||
|
|
||||||
|
|
||||||
|
**8. Go 语言测试面试题**
|
||||||
|
|
||||||
|
- 编写和运行测试:
|
||||||
|
|
||||||
|
测试是软件开发过程中至关重要的一环。面试中会问到如何在 Golang 中进行测试,以及如何使用 testing 包编写和运行测试。通常测试文件以 _test.go 结尾,测试函数以 Test 开头并接收 *testing.T 参数。运行测试通常使用 go test 命令。
|
||||||
|
|
||||||
|
- 测试类型:
|
||||||
|
|
||||||
|
Go 语言支持多种类型的测试,包括单元测试、集成测试和端到端测试。了解这些不同测试类型的目的和使用场景很重要。此外,契约测试(contract tests)也用于验证服务之间的交互是否符合预期。
|
||||||
|
|
||||||
|
- Go 语言中的基准测试:
|
||||||
|
|
||||||
|
基准测试用于评估代码的性能。Go 语言的 testing 包也支持编写基准测试函数,通常以 Benchmark 开头,可以使用 go test -bench=. 命令运行。
|
||||||
|
|
||||||
|
- 创建自定义测试套件:
|
||||||
|
|
||||||
|
了解如何在 Golang 中创建自定义测试套件 可以展示更深入的测试知识。
|
||||||
|
|
||||||
|
- 使用第三方测试工具:
|
||||||
|
|
||||||
|
除了 Go 语言内置的 testing 包之外,还有一些流行的第三方测试工具,例如 testify 和 gomega,它们提供了更丰富的断言库和测试辅助功能。熟悉这些工具的使用可以提高测试效率和代码质量。
|
||||||
|
|
||||||
|
|
||||||
|
**9. Go 语言高级面试题**
|
||||||
|
|
||||||
|
- Go 语言中的反射:
|
||||||
|
|
||||||
|
反射是 Go 语言的一个高级特性,它允许程序在运行时检查和操作类型的信息。面试中可能会问到什么是反射以及它的使用场景。反射功能强大但应谨慎使用,因为它可能会带来性能开销。了解 == 和 reflect.DeepEqual() 之间的区别 在比较复杂数据结构时很重要,reflect.DeepEqual() 可以进行深层比较。
|
||||||
|
|
||||||
|
- Go Modules 进行依赖管理:
|
||||||
|
|
||||||
|
依赖管理是现代软件开发中不可或缺的一部分。Go Modules 是 Go 语言官方推荐的依赖管理解决方案。面试中可能会问到如何使用 Go Modules 管理项目依赖,以及 go mod、go mod tidy 和 go clean -modcache 等命令的用途。
|
||||||
|
|
||||||
|
- 交叉编译:
|
||||||
|
|
||||||
|
Go 语言支持交叉编译,允许在一种操作系统和架构上构建可在另一种操作系统和架构上运行的程序。面试中可能会问到如何进行 Go 语言的交叉编译。
|
||||||
|
|
||||||
|
- Go 语言的运行时调度器:
|
||||||
|
|
||||||
|
Go 语言的运行时调度器负责管理 goroutine 的执行。面试中可能会问到 Go 语言的调度器是如何工作的以及如何管理 goroutine。理解 GOMAXPROCS 和 runtime.Gosched() 之间的区别 可以帮助更好地控制 Go 程序的并发行为。GOMAXPROCS 设置了可以同时执行 goroutine 的操作系统线程的最大数量,而 runtime.Gosched() 则会主动让出当前 goroutine 的执行权,允许其他 goroutine 运行。
|
||||||
|
|
||||||
|
- Go 语言中的可见性规则:
|
||||||
|
|
||||||
|
了解 Go 语言中的可见性规则对于编写模块化的代码很重要。导出(public)的标识符以大写字母开头,而未导出(private)的标识符以小写字母开头。
|
||||||
|
|
||||||
|
|
||||||
|
**10. Go 语言在实际应用中的面试题 - 微服务、API 设计和性能优化**
|
||||||
|
|
||||||
|
- 使用 Go 语言构建微服务:
|
||||||
|
|
||||||
|
Go 语言因其高效的并发和网络能力,非常适合构建微服务。面试中可能会问到是否有使用 Go 语言实现微服务的项目经验。此外,还会考察对微服务架构的理解,例如微服务是什么,它与单体架构的区别,以及微服务的主要优势和常见挑战。微服务之间的通信方式(如 RESTful API 和 gRPC),API 版本管理,服务发现,API 网关的作用,如何确保多个微服务之间的数据一致性,Saga 模式,服务发现的实现,微服务的安全性,以及如何在生产环境中监控微服务 等都是常见的面试话题。还需要了解微服务的关键特性,什么是“无状态”服务,微服务环境中的负载均衡如何工作,常见的服务间通信模式,每个服务一个数据库的模式,最终一致性的概念,领域驱动设计(DDD)在微服务中的重要性,事件驱动架构与微服务的区别,API 网关在处理跨切面关注点方面的作用,幂等性的概念,熔断器模式,防止服务间通信失败的策略,Strangler Fig 模式在从单体迁移到微服务时的作用,健康检查的实现,Bulkhead 模式,服务之间的安全性处理,管理多个数据库的挑战,用于服务监控的工具,REST 和 gRPC 的主要区别,API 网关如何帮助管理速率限制和节流,OAuth2 在保护微服务方面的作用,如何确保可追溯性,事件驱动架构的用途,容器(如 Docker)在微服务系统中的作用,如何使用 Kubernetes 部署微服务架构,服务发现及其在 Kubernetes 中的工作方式,如何管理认证和授权等跨切面关注点,服务网格(如 Istio)的作用,在大型分布式系统中微服务的版本管理,微服务中日志记录的挑战,分布式追踪的重要性,如何实现速率限制,如何测试微服务系统,集中式日志系统如何帮助调试,设计可伸缩微服务的最佳实践,以及消息代理(如 RabbitMQ 或 Kafka)及其在微服务中的使用场景。
|
||||||
|
|
||||||
|
- Go 语言中的 API 设计原则和最佳实践:
|
||||||
|
|
||||||
|
面试中可能会问到通用的 API 设计最佳实践,例如一致的命名约定、错误处理和版本控制。还会考察在 Go 语言中设计 API 的具体考虑因素,例如使用结构体作为请求和响应体,以及利用 Go 语言的错误处理机制。
|
||||||
|
|
||||||
|
- Go 应用程序的性能优化技巧:
|
||||||
|
|
||||||
|
性能优化是实际应用中非常重要的方面。面试中可能会问到前面提到的一些策略(最小化内存分配、高效的并发、优化循环、profiling)。还会考察特定的 Go 语言性能分析工具(如 pprof)。缓存策略、与 Go 应用程序相关的数据库优化技巧、负载均衡和扩展方面的考虑,以及优化 API 性能的经验 都是可能涉及的话题。此外,还会问到用于监控和优化性能的特定工具和技术(如 New Relic、LoadImpact、Apache JMeter)。
|
||||||
|
|
||||||
|
- 实际场景中常见的并发模式:
|
||||||
|
|
||||||
|
在实际应用中,会使用各种并发模式来解决不同的问题。面试中可能会问到 worker pool 在任务处理中的应用、使用 channel 实现发布/订阅模式、速率限制的实现以及熔断器模式的实现等。
|
||||||
|
|
||||||
|
|
||||||
|
**关键价值表格:**
|
||||||
|
|
||||||
|
1. **Goroutines vs. OS Threads 的比较 (第三节:Go 语言并发面试题):**
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
|---|---|---|
|
||||||
|
|**特性**|**Goroutine**|**OS 线程**|
|
||||||
|
|创建开销|非常低|相对较高|
|
||||||
|
|栈大小|初始较小 (约 2KB),可动态增长|通常较大,固定大小|
|
||||||
|
|管理方式|Go 运行时管理 (用户级别)|操作系统内核管理|
|
||||||
|
|上下文切换|非常快,由 Go 运行时调度器完成|相对较慢,需要操作系统介入|
|
||||||
|
|并发数量|可以轻松创建数千甚至数百万个|受系统资源限制,数量有限|
|
||||||
|
|典型用途|高并发任务,例如网络请求处理|CPU 密集型任务,I/O 操作|
|
||||||
|
|
||||||
|
2. **数组和切片的比较 (第二节:Go 语言基础面试题):**
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
|---|---|---|
|
||||||
|
|**特性**|**数组 (Array)**|**切片 (Slice)**|
|
||||||
|
|大小|固定大小,声明时指定|大小可动态变化|
|
||||||
|
|声明|`var a [n]T`|`var sT` 或 `s := make(T, len, cap)`|
|
||||||
|
|初始化|`[n]T{values}`|`T{values}` 或 `make(T, len, cap)`|
|
||||||
|
|类型|值类型|引用类型 (包含指向底层数组的指针)|
|
||||||
|
|长度|声明后长度不可变|可以通过 append 等操作改变长度|
|
||||||
|
|常见用途|通常作为底层数据结构使用|更常用,用于表示可变长度的序列|
|
||||||
|
|
||||||
|
**结论**
|
||||||
|
|
||||||
|
通过上述对 Go 语言相关面试题的全面梳理,可以看出,Go 语言的面试考察范围广泛,从基础语法、数据类型到高级并发、内存管理,再到实际应用场景如微服务和性能优化都有涉及。对于准备 Go 语言面试的工程师而言,不仅需要掌握扎实的语言基础,还需要深入理解其并发模型、内存管理机制以及在实际项目中的应用。对于招聘 Go 语言开发人员的企业而言,这份全面的面试题集可以帮助他们更有效地评估候选人的技能水平和经验,从而找到最适合团队的人才。深入理解这些问题背后的原理和最佳实践,将有助于在面试中展现出对 Go 语言的深刻理解和应用能力。
|
109
技术/MySQL/MySQL相关.md
Normal file
109
技术/MySQL/MySQL相关.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
## ACID vs BASE
|
||||||
|
|
||||||
|
**ACID**(原子性、一致性、隔离性、持久性)和 **BASE**(基本可用、软状态、最终一致性)是数据库事务和系统设计的两种核心理念,分别适用于不同场景。
|
||||||
|
|
||||||
|
### **ACID**
|
||||||
|
*适用于传统关系型数据库(如 MySQL、PostgreSQL)*
|
||||||
|
1. **原子性(Atomicity)**:事务要么全部成功,要么全部失败回滚。
|
||||||
|
2. **一致性(Consistency)**:数据始终符合预定义规则(如约束、触发器)。
|
||||||
|
3. **隔离性(Isolation)**:并发事务互不干扰,结果等同于串行执行。
|
||||||
|
4. **持久性(Durability)**:事务提交后数据永久保存,即使系统崩溃也不丢失。
|
||||||
|
|
||||||
|
**典型场景**:银行转账、订单支付、库存扣减等对数据准确性要求极高的场景。
|
||||||
|
|
||||||
|
### **BASE**
|
||||||
|
*适用于分布式 NoSQL 系统(如 Cassandra、MongoDB)*
|
||||||
|
1. **基本可用(Basically Available)**:系统即使部分故障,仍能响应请求(允许降级)。
|
||||||
|
2. **软状态(Soft State)**:数据可能随时间变化,无需实时同步。
|
||||||
|
3. **最终一致性(Eventually Consistent)**:数据更新会延迟同步,但最终全局一致。
|
||||||
|
|
||||||
|
**典型场景**:社交网络动态、电商商品浏览、日志存储等高并发、可容忍短暂不一致的场景。
|
||||||
|
|
||||||
|
|
||||||
|
### **核心差异**
|
||||||
|
| **维度** | **ACID** | **BASE** |
|
||||||
|
|----------------|-----------------------------------|-----------------------------------|
|
||||||
|
| **一致性** | 强一致性(实时) | 最终一致性(延迟) |
|
||||||
|
| **优先目标** | 数据安全与准确性 | 高可用性与扩展性 |
|
||||||
|
| **适用系统** | 单机/集中式数据库 | 分布式系统(如微服务、云原生) |
|
||||||
|
| **性能特点** | 读写延迟较高,吞吐量较低 | 读写延迟低,吞吐量高 |
|
||||||
|
|
||||||
|
**一句话总结**:
|
||||||
|
- **ACID**:牺牲性能换安全,适合“钱不能错”(如银行系统)。
|
||||||
|
- **BASE**:牺牲强一致换高可用,适合“用户能等”(如微博评论)。
|
||||||
|
|
||||||
|
根据业务需求选择:**要么严格保数据,要么灵活保体验**。
|
||||||
|
|
||||||
|
## 索引的分类
|
||||||
|
- 按 **「数据结构」** 分类:B+tree索引、Hash索引、Full-text索引。
|
||||||
|
- 按 **「物理存储」** 分类:聚簇索引(主键索引)、二级索引(辅助索引)。
|
||||||
|
- 按 **「字段特性」** 分类:主键索引、唯一索引、普通索引、前缀索引。
|
||||||
|
- 按 **「字段个数」** 分类:单列索引、联合索引。
|
||||||
|
|
||||||
|
|
||||||
|
## InnoDB默认索引
|
||||||
|
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
|
||||||
|
|
||||||
|
- 如果有主键,默认会使用主键作为聚簇索引的索引键(key);
|
||||||
|
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);
|
||||||
|
- 在上面两个都没有的情况下,InnoDB 将自动生成一个**隐式自增 id (不可见的名为row_id的列名为GEN_CLUST_INDEX的聚簇索引,该列是一个6字节的自增数值)** 列作为聚簇索引的索引键(key);
|
||||||
|
其它索引都属于**辅助索引(Secondary Index)**,也被称为**二级索引或非聚簇索引**。创建的主键索引和二级索引默认使用的是 **B+Tree 索引**。
|
||||||
|
|
||||||
|
## MySQL 的存储引擎有哪些?为什么常用InnoDB?
|
||||||
|
MySQL 的存储引擎常用的主要有 3 个:
|
||||||
|
|
||||||
|
- **InnoDB存储引擎**:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
|
||||||
|
- **MyISAM存储引擎**:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率
|
||||||
|
- **MEMORY存储引擎**:所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果
|
||||||
|
**常用InnoDB的原因是支持事务,且最小锁的粒度是行级锁。**
|
||||||
|
|
||||||
|
## 执行一条 SQL 查询语句,期间发生了什么?
|
||||||
|
|
||||||
|
- 连接器:建立连接,管理连接、校验用户身份;
|
||||||
|
- 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
|
||||||
|
- 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
|
||||||
|
- 执行 SQL:执行 SQL 共有三个阶段:
|
||||||
|
- 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
|
||||||
|
- 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
|
||||||
|
- 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## MySQL 的 NULL 值是怎么存放的?
|
||||||
|
|
||||||
|
MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。
|
||||||
|
|
||||||
|
NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
|
||||||
|
|
||||||
|
MySQL 怎么知道 varchar(n) 实际占用数据的大小?
|
||||||
|
|
||||||
|
MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。
|
||||||
|
|
||||||
|
## varchar(n) 中 n 最大取值为多少?
|
||||||
|
|
||||||
|
一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。
|
||||||
|
|
||||||
|
如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。
|
||||||
|
|
||||||
|
计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL值列表所占用的字节数 = 65535 - 2 - 1 = 65532。
|
||||||
|
|
||||||
|
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
|
||||||
|
|
||||||
|
## 行溢出后,MySQL 是怎么处理的?
|
||||||
|
|
||||||
|
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。
|
||||||
|
|
||||||
|
Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
|
||||||
|
|
||||||
|
Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
|
||||||
|
|
||||||
|
## B+索引
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
相比于标准的B+树,InnoDB使用的B+树有如下特点:
|
||||||
|
|
||||||
|
- B+ 树的叶子节点之间是用「双向链表」进行连接,既能向右遍历、也能向左遍历
|
||||||
|
- B+ 树点节点内容是数据页,数据页里存放了用户的记录以及各种信息,每个数据页默认大小是 16 KB
|
||||||
|
|
||||||
|
|
121
技术/MySQL/基础.md
Normal file
121
技术/MySQL/基础.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
## 1. 关系型数据库和非关系型数据库的区别
|
||||||
|
|
||||||
|
| **对比维度** | **关系型数据库** | **非关系型数据库** |
|
||||||
|
|--------------------|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
|
||||||
|
| **数据存储方式** | 数据以二维表格形式存储,结构化组织,强调行和列的关系 。 | 存储方式多样,如键值对、文档(JSON)、列族或图结构,适合非结构化或半结构化数据 。 |
|
||||||
|
| **数据模型** | 基于关系模型,强调数据的一致性和完整性 。 | 数据模型灵活,支持分布式架构,适合动态变化的数据需求 。 |
|
||||||
|
| **事务特性** | 遵循ACID原则(原子性、一致性、隔离性、持久性),确保强一致性 。 | 基于CAP理论(一致性、可用性、分区容错性),通常牺牲部分一致性以换取高可用性和扩展性 。 |
|
||||||
|
| **扩展性** | 通常采用垂直扩展(增加硬件性能),扩展性有限 。 | 支持水平扩展(增加节点),适合大规模分布式系统,扩展性更强 。 |
|
||||||
|
| **查询语言** | 使用SQL(结构化查询语言),通用性强且易于理解 。 | 通常使用特定API或查询语言,灵活性更高但学习成本较大 。 |
|
||||||
|
| **适用场景** | 适合需要复杂查询、事务处理和强一致性的场景,如银行系统、ERP等 。 | 适合大数据、高并发、实时性要求高的场景,如社交网络、物联网等 。 |
|
||||||
|
|
||||||
|
## 2. 为什么我们需要索引
|
||||||
|
|
||||||
|
* 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
|
||||||
|
* 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
|
||||||
|
* 帮助服务器避免排序和临时表
|
||||||
|
* 将随机IO变为顺序IO。
|
||||||
|
* 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
|
||||||
|
|
||||||
|
## 3. mysql优化了解吗-说一下从哪些方面可以做到性能优化
|
||||||
|
|
||||||
|
* 为搜索字段创建索引
|
||||||
|
* 避免使用 Select \*,列出需要查询的字段
|
||||||
|
* 垂直分割分表
|
||||||
|
* 选择正确的存储引擎
|
||||||
|
|
||||||
|
## 隔离级别和问题避免
|
||||||
|
|
||||||
|
* 脏读:读到其他事务未提交的数据;
|
||||||
|
* 不可重复读:前后读取的数据不一致;
|
||||||
|
* 幻读:前后读取的记录数量不一致。
|
||||||
|
|
||||||
|
* 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
|
||||||
|
* 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
|
||||||
|
* 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
|
||||||
|
* 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
|
||||||
|
|
||||||
|
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|
||||||
|
| --------------------- | --- | ----- | --- |
|
||||||
|
| READ-UNCOMMITTED 未提交读 | √ | √ | √ |
|
||||||
|
| READ-COMMITTED 提交读 | × | √ | √ |
|
||||||
|
| REPEATABLE-READ 重复读 | × | × | √ |
|
||||||
|
| SERIALIZABLE 可串行化读 | × | × | × |
|
||||||
|
|
||||||
|
|
||||||
|
## Mysql有哪些日志,简单概括有什么用
|
||||||
|
MySQL 中有多种日志,每种日志的作用各不相同,以下是它们的简单概括:
|
||||||
|
|
||||||
|
1. **Binlog(二进制日志)**
|
||||||
|
- **作用**:记录所有对数据库的修改操作(DDL 和 DML 语句),但不包括查询语句(如 SELECT、SHOW)。主要用于数据恢复、主从复制和审计 。
|
||||||
|
- **特点**:以二进制格式存储,支持 STATEMENT、ROW 和 MIXED 三种模式记录 。
|
||||||
|
|
||||||
|
2. **Redo Log(重做日志)**
|
||||||
|
- **作用**:保证事务的持久性。记录的是数据页的物理修改,用于在 MySQL 崩溃后恢复未写入磁盘的数据(即“崩溃恢复”)。
|
||||||
|
- **特点**:循环写入,固定大小,保存未刷入磁盘的脏页日志 。
|
||||||
|
|
||||||
|
3. **Undo Log(回滚日志)**
|
||||||
|
- **作用**:保证事务的原子性。记录的是事务执行前的数据状态,用于回滚操作或实现 MVCC(多版本并发控制)。
|
||||||
|
- **特点**:与 Redo Log 配合使用,确保事务的一致性和隔离性。
|
||||||
|
|
||||||
|
|
||||||
|
## 执行一条语句操作日志的完整过程
|
||||||
|
具体更新一条记录 `UPDATE t_user SET name = 'xiaolin' WHERE id = 1;` 的流程如下:
|
||||||
|
|
||||||
|
1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
|
||||||
|
* 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
|
||||||
|
* 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
|
||||||
|
2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
|
||||||
|
* 如果一样的话就不进行后续更新流程;
|
||||||
|
* 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
|
||||||
|
3. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
|
||||||
|
4. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
|
||||||
|
5. 至此,一条记录更新完了。
|
||||||
|
6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
|
||||||
|
7. 事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):
|
||||||
|
8. prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
|
||||||
|
9. commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
|
||||||
|
10. 至此,一条更新语句执行完成。
|
||||||
|
|
||||||
|
|
||||||
|
## 介绍MVCC的原理
|
||||||
|
|
||||||
|
MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。
|
||||||
|
|
||||||
|
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
|
||||||
|
|
||||||
|
- 「读提交」隔离级别是在「每个select语句执行前」都会重新生成一个 Read View;
|
||||||
|
- 「可重复读」隔离级别是执行第一条select时,生成一个 Read View,然后整个事务期间都在用这个 Read View。
|
||||||
|
|
||||||
|
Read View 有四个重要的字段:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的**事务 id 列表**,注意是一个列表,**“活跃事务”指的就是,启动了但还没提交的事务**。
|
||||||
|
- min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 **id 最小的事务**,也就是 m_ids 的最小值。
|
||||||
|
- max_trx_id :这个并不是 m_ids 的最大值,而是**创建 Read View 时当前数据库中应该给下一个事务的 id 值**,也就是全局事务中最大的事务 id 值 + 1;
|
||||||
|
- creator_trx_id :指的是**创建该 Read View 的事务的事务 id**。
|
||||||
|
|
||||||
|
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会**把该事务的事务 id 记录在 trx_id 隐藏列里**;
|
||||||
|
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后**这个隐藏列是个指针,指向每一个旧版本记录**,于是就可以通过它找到修改前的记录。
|
||||||
|
|
||||||
|
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
|
||||||
|
|
||||||
|
- 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View **前**已经提交的事务生成的,所以该版本的记录对当前事务**可见**。
|
||||||
|
|
||||||
|
- 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View **后**才启动的事务生成的,所以该版本的记录对当前事务**不可见**。
|
||||||
|
|
||||||
|
- 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
|
||||||
|
|
||||||
|
- 如果记录的 trx_id **在** m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务**不可见**。
|
||||||
|
- 如果记录的 trx_id **不在** m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务**可见**。
|
||||||
|
|
||||||
|
**这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)**
|
109
技术/Redis/Redis.md
Normal file
109
技术/Redis/Redis.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
## 常见的几种网络模型?
|
||||||
|
### 阻塞 IO
|
||||||
|
|
||||||
|
- 过程 1:应用程序想要去读取数据,他是无法直接去读取磁盘数据的,他需要先到内核里边去**等待内核操作硬件**拿到数据,这个等待数据就绪的过程便是过程1。
|
||||||
|
|
||||||
|
- 过程 2:内核态准备好了,开始拷贝数据给用户缓冲区,便是过程2。 
|
||||||
|
|
||||||
|
|
||||||
|
用户去读取数据时,会去先发起 `recvform` 一个命令,去尝试从内核上加载数据,如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回 ok,整个过程,都是阻塞等待的,这就是阻塞 IO
|
||||||
|
|
||||||
|
> 也就是两个过程都阻塞的话,便是阻塞IO
|
||||||
|
|
||||||
|
### 非阻塞 IO
|
||||||
|
顾名思义,非阻塞 IO 的 **recvfrom 操作会立即返回结果**而不是阻塞用户进程。
|
||||||
|
|
||||||
|
阶段一:
|
||||||
|
|
||||||
|
- 用户进程尝试读取数据(比如网卡数据)
|
||||||
|
- 此时数据尚未到达,内核需要等待数据
|
||||||
|
- 返回**异常**给用户进程
|
||||||
|
- **用户进程收到 error 后,再次尝试读取【忙轮询】**
|
||||||
|
- 循环往复,直到数据就绪
|
||||||
|
|
||||||
|
阶段二:
|
||||||
|
|
||||||
|
- 将内核数据拷贝到用户缓冲区
|
||||||
|
- 拷贝过程中,用户进程**依然阻塞等待**
|
||||||
|
- 拷贝完成,用户进程解除阻塞,处理数据
|
||||||
|
|
||||||
|
> 可以看到,非阻塞 IO 模型中,用户进程在**第一个阶段是非阻塞,第二个阶段是阻塞状态**。虽然是非阻塞,但性能并没有得到提高。而且**忙等机制**会导致 **CPU 空转,CPU 使用率暴增。**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 信号驱动
|
||||||
|
|
||||||
|
信号驱动 IO 是与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 就绪时,会发出 SIGIO 信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
|
||||||
|
|
||||||
|
阶段一:
|
||||||
|
|
||||||
|
- 用户进程调用 sigaction ,**注册信号处理函数**
|
||||||
|
- 内核返回成功,开始监听 FD
|
||||||
|
- 用户进程不阻塞等待,可以执行其它业务
|
||||||
|
- 当内核数据就绪后,回调用户进程的 SIGIO 处理函数
|
||||||
|
|
||||||
|
阶段二:
|
||||||
|
|
||||||
|
- 收到 SIGIO 回调信号
|
||||||
|
- 调用 recvfrom ,读取
|
||||||
|
- 内核将数据拷贝到用户空间
|
||||||
|
- 用户进程处理数据
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 缺点
|
||||||
|
|
||||||
|
当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致**信号队列溢出**,而且内核空间与用户空间的频繁信号交互性能也较低。
|
||||||
|
|
||||||
|
|
||||||
|
### IO多路复用
|
||||||
|
|
||||||
|
IO多路复用的实现可以通过操作系统提供的不同系统调用来实现,其中最常用的有**select**、**poll**和**epoll**。
|
||||||
|
|
||||||
|
1. **select**: select是最古老的IO多路复用机制,它使用一个文件描述符集合来监听多个IO事件的就绪状态。应用程序需要将需要监听的文件描述符添加到集合中,然后调用select函数进行监听。当有文件描述符就绪时,select函数会返回,并告知哪些文件描述符已经准备好进行读取或写入操作。然后应用程序可以通过遍历文件描述符集合来处理就绪的IO事件。
|
||||||
|
|
||||||
|
2. **poll**: poll是select的改进版本,它也使用一个文件描述符集合来监听多个IO事件的就绪状态。与select不同的是,poll不需要每次调用都将文件描述符集合传递给内核,而是使用一个pollfd结构体数组来传递。应用程序需要将需要监听的文件描述符和事件类型添加到pollfd数组中,然后调用poll函数进行监听。当有文件描述符就绪时,poll函数会返回,并告知哪些文件描述符已经准备好进行读取或写入操作。然后应用程序可以通过遍历pollfd数组来处理就绪的IO事件。
|
||||||
|
|
||||||
|
3. **epoll**: epoll是Linux特有的IO多路复用机制,它使用一个内核事件表来管理和监听多个IO事件的就绪状态。应用程序需要将需要监听的文件描述符添加到内核事件表中,然后调用epoll_wait函数进行监听。当有文件描述符就绪时,epoll_wait函数会返回,并告知哪些文件描述符已经准备好进行读取或写入操作。与select和poll不同的是,epoll使用回调函数来处理就绪的IO事件,而不需要应用程序遍历事件列表。
|
||||||
|
|
||||||
|
| **机制** | **原理** | **优点** | **缺点** |
|
||||||
|
| ---------- | ---------------------------- | ------------------ | ------------------------- |
|
||||||
|
| **select** | 轮询所有描述符,返回就绪的集合 | 跨平台兼容性好 | 描述符数量受限(通常 1024),性能随连接数下降 |
|
||||||
|
| **poll** | 类似`select`,但使用链表存储描述符,无数量限制 | 解决文件描述符数量限制 | 仍需轮询,效率低(O(n) 复杂度) |
|
||||||
|
| **epoll** | 事件驱动机制,仅返回就绪的描述符,通过红黑树和事件表优化 | 高效处理万级连接(O(1) 复杂度) | 仅适用于 Linux |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 跳表
|
||||||
|
|
||||||
|
通过建立索引的方式,对于数据量越大的有序链表,通过建立多级索引,查找效率提升会非常明显。
|
||||||
|
这种**链表加多级索引的结构** 就是 **跳表**。
|
||||||
|
|
||||||
|
Redis中的 有序集合 **zset** 就是用跳表实现的。
|
||||||
|
|
||||||
|
> **跳表中操作的时间复杂度**就是**O(logn).** 与二分查找的时间复杂度相同。
|
||||||
|
|
||||||
|
基于单链表实现了二分查找,查询效率的提升依赖构建了多级索引,是一种空间换时间的设计思路。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## MySQL为什么用B+树,而不是跳表
|
||||||
|
|
||||||
|
**MySQL是持久化数据库、即存储到磁盘上,因此查询时要求更少磁盘 IO,且 Mysql 是读多写少的场景较多,显然 B+ 树更加适合Mysql。**[[MySQL相关#B+索引]]
|
||||||
|
|
||||||
|
Redis是直接操作内存的、并不需要磁盘io;而MySQL需要去读取磁盘io,所以MySQL使用b+树的方式去减少磁盘io。B+树原理是 叶子节点存储数据、非叶子节点存储索引,每次读取磁盘页时就会读取一整个节点,每个叶子节点还要指向前后节点的指针,其目的是最大限度地降低磁盘io
|
||||||
|
|
||||||
|
数据在内存中读取 耗费时间是磁盘IO读取的百万分之一,而Redis是内存中读取数据、不涉及IO,因此使用了跳表,跳表模型是更快更简单的方式
|
||||||
|
|
||||||
|
- **时间复杂度优势**:跳表是一种基于链表的数据结构,可以在O(log n)的时间内进行插入、删除和查找操作。而B树需要维护平衡,操作的时间复杂度较高,通常为O(log n)或者更高。在绝大多数情况下,跳表的性能要优于B树。
|
||||||
|
- **简单高效**:跳表的实现相对简单,并且容易理解和调试。相比之下,B树的实现相对复杂一些,需要处理更多的情况,例如节点的分裂和合并等操作。
|
||||||
|
- **空间利用率高**:在关键字比较少的情况下,跳表的空间利用率要优于B树。B树通常需要每个节点存储多个关键字和指针,而跳表只需要每个节点存储一个关键字和一个指针。
|
||||||
|
- **并发性能好**:跳表的插入和删除操作比B树更加简单,因此在并发环境下更容易实现高性能。在多线程读写的情况下,跳表能够提供较好的并发性能。
|
||||||
|
## Hash、B+树、跳表的比较
|
||||||
|
|数据结构|实现原理|key查询方式|查找效率|存储大小|插入、删除效率|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|Hash|哈希表|支持单key|接近O(1)|小,除了数据没有额外的存储|O(1)|
|
||||||
|
|B+树|平衡二叉树扩展而来|单key,范围,分页|O(logn)|除了数据,还多了左右指针,以及叶子节点指针|O(logn),需要调整树的结构,算法比较复杂|
|
||||||
|
|跳表|有序链表扩展而来|单key,分页|O(logn)|除了数据,还多了指针,但是每个节点的指针小于<2,所以比B+树占用空间小|O(logn),只用处理链表,算法比较简单
|
16
技术/操作系统/OS.md
Normal file
16
技术/操作系统/OS.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
## 死锁是什么?怎么产生的?
|
||||||
|
|
||||||
|
死锁只有**同时满足**以下四个条件才会发生:
|
||||||
|
|
||||||
|
- 互斥条件:互斥条件是指**多个线程不能同时使用同一个资源**。
|
||||||
|
- 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是**线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1**。
|
||||||
|
- 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,**在自己使用完之前不能被其他线程获取**,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
|
||||||
|
- 环路等待条件:环路等待条件指的是,在死锁发生的时候,**两个线程获取资源的顺序构成了环形链**。
|
||||||
|
|
||||||
|
## 如何避免死锁?
|
||||||
|
|
||||||
|
避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是**使用资源有序分配法,来破环环路等待条件**。
|
||||||
|
|
||||||
|
那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
|
||||||
|
|
||||||
|

|
40
技术/项目提升.md
Normal file
40
技术/项目提升.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **技术深度拓展**
|
||||||
|
1. **分布式架构增强**
|
||||||
|
- **跨地域容灾**:采用Redis Cluster + MySQL主从跨地域部署,通过DNS解析或全局负载均衡(如Cloudflare)实现流量就近接入,结合知识库中提到的“全球融合式课堂”分布式理念。
|
||||||
|
- **服务网格化**:引入Istio管理微服务间的通信,实现流量监控、熔断降级,提升系统韧性。
|
||||||
|
|
||||||
|
2. **一致性保障**
|
||||||
|
- **缓存与数据库双写一致性**:通过MySQL Binlog + Canal订阅数据变更,异步更新Redis缓存,避免直接操作导致的数据不一致。
|
||||||
|
- **分布式事务**:对涉及资金或敏感操作的场景(如付费短链接),采用Seata框架实现Saga模式事务回滚。
|
||||||
|
|
||||||
|
3. **智能调度与负载均衡**
|
||||||
|
- **动态权重分配**:根据节点负载(CPU/内存)动态调整Nginx或Envoy的流量权重,参考知识库中“拓展近50家就业见习基地”的弹性扩容思路。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **业务创新方向**
|
||||||
|
1. **场景化功能扩展**
|
||||||
|
- **自定义短链接**:允许用户自定义短ID(如`domain.com/brand`),通过Redis的`SETNX`命令校验唯一性,类似“专精特新企业培育”中强调的定制化服务。
|
||||||
|
- **链接时效性策略**:支持按访问次数或时间周期自动失效,结合知识库中“过期策略”设计。
|
||||||
|
|
||||||
|
2. **数据分析与商业化**
|
||||||
|
- **访问统计看板**:利用Redis HyperLogLog统计UV/PV,或通过MySQL归档表记录访问日志,生成用户行为热力图(参考“跨境电商新模式”数据驱动理念)。
|
||||||
|
- **广告植入**:在短链接跳转页面嵌入广告位,通过A/B测试优化转化率,类似“拓展市场”的创新模式。
|
||||||
|
|
||||||
|
3. **开发者生态**
|
||||||
|
- **OpenAPI开放**:提供短链接生成/查询的RESTful API,集成OAuth2.0认证与速率限制(如令牌桶算法),参考“全球融合式课堂”的开放协作思想。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **安全与合规升级**
|
||||||
|
1. **隐私保护**
|
||||||
|
- **GDPR合规**:对用户提交的长链接进行敏感信息过滤(如手机号、身份证号),结合知识库中“链接过滤”建议。
|
||||||
|
- **加密传输**:强制HTTPS协议,使用Let's Encrypt自动管理证书,避免中间人攻击。
|
||||||
|
|
||||||
|
2. **风控体系**
|
||||||
|
- **动态IP信誉库**:基于Redis记录恶意IP的访问行为,结合速率限制(如每秒最多10次请求),参考“防暴破机制”设计。
|
||||||
|
- **链接沙箱检测**:对生成的短链接目标地址进行沙箱预执行,识别钓鱼或恶意内容。
|
108
技术/项目设计/分布式与系统一致性.md
Normal file
108
技术/项目设计/分布式与系统一致性.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
## 1、分布式系统基本概念
|
||||||
|
|
||||||
|
### 1、CAP理论基础
|
||||||
|
|
||||||
|
分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。
|
||||||
|
|
||||||
|
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:
|
||||||
|
|
||||||
|
> * Consistency
|
||||||
|
> * Availability
|
||||||
|
> * Partition tolerance
|
||||||
|
|
||||||
|
它们的第一个字母分别是 `C`、`A`、`P`。Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 `CAP` 定理。
|
||||||
|
|
||||||
|
它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
|
||||||
|
|
||||||
|
* 一致性(Consistency) :等同于所有节点访问同一份最新的数据副本,或者说同一数据在不同节点上的副本在同一逻辑时钟应当是相同的内容。
|
||||||
|
* 可用性(Availability):每次请求都能获取到非错的响应,以及尽量保证低延迟,但是不保证获取的数据为最新数据。
|
||||||
|
* 分区容错性(Partition tolerance):以实际效果而言,分区相当于对**通信的时限要求**。要求任意节点故障时,系统仍然可以对外服务。
|
||||||
|
|
||||||
|
### 2、数据一致性(C侧)
|
||||||
|
|
||||||
|
一些分布式系统通过复制数据来提高系统的可靠性和容错性,并且将数据的不同的副本存放在不同的机器,由于维护数据副本的一致性代价高,因此许多系统**采用弱一致性来提高性能**,一些不同的一致性模型也相继被提出。
|
||||||
|
|
||||||
|
* **强一致性**: 要求无论更新操作实在哪一个副本执行,之后所有的读操作都要能获得最新的数据。
|
||||||
|
* **弱一致性**:用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。
|
||||||
|
* **最终一致性**:是弱一致性的一种特例,保证用户\*\*最终(即窗口尽量长)\*\*能够读取到某操作对系统特定数据的更新。
|
||||||
|
|
||||||
|
#### 一致性解决方案
|
||||||
|
|
||||||
|
1. 分布式事务:两段提交
|
||||||
|
2. 分布式锁
|
||||||
|
3. 消息队列、消息持久化、重试、幂等操作
|
||||||
|
4. Raft / Paxos 等一致性算法
|
||||||
|
|
||||||
|
### 3、服务可用性(A侧)
|
||||||
|
|
||||||
|
可用性,意思是只要收到用户的请求,服务器就必须给出回应。
|
||||||
|
|
||||||
|
#### 高可用解决方案
|
||||||
|
|
||||||
|
* **负载均衡**:尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。
|
||||||
|
* **降级**:当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
|
||||||
|
* **熔断**:对于目标服务的请求和调用大量超时或失败,这时应该熔断该服务的所有调用,并且对于后续调用应直接返回,从而快速释放资源。确保在目标服务不可用的这段时间内,所有对它的调用都是立即返回的、不会阻塞的,等到目标服务好转后进行接口恢复。
|
||||||
|
* **流量控制**:流量控制可以有效的防止由于网络中瞬间的大量数据对网络带来的冲击,保证用户网络高效而稳定的运行,类似于TCP拥塞控制方法。
|
||||||
|
* **异地多活**:在不同地区维护不同子系统,并保证子系统的可用性
|
||||||
|
|
||||||
|
熔断是减少由于下游服务故障对自己的影响;而降级则是在整个系统的角度上,考虑业务整体流量,保护核心业务稳定。
|
||||||
|
|
||||||
|
### 4、分区容错性(P侧)
|
||||||
|
|
||||||
|
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
|
||||||
|
|
||||||
|
般来说,分区容错无法避免,因此可以认为 CAP 的 `P` 总是成立。CAP 定理告诉我们,剩下的 `C` 和 `A` 无法同时做到。
|
||||||
|
|
||||||
|
## 2、系统一致性
|
||||||
|
|
||||||
|
### 1、基本要求
|
||||||
|
|
||||||
|
规范的说,理想的分布式系统一致性应该满足:
|
||||||
|
|
||||||
|
1. 可终止性(Termination):一致的结果在有限时间内能完成;
|
||||||
|
2. 共识性(Consensus):不同节点最终完成决策的结果应该相同;
|
||||||
|
3. 合法性(Validity):决策的结果必须是其它进程提出的提案。
|
||||||
|
|
||||||
|
第一点很容易理解,这是计算机系统可以被使用的前提。需要注意,在现实生活中这点并不是总能得到保障的,例如取款机有时候会是 `服务中断` 状态,电话有时候是 `无法连通` 的。
|
||||||
|
|
||||||
|
第二点看似容易,但是隐藏了一些潜在信息。算法考虑的是任意的情形,凡事一旦推广到任意情形,就往往有一些惊人的结果。例如现在就剩一张票了,中关村和西单的电影院也分别刚确认过这张票的存在,然后两个电影院同时来了一个顾客要买票,从各自观察看来,自己的顾客都是第一个到的……怎么能达成结果的共识呢?记住我们的唯一秘诀:**核心在于需要把两件事情进行排序,而且这个顺序还得是合理的、大家都认可的**。
|
||||||
|
|
||||||
|
第三点看似绕口,但是其实比较容易理解,即达成的结果必须是节点执行操作的结果。仍以卖票为例,如果两个影院各自卖出去一千张,那么达成的结果就是还剩八千张,决不能认为票售光了。
|
||||||
|
|
||||||
|
### 2、强一致性
|
||||||
|
|
||||||
|
#### 线性一致性
|
||||||
|
|
||||||
|
线性一致性或称 **原子一致性** 或 **严格一致性** 指的是程序在执行的历史中在存在可线性化点P的执行模型,这意味着一个操作将在程序的调用和返回之间的某个点P起作用。这里“起作用”的意思是被系统中并发运行的所有其他线程所感知。要求如下:
|
||||||
|
|
||||||
|
1. **写后读** 这里写和读是两个操作,如果写操作在完成之后,读才开始,读要能读到最新的数据,而且保证以后也能读操作也都能读到这个最新的数据。
|
||||||
|
2. **所有操作的时序与真实物理时间一致**,要求即使不相关的两个操作,如果执行有先后顺序,线性一致性要求最终执行的结果也需要满足这个先后顺序。比如,操作序列(写A,读A,写B,读B),那么不仅,读A,读B能读到最新A值和B值;而且要保证,如果读B读到最新值时,读A一定也能读到最新值,也就是需要保证执行时序与真实时序相同。
|
||||||
|
3. 如果两个操作是并发的(比如读A没有结束时,写B开始了),那么这个并发时序不确定,但从最终执行的结果来看,要确保所有线程(进程,节点)看到的执行序列是一致的。
|
||||||
|
|
||||||
|
#### 顺序一致性
|
||||||
|
|
||||||
|
相比线性一致性,主要区别在于,**对于物理上有先后顺序的操作,不保证这个时序**。具体而言,对于单个线程,操作的顺序仍然要保留,对于多个线程(进程,节点),执行的事件的先后顺序与物理时钟顺序不保证。但是要求,从执行结果来看,所有线程(进程,节点)看到的执行序列是一样的。
|
||||||
|
|
||||||
|
#### 因果一致性
|
||||||
|
|
||||||
|
因果一致性,被认为是比`顺序一致性`更弱的一致性,在因果一致性中,只对**有因果关系的事件**有顺序要求。
|
||||||
|
|
||||||
|
### 3、带约束的一致性
|
||||||
|
|
||||||
|
绝对理想的 **强一致性(Strong Consistency)** 代价很大。除非不发生任何故障,所有节点之间的通信无需任何时间,这个时候其实就等价于一台机器了。实际上,越强的一致性要求往往意味着越弱的性能、越低的可用性。
|
||||||
|
|
||||||
|
强一致的系统往往比较难实现。很多时候,人们发现实际需求并没有那么强,可以适当放宽一致性要求,降低系统实现的难度。例如在一定约束下实现所谓 **最终一致性(Eventual Consistency)**,即总会存在一个时刻(而不是立刻),系统达到一致的状态,这对于大部分的 Web 系统来说已经足够了。这一类弱化的一致性,被笼统称为 **弱一致性(Weak Consistency)**。
|
||||||
|
|
||||||
|
#### 最终一致性
|
||||||
|
|
||||||
|
最终一致性也被称为 **乐观复制(optimistic replication)**,用户只能读到某次更新后的值,但系统保证数据将最终达到完全一致的状态,只是所需时间不能保障。这个达成一致所需要的时间,我们称为 **窗口时间**。
|
||||||
|
|
||||||
|
我们常见的 **异步复制的主从架构实现的是最终一致性** 。它的一个典型常见是用户读取异步从库时,可能读取到较旧的信息,因为该从库尚未完全与主库同步。注意,同步复制的主从架构会出现任一节点宕机导致的单点问题。
|
||||||
|
|
||||||
|
### 4、一致性(Consistency)与共识(Consensus)的关系
|
||||||
|
|
||||||
|
我们常说的 **一致性(Consistency)** 在分布式系统中指的是 `副本(Replication)` 问题中对于同一个数据的多个副本,其对外表现的数据一致性,如 `线性一致性` 、`因果一致性`、`最终一致性`等,都是用来描述副本问题中的一致性的。
|
||||||
|
|
||||||
|
而 **共识(Consensus)** 则不同,共识问题中所有的节点要最终达成共识,由于最终目标是所有节点都要达成一致,所以根本 **不存在一致性强弱** 之分。
|
||||||
|
|
||||||
|
 
|
77
技术/项目设计/鉴权和限流.md
Normal file
77
技术/项目设计/鉴权和限流.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
## 常用鉴权方式
|
||||||
|
|
||||||
|
我们常用的鉴权有四种:
|
||||||
|
|
||||||
|
1. HTTP Basic Authentication
|
||||||
|
|
||||||
|
2. session-cookie
|
||||||
|
|
||||||
|
3. Token 验证
|
||||||
|
|
||||||
|
4. OAuth(开放授权)
|
||||||
|
|
||||||
|
|
||||||
|
## 看到你项目用了JWT说一下原理。
|
||||||
|
|
||||||
|
JWT令牌由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。其中,头部和载荷均为JSON格式,使用Base64编码进行序列化,而签名部分是对头部、载荷和密钥进行签名后的结果。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
在传统的基于会话和Cookie的身份验证方式中,会话信息通常存储在服务器的内存或数据库中。但在集群部署中,不同服务器之间没有共享的会话信息,这会导致用户在不同服务器之间切换时需要重新登录,或者需要引入额外的共享机制(如Redis),增加了复杂性和性能开销。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
而JWT令牌通过在令牌中包含所有必要的身份验证和会话信息,使得服务器无需存储会话信息,从而解决了集群部署中的身份验证和会话管理问题。当用户进行登录认证后,服务器将生成一个JWT令牌并返回给客户端。客户端在后续的请求中携带该令牌,服务器可以通过对令牌进行验证和解析来获取用户身份和权限信息,而无需访问共享的会话存储。
|
||||||
|
|
||||||
|
由于JWT令牌是自包含的,服务器可以独立地对令牌进行验证,而不需要依赖其他服务器或共享存储。这使得集群中的每个服务器都可以独立处理请求,提高了系统的可伸缩性和容错性。
|
||||||
|
|
||||||
|
JWT 的缺点是一旦派发出去,在失效之前都是有效的,没办法即使撤销JWT。要解决这个问题的话,得在业务层增加判断逻辑,比如增加黑名单机制。使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 **黑名单** 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。
|
||||||
|
|
||||||
|
|
||||||
|
## 常用限流算法
|
||||||
|
|
||||||
|
### 1 . 计数器(固定窗口)算法
|
||||||
|
计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
|
||||||
|
|
||||||
|
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:
|
||||||
|

|
||||||
|
|
||||||
|
假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
|
||||||
|
|
||||||
|
### 2. 滑动窗口算法
|
||||||
|
滑动窗口算法是将时间周期分为N个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。
|
||||||
|
|
||||||
|
如下图,假设时间周期为1min,将1min再分为2个小周期,统计每个小周期的访问数量,则可以看到,第一个时间周期内,访问数量为75,第二个时间周期内,访问数量为100,超过100的访问则被限流掉了
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
|
||||||
|
|
||||||
|
此算法可以很好的解决固定窗口算法的临界问题。
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 漏桶算法
|
||||||
|
|
||||||
|
漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 4. 令牌桶算法
|
||||||
|
令牌桶算法是程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### **各个算法比较**
|
||||||
|
|
||||||
|
| 算法 | 确定参数 | 空间复杂度 | 时间复杂度 | 限制突发流量 | 平滑限流 | 分布式环境下实现难度 |
|
||||||
|
| ---- | ----------------------- | ------------------------------- | ----- | ------ | ------------------------------- | ---------- |
|
||||||
|
| 固定窗口 | 计数周期T、<br><br>周期内最大访问数N | 低O(1)<br><br>(记录周期内访问次数及周期开始时间) | 低O(1) | 否 | 否 | 低 |
|
||||||
|
| 滑动窗口 | 计数周期T、<br><br>周期内最大访问数N | 高O(N)<br><br>(记录每个小周期中的访问数量) | 中O(N) | 是 | 相对实现。滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑 | 中 |
|
||||||
|
| 漏桶 | 漏桶流出速度r、漏桶容量N | 低O(1)<br><br>(记录当前漏桶中容量) | 高O(N) | 是 | 是 | 高 |
|
||||||
|
| 令牌桶 | 令牌产生速度r、令牌桶容量N | 低O(1)<br><br>(记录当前令牌桶中令牌数) | 高O(N) | 是 | 是 | 高 |
|
19
技术/项目设计/高并发系统的设计.md
Normal file
19
技术/项目设计/高并发系统的设计.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
### 1、系统拆分
|
||||||
|
|
||||||
|
将一个系统拆分为多个子系统,用 RPC 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。(微服务)
|
||||||
|
|
||||||
|
### 2、缓存
|
||||||
|
|
||||||
|
大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟 Redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。
|
||||||
|
|
||||||
|
### 3、消息队列
|
||||||
|
|
||||||
|
可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改。那高并发绝对搞挂你的系统,你要是用 Redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 MySQL 还得用 MySQL 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,后边系统消费后慢慢写,控制在 MySQL 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。
|
||||||
|
|
||||||
|
### 4、分库分表
|
||||||
|
|
||||||
|
分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 SQL 跑的性能。
|
||||||
|
|
||||||
|
### 5、读写分离
|
||||||
|
|
||||||
|
读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。
|
Loading…
x
Reference in New Issue
Block a user