编程笔记

lifelong learning & practice makes perfect

sync.RWMutex 的基本特性

  • 值类型: sync.RWMutex 是一个 struct。这意味着如果你直接传递它,你将得到一个副本。对副本的锁定操作不会影响原始的 mutex,这会导致同步失效。
  • 不应在第一次使用后复制: Go 官方文档明确指出,sync.Mutex (以及 sync.RWMutex) 不应在第一次使用(即调用 Lock/UnlockRLock/RUnlock)后进行复制。复制会导致未定义的行为,通常是死锁或 panic。
  • 零值可用: sync.RWMutex{} (零值) 是一个有效的、可直接使用的 mutex。

在局部函数中创建并返回 *sync.RWMutex 的情况

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

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

// 情况 A: 函数内部创建 RWMutex 并返回其指针
func createAndReturnRWMutex() *sync.RWMutex {
var mu sync.RWMutex // 局部变量,Go的逃逸分析会将其分配到堆上
fmt.Println("Mutex created at address:", &mu)
return &mu
}

func main() {
// 获取由 createAndReturnRWMutex 函数创建的 mutex 指针
mu1 := createAndReturnRWMutex()
mu2 := createAndReturnRWMutex() // 再次调用,会得到一个新的 mutex

fmt.Println("Returned mu1 address:", mu1)
fmt.Println("Returned mu2 address:", mu2)

// 使用 mu1
mu1.RLock()
fmt.Println("mu1 RLocked")
go func() {
mu1.Lock() // 这个地方会等待上面的 RLock 释放
fmt.Println("mu1 Locked by goroutine")
time.Sleep(100 * time.Millisecond)
mu1.Unlock()
fmt.Println("mu1 Unlocked by goroutine")
}()
time.Sleep(50 * time.Millisecond) // 等待 goroutine 尝试加锁
mu1.RUnlock()
fmt.Println("mu1 RUnlocked")

// 确保 goroutine 完成
time.Sleep(200 * time.Millisecond)

// 使用 mu2 (与 mu1 独立)
mu2.Lock()
fmt.Println("mu2 Locked")
mu2.Unlock()
fmt.Println("mu2 Unlocked")
}

分析:

  1. 技术上可行: Go 的逃逸分析会识别到局部变量 mu 的地址被返回了函数外部,因此它会被分配到堆上(heap),而不是栈上。所以,返回的指针是有效的,不会指向已被销毁的内存。
  2. 不常见,且通常不是好的设计模式:
    • 封装性差: sync.RWMutex 的核心作用是保护共享数据。它几乎总是作为某个数据结构(struct)的字段而存在,由该数据结构的方法来管理其锁定和解锁,以保护该结构体内部的数据。
    • 职责不清: 如果一个函数仅仅返回一个 *sync.RWMutex,那么这个 mutex 是为了保护什么数据?这个信息并没有被封装起来,使用者需要自己去追踪这个 mutex 的用途,这增加了代码的复杂性和出错的风险。
    • 容易误用: 调用者可能会不清楚这个 mutex 应该保护什么数据,或者在不适当的时候加锁/解锁,导致死锁、竞争条件等问题。
    • 代码可读性差: 相比于一个明确的结构体(例如 SafeCache)包含一个 RWMutex,仅仅返回一个 RWMutex 指针会让代码意图模糊。

在局部函数中返回已存在*sync.RWMutex 指针

另一种情况是,函数不是创建新的 RWMutex,而是返回一个指向已经存在的 RWMutex 的指针,比如从一个全局变量或者传入参数的结构体中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "sync"

var globalMu sync.RWMutex

type MyContainer struct {
Mu sync.RWMutex
Data int
}

// 情况 B: 返回全局 RWMutex 的指针
func getGlobalRWMutex() *sync.RWMutex {
return &globalMu
}

// 情况 C: 返回结构体字段 RWMutex 的指针
func getContainerRWMutex(c *MyContainer) *sync.RWMutex {
return &c.Mu
}

func main() {
gMu := getGlobalRWMutex()
gMu.Lock()
// ...
gMu.Unlock()

container := &MyContainer{}
cMu := getContainerRWMutex(container)
cMu.RLock()
// ...
cMu.RUnlock()
}

分析:

  • 技术上可行: 同样,返回的指针是有效的。
  • 合适性存疑:
    • 暴露内部实现: 这种做法打破了封装性。一个结构体通常应该通过其方法来提供对数据(包括同步机制)的受控访问,而不是直接暴露其内部的 sync.RWMutex 字段。
    • 耦合性增加: 外部代码直接操作内部 mutex 会增加模块间的耦合,使得结构体的内部实现更难修改。
    • 潜在的API风险: 如果 MyContainer 的设计者希望控制对 Data 的访问方式,而调用者直接使用 c.Mu 进行锁操作,可能会绕过 MyContainer 提供的方法,导致不一致或错误的状态。

推荐做法

在 Go 中,管理 sync.RWMutex惯用和最佳实践是将其嵌入到一个结构体中,并通过该结构体的方法来管理锁定和解锁,从而保护结构体自身的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"sync"
)

// SafeCounter 是一个线程安全的计数器
type SafeCounter struct {
mu sync.RWMutex // RWMutex 作为结构体的字段
count int
}

// Inc 增加计数器的值
func (sc *SafeCounter) Inc() {
sc.mu.Lock() // 方法内部管理锁
defer sc.mu.Unlock()
sc.count++
}

// Value 返回当前计数器的值
func (sc *SafeCounter) Value() int {
sc.mu.RLock() // 方法内部管理读锁
defer sc.mu.RUnlock()
return sc.count
}

// NewSafeCounter 是 SafeCounter 的构造函数
func NewSafeCounter() *SafeCounter {
// RWMutex 的零值是可用的,无需额外初始化
return &SafeCounter{}
}

func main() {
counter := NewSafeCounter() // 返回的是 *SafeCounter,而不是 *sync.RWMutex

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc() // 通过 SafeCounter 的方法访问计数器,由方法处理同步
}()
}

wg.Wait()
fmt.Println("Final Counter Value:", counter.Value())
}

总结

在Go语言中,一般不推荐在局部函数中创建 sync.RWMutex 并单独返回其指针。这通常表示设计上存在封装性不足的问题。

更推荐的做法是:

  1. sync.RWMutex 作为你希望保护的结构体的字段。
  2. 通过该结构体的方法来封装对数据的访问和 sync.RWMutex 的锁定/解锁操作。
  3. 如果需要提供一个同步的实例,你的构造函数应该返回一个指向该结构体实例的指针(例如 *SafeCounter),而不是单独的 *sync.RWMutex 指针。

这样做可以提高代码的封装性、可读性和健壮性,减少误用的可能性。

Go 语言自 1.18 版本开始支持泛型(Generics)。泛型允许我们为函数和数据结构定义类型参数,增强代码的复用性和类型安全性。然而,泛型并不是在所有情况下都适用,正确地使用泛型能带来代码清晰性和性能提升,错误使用则可能使代码复杂或降低性能。

本文旨在帮助你了解在什么情况下应该考虑使用泛型。

泛型的优点

  1. 代码复用性:泛型允许用一种实现支持多种类型,减少代码重复。
  2. 类型安全:在编译时检查类型,防止运行时类型错误。
  3. 抽象能力:抽象地处理数据结构和算法,提升代码表达力。
  4. 性能:避免因为使用interface{}类型带来的类型断言,从而减少运行时开销。

何时使用泛型?

泛型适用于以下场景:

  • 你有一组算法或数据结构逻辑,它们在不同的类型上运作,但逻辑完全相同。
  • 你想写高度通用的代码来简化代码库,同时保持类型安全。
  • 你的代码需要同时处理多种类型,但不想牺牲性能。
  • 你希望避免冗余代码,减少维护工作量。

何时不适合使用泛型?

泛型不是万金油,某些情况下反而带来负担:

  • 只有单一具体类型需求时,泛型会增加代码复杂度。
  • 泛型代码过于复杂,导致可读性变差,增加维护难度。
  • 性能占优且简单逻辑可以接受轻微重复代码时,不必强制用泛型。
  • 当泛型参数变得过于复杂(过多约束、嵌套)时,可能适得其反。

不要把interface类型替换为类型参数

Go语言有interface类型,interface支持某种意义上的泛型编程。

举个例子,被广泛使用的io.Reader接口提供了一种泛型机制用于读取数据,比如支持从文件和随机数生成器里读取数据。

如果你对某些类型的变量的操作只是调用该类型的方法,那就直接使用interface类型,不要使用类型参数。io.Reader从代码角度易于阅读且高效,没必要使用类型参数。

举个例子,有人可能会把下面第1个基于interface类型的ReadSome版本修改为第2个基于类型参数的版本。

1
2
3
func ReadSome(r io.Reader) ([]byte, error)

func ReadSome[T io.Reader](r T) ([]byte, error)

不要做这种修改,使用第1个基于interface的版本会让函数更容易编写和阅读,并且函数执行效率也几乎一样。

注意:尽管可以使用不同的方式来实现泛型,并且泛型的实现可能会随着时间的推移而发生变化,但是Go 1.18中泛型的实现在很多情况下对于类型为interface的变量和类型为类型参数的变量处理非常相似。这意味着使用类型参数通常并不会比使用interface快,所以不要单纯为了程序运行速度而把interface类型修改为类型参数,因为它可能并不会运行更快。

用例示例

例如处理一组不同类型的切片排序、查找操作,可以使用泛型函数避免重复编写针对不同类型的代码。

非泛型版本

1
2
func IntContains(slice []int, val int) bool { ... }
func StringContains(slice []string, val string) bool { ... }

上述重复很多类似逻辑。

泛型版本

1
func Contains[T comparable](slice []T, val T) bool { ... }

案例2: Go内置的容器类型

入参是一个map,要返回该map的所有key组成的slice,key的类型可以是map支持的任意key类型。

1
2
3
4
5
6
7
8
9
// MapKeys returns a slice of all the keys in m.
// The keys are not returned in any particular order.
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {
s := make([]Key, 0, len(m))
for k := range m {
s = append(s, k)
}
return s
}

这段代码没有对map里key的类型做任何限定,并且没有用map里的value,因此这段代码适用于所有的map类型。这就是使用类型参数的一个很好的示例。

这种场景下,也可以使用反射(reflection),但是反射是一种比较别扭的编程模型,在编译期没法做静态类型检查,并且会导致运行期的速度变慢。

结语

泛型是强大的工具,但需用时权衡复杂度与收益。建议代码在泛型和具体类型之间平衡。合适时用泛型,实现代码复用和类型安全;不合适则回归简单可理解方案。

如果你正在考虑是否使用泛型,问自己:

  • 这段代码是否适用于多种类型?
  • 是否存在类型重复代码?

参考

redis

“两大维度”就是指系统维度和应用维度,“三大主线”也就是指高性能、高可靠和高可扩展(可以简称为“三高”)

系统维度

需要了解 Redis 的各项关键技术的设计原理

三大主线

  • 高性能: 线程模型、数据结构、持久化、网络框架;
  • 高可靠: 主从复制、哨兵机制;
  • 高可扩展: 数据分片、负载均衡。

常见问题

redis

文章指出特斯拉利用激光雷达车队在奥斯汀等地收集数据,并非放弃纯视觉路线,而是为Robotaxi建立高精度的地面实况(Ground Truth)。通过时间戳同步技术,将激光雷达的精确测量值与摄像头图像自动配对,高效生成海量训练数据,从而“教导”纯视觉系统独立完成距离和速度估算,加速自动驾驶的实现。

阅读全文 »

《日经亚洲》报道,来自八个国家14个学术机构的研究人员被发现在论文中隐藏提示,以操纵AI审稿人给出正面评价,引发了关于学术诚信和AI在同行评审中作用的激烈辩论。

阅读全文 »

下面的sql查询同一张表的数据,差异仅在where条件不同,但是同样是active =true的记录在返回的结果中,排序顺序不同,这是为什么

1
2
3
SELECT id,name FROM `tag` WHERE corp_id =4 ORDER BY active DESC LIMIT 10;

SELECT id,name FROM `tag` WHERE (corp_id =4) AND active =true ORDER BY active DESC LIMIT 10;

这两条SQL语句返回的数据顺序不一致的原因在于第一条SQL语句中 active 列可能存在除了 true (或 1) 以外的其他值,比如 false (或 0) 或者 NULL

让我们更详细地分析一下:

SQL 1:

1
SELECT id, name FROM `tag` WHERE corp_id = 4 ORDER BY active DESC LIMIT 10;
  • WHERE corp_id = 4: 首先过滤出 corp_id 为 4 的所有行。
  • ORDER BY active DESC: 然后按照 active 列降序排序。 这意味着:
    • true (或 1) 的行会排在前面。
    • false (或 0) 的行会排在 true 之后。
    • NULL 的行会排在 false 之前,但具体位置取决于数据库实现。 MySQL会默认把 NULL 值放在最前面
  • LIMIT 10: 最后只返回排序后的前10行。

SQL 2:

1
SELECT id, name FROM `tag` WHERE (corp_id = 4) AND active = true ORDER BY active DESC LIMIT 10;
  • WHERE corp_id = 4 AND active = true: 首先过滤出 corp_id 为 4 并且 activetrue 的所有行。
  • ORDER BY active DESC: 然后按照 active 列降序排序。 由于 WHERE 子句已经确保了所有行的 active 都是 true,所以这个排序实际上是多余的,因为所有行的 active 值都相同。
  • LIMIT 10: 最后只返回排序后的前10行。

关键区别在于WHERE子句的过滤条件:

  • 第一条SQL语句包含了所有 corp_id = 4 的行,然后根据 active 进行排序,NULL值可能在最前面。
  • 第二条SQL语句只包含了 corp_id = 4 并且 active = true 的行。 排序没有实际作用。

举例说明:

假设你的 tag 表有以下数据(简化):

id name corp_id active
1 A 4 true
2 B 4 false
3 C 4 NULL
4 D 4 true
5 E 4 false
6 F 4 NULL
7 G 4 true
8 H 4 false
9 I 4 NULL
10 J 4 true
11 K 4 false

SQL 1 的结果 (可能):

假设 MySQL 把 NULL 放在最前面排序

  1. id = 3, name = C (active = NULL)
  2. id = 6, name = F (active = NULL)
  3. id = 9, name = I (active = NULL)
  4. id = 1, name = A (active = true)
  5. id = 4, name = D (active = true)
  6. id = 7, name = G (active = true)
  7. id = 10, name = J (active = true)
  8. id = 2, name = B (active = false)
  9. id = 5, name = E (active = false)
  10. id = 8, name = H (active = false)

SQL 2 的结果:

  1. id = 1, name = A (active = true)
  2. id = 4, name = D (active = true)
  3. id = 7, name = G (active = true)
  4. id = 10, name = J (active = true)
    … 以及其他 active = true 的行,直到达到10行。 因为排序没意义,结果具有不确定性,取决于数据库内部的行存储顺序。

总结:

  • 如果 active 列只包含 truefalse,并且没有 NULL 值,那么这两条SQL语句的结果在 idname 上可能相同,但在结果排序上可能不同(第一条语句明确排序了,第二条排序实质上无效)。
  • 如果 active 列包含 NULL 值,这是最可能导致结果不同的原因,因为第一条语句会受到NULL值排序的影响。

如何解决:

如果你的意图是只获取 activetrue 的记录,那么第二条SQL语句是正确的选择。

如果你想要获取所有 corp_id = 4 的记录,并让 activetrue 的记录排在最前面,可以使用以下SQL语句(假设你想把 NULL 值放在最后):

1
2
3
4
5
6
7
8
9
10
SELECT id, name
FROM `tag`
WHERE corp_id = 4
ORDER BY
CASE
WHEN active IS NULL THEN 1 -- 把 NULL 放在最后
ELSE 0 -- 把 true 和 false 放在前面
END,
active DESC -- 然后按照 true > false 排序
LIMIT 10;

或者,更简单的使用 IFNULL 函数(MySQL 特性):

1
2
3
4
5
SELECT id, name
FROM `tag`
WHERE corp_id = 4
ORDER BY IFNULL(active, 0) DESC
LIMIT 10;

这条语句将 NULL 视为 0 (false),从而将 true 的行放在最前面。 如果你想将 NULL 视为 True, 将 0 改为 1 即可.

或者,增加排序字段,用id和active同时排序:

1
2
3
4
5
6
7
8
SELECT id,name 
FROM `tag`
WHERE corp_id =4
ORDER BY id,active DESC LIMIT 10;

SELECT id,name
FROM `tag` WHERE (corp_id =4) AND active =true
ORDER BY id,active DESC LIMIT 10;

选择哪种方法取决于你想要如何处理 active 列中的 NULL 值。 最重要的是理解你的数据和你想达到的结果,然后选择正确的SQL语句。

在 Elasticsearch 中,向已有索引的 mapping 里新增字段时,如果你尝试添加一个已经存在的字段(即字段名重复),会出现以下情况:

  • 不能修改已存在字段的类型:Elasticsearch 不允许修改已存在字段的类型或映射配置。如果你试图用不同的类型或属性重新定义已存在字段,操作会失败并报错,因为字段映射一旦确定,不能更改[3][5]。

  • 如果新增字段名和已有字段完全一致且映射相同,则相当于“重复添加”,这通常不会有实际影响,但也不会做任何修改,mapping 保持不变。

  • 如果新增字段名重复但映射不同,Elasticsearch 会拒绝更新 mapping,返回错误提示,防止数据索引混乱[3][5]。

  • 新增字段时,必须保证字段名唯一且映射合理,否则需要新建索引并通过 Reindex API 迁移数据来实现字段类型变更[3][5]。

总结:

操作场景 结果说明
新增字段名不存在 成功添加字段到 mapping
新增字段名已存在且映射相同 无变化,mapping 不会重复添加
新增字段名已存在但映射不同 报错,更新失败,不能修改字段类型

因此,新增字段时如果字段名重复且映射不同,ES 会拒绝更新 mapping 并报错,你需要通过新建索引和重新索引数据来变更字段类型。

这是 Elasticsearch 设计的限制,保证倒排索引结构的稳定性和数据一致性[3][5]。

[1] https://codeshellme.github.io/2021/02/es-mappings/
[2] https://blog.csdn.net/weixin_48990070/article/details/120342866
[3] http://masikkk.com/article/Elasticsearch-Mapping/
[4] http://www.zbpblog.com/blog-458.html
[5] https://www.cnblogs.com/wupeixuan/p/12514843.html
[6] https://www.cnblogs.com/shoufeng/p/10648835.html
[7] https://blog.csdn.net/yxd179/article/details/82907796
[8] https://scsundefined.gitbooks.io/elasticsearch-reference-cn/content/s12/00_mapping.html

Elasticsearch 中 textkeyword 是两种常用的字符串字段类型,它们的主要区别在于是否进行分词,进而影响索引和查询行为。

1. textkeyword 的区别

特性 text keyword
是否分词 会分词,进行全文分析 不分词,整体作为一个词项索引
适用场景 需要全文检索、模糊查询、相关度排序 需要精确匹配、过滤、排序、聚合
支持的查询类型 matchmatch_phrase 等全文查询 termterms 精确查询
支持聚合/排序 不支持(性能差且不合理) 支持
存储限制 无字符长度限制 默认最大长度256字符,超过不索引(可配置)
典型用途 文章内容、评论、描述等长文本 用户名、邮箱、标签、状态、ID等

2. 使用案例

2.1 Mapping 示例(含 multi-fields)

通常为了兼顾全文检索和精确匹配,字段会定义成 text 类型,同时添加一个 keyword 子字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}

这样,title 字段既可以全文检索,也可以做精确匹配和聚合。

2.2 查询示例

  • 全文检索(text 字段)
1
2
3
4
5
6
7
8
GET my_index/_search
{
"query": {
"match": {
"title": "Elasticsearch tutorial"
}
}
}
  • 精确匹配(keyword 字段)
1
2
3
4
5
6
7
8
GET my_index/_search
{
"query": {
"term": {
"title.keyword": "Elasticsearch tutorial"
}
}
}
  • 聚合示例(keyword 字段)
1
2
3
4
5
6
7
8
9
10
11
GET my_index/_search
{
"size": 0,
"aggs": {
"titles": {
"terms": {
"field": "title.keyword"
}
}
}
}

2.3 修改 Mapping

Elasticsearch 不支持直接修改已有字段的类型。如果想给已有索引新增 keyword 子字段,需要使用 动态模板或在创建索引时定义好,或者新建索引并重建数据。

示例:新增字段时定义 multi-fields

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT my_index/_mapping
{
"properties": {
"new_field": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}

如果字段已存在且类型不同,修改会失败,需要新建索引。

3. 总结

  • text 适合全文检索,支持分词和相关度评分,不能用于聚合和排序。
  • keyword 适合精确匹配、过滤、排序和聚合,不分词。
  • 多数字符串字段建议用 text + keyword 多字段映射,兼顾两种需求。
  • 查询时全文检索用 match 查询 text 字段,精确匹配用 term 查询 keyword 字段。

以上内容基于 Elasticsearch 官方设计理念及社区实践总结[1][2][3][4][6]。如果需要,我可以帮你写具体的 mapping 和查询模板。

[1] https://www.cnblogs.com/hahaha111122222/p/12177377.html
[2] https://blog.csdn.net/UbuntuTouch/article/details/128904528
[3] https://cloud.tencent.com/developer/article/2357713
[4] https://bbs.huaweicloud.com/blogs/410730
[5] https://www.cnblogs.com/Rawls/p/10069670.html
[6] https://blog.csdn.net/weixin_41860630/article/details/126471632
[7] https://blog.51cto.com/u_15730090/5510216
[8] https://blog.51cto.com/u_15278282/2933670