跳至主要内容

Go: slice作为函数参数的解包

 

在 Go 语言里,... 跟在 slice 后面,是 “解包(unpack)”操作,它的作用是把一个切片的元素逐个展开,作为可变参数传入函数,而不是把整个切片当作一个单独的参数。


📌 典型场景:append 追加切片

s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}

// 如果直接 append(s1, s2) 会报错,因为 s2 是一个切片,不是单个元素
// 用 ... 解包,把 s2 的每个元素依次追加到 s1
s1 = append(s1, s2...)
fmt.Println(s1) // 输出: [1 2 3 4 5 6]

这里的 s2... 就是把 s2 展开成 4, 5, 6 三个独立的参数传给 append


📌 适用范围

... 这种解包语法只能用在函数调用的最后一个参数位置,并且该参数必须是可变参数(variadic parameter),例如:

func sum(nums ...int) int {
    total := 0
    for _, v := range nums {
        total += v
    }
    return total
}

arr := []int{1, 2, 3}
fmt.Println(sum(arr...)) // 等价于 sum(1, 2, 3)


在 Go 里,严格意义上并没有一个和 ... 解包对称的“打包/合并”运算符。
所谓“合并”切片,通常是通过 appendcopy 等函数来实现的,而不是用某个单独的语法符号。


🔹 常见的“合并”方式

1. 用 append 合并两个或多个切片

a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := []int{7, 8}

// 合并 a 和 b
a = append(a, b...) // b... 是解包,把 b 的元素追加到 a

// 再合并 c
a = append(a, c...)
fmt.Println(a) // [1 2 3 4 5 6 7 8]

这里虽然用到了 ...,但它的作用是解包参数,合并的动作是由 append 完成的。


2. 用 copy 先分配好目标切片,再拷贝

a := []int{1, 2, 3}
b := []int{4, 5, 6}

merged := make([]int, len(a)+len(b))
copy(merged, a)
copy(merged[len(a):], b)

fmt.Println(merged) // [1 2 3 4 5 6]

这种方式在需要精确控制内存分配时更高效,因为只分配一次底层数组。


3. 多个切片一次性合并

a := []int{1, 2}
b := []int{3, 4}
c := []int{5, 6}

merged := append(append(a, b...), c...)
fmt.Println(merged) // [1 2 3 4 5 6]

Go 没有像 Python 那样的 + 运算符直接拼接切片,必须显式调用 append


在 Go 语言里,copy 是一个内置函数,专门用来将一个切片(slice)的元素复制到另一个切片。它的函数签名是:

func copy(dst, src []T) int
  • dst:目标切片(要把数据复制到这里)
  • src:源切片(数据从这里来)
  • 返回值:实际复制的元素个数(取 len(dst)len(src) 的最小值)

🔹 基本用法

a := []int{1, 2, 3}
b := make([]int, 5) // 目标切片长度为 5
n := copy(b, a)

fmt.Println(b) // [1 2 3 0 0]
fmt.Println(n) // 3

这里 copy 会把 a 的前三个元素复制到 b 的前三个位置,剩下的保持原值。


🔹 部分复制

copy 可以配合切片表达式复制一部分:

src := []int{10, 20, 30, 40, 50}
dst := make([]int, 3)

copy(dst, src[2:]) // 从 src 的第 3 个元素开始复制
fmt.Println(dst)   // [30 40 50]

🔹 特点与注意事项

  • 不会自动扩容copy 只会在 dst 已有的长度范围内复制,不会改变 dst 的长度。
  • 类型必须一致dstsrc 的元素类型必须相同。
  • 可以重叠dstsrc 可以引用同一个底层数组(甚至有重叠区域),copy 会安全处理。
  • 深浅拷贝
    • 对于基础类型元素,copy 复制的是值(相当于深拷贝元素本身)。
    • 对于引用类型元素(如切片、map、指针、结构体中含指针),copy 复制的是引用地址(浅拷贝)。


此博客中的热门博文

Elasticsearch 读写原理指南

### 1. 什么是 segment,里面装了什么? 在 Lucene(也是 Elasticsearch)里,索引被切分成若干 **segment(段)**,每个 segment 是一个完整的、只读的倒排索引单元。一个 segment 包含: * **倒排词典** —— 用 **FST(Finite‑State Transducer)** 以高度压缩的形式保存每个字段出现的所有 term 以及 term→ord 的映射。对应的磁盘文件是 `*.tim`(新版)或 `*.tis/*.tii`(旧版)。 * **倒排列表(postings)** —— 保存每个 term 出现的文档 ID、频次、位置信息等,文件名通常是 `*.doc`、`*.pos`、`*.pay`。 * **存储字段**(_source、store:true 的字段)—— 以二进制块的形式写入 `*.fdt` / `*.fdx`。 * **doc‑values、norms、向量** 等辅助结构,分别保存在 `*.dv`、`*.norm`、`*.tv` 等文件里。 * **deleted‑docs bitmap**(`*.del`),标记哪些文档已被删除或被更新。 所有这些文件在 segment **写入磁盘后即成为只读**,后续的查询只能读取,永远不会在原文件上进行增删改。 --- ### 2. 原始文档和 FST 为什么都在 segment 里? * **原始文档**:Elasticsearch 默认把完整的 JSON(_source)以及任何 `store:true` 的字段写入 segment 的 `*.fdt/*.fdx` 文件。每个 segment 保存自己的那部分文档,旧的 segment 在合并前仍然保留,直到合并后被删除。 * **FST**:每个字段的词典在每个 segment 中单独维护,采用 FST 进行前缀共享和字节压缩。这样即使同一个 term 在多个 segment 中出现,也会在每个 segment 里拥有独立的映射,查询时只需要在对应 segment 的 FST 中定位即可。 --- ### 3. 查询时到底是怎么遍历 segment 的? 1. **请求入口**      客户端的搜索请求先到达 **协调节点**,协调节点把请求 ...

LLM缓存详解

 可以把“大模型缓存”理解成: 把已经算过的结果(或中间结果)存下来,下次尽量复用 。但这里面其实分几层,不只是简单的“问题→答案”缓存。 1️⃣ 常见的几种缓存类型 (1)KV Cache(推理内部缓存) Transformer 在生成时,会把前面 token 的 Key/Value 向量 缓存下来。 本质:避免重复计算 attention 作用: 同一请求内部加速 特点: 👉 只对“同一上下文继续生成”有效 👉 不跨用户、不跨请求 这类缓存是你体感“流式输出越来越快”的原因之一。 (2)Prompt Cache(提示词缓存) 缓存的是: 相同(或高度相似)的 prompt → 对应的中间表示 / 输出 典型场景: 系统提示词(system prompt)很长 多轮对话里前文基本不变 👉 这里能省掉 前缀计算成本(prefill) (3)Embedding / 语义缓存(Semantic Cache) 这个才是你问题的关键 👇 不是按“字符串完全一致”,而是: 把问题转成向量 → 找“语义相似”的历史问题 → 直接复用答案 2️⃣ 为什么命中缓存成本低很多? 因为大模型推理成本主要在两块: (1)Prefill(吃 prompt) 复杂度 ~ O(n²) 很贵(尤其长 prompt) (2)Decode(逐 token 生成) 每个 token 都要算一遍模型 而缓存命中后: KV cache:不用重复 attention Prompt cache:不用重新 encode 语义缓存: 直接跳过模型推理 👉 相当于从: 几十~几百毫秒 + GPU算力 变成: 一次向量检索(毫秒级)+ 直接返回 所以成本差一个数量级是正常的。 3️⃣ “每个人问法不同,怎么命中缓存?” 这是核心难点,也是工程重点👇 ❌ 不能靠字符串匹配 比如: “今天天气怎么样” “今天外面热不热” 字符串完全不同 → 必须 miss ✅ 用语义相似度(Embedding) 流程一般是: 把问题转 embedding(向量) 在向量数据库里找 TopK 相似问题 如果相似度 > 阈值(比如 0.9) 直接返回缓存答案 一个简单示意 Q1: 北京天气怎么样 → embedding A Q2: 北京今天热吗 → embedding B cosine(A, B) ≈ 0.95...

事务的ACID是什么

 事务的 ACID 是数据库事务必须满足的四个基本性质,用来保证在并发和故障情况下数据的正确性与可靠性: A(Atomicity,原子性) 一个事务中的操作要么 全部成功 ,要么 全部失败回滚 ,不存在“只做了一半”的中间状态。 C(Consistency,一致性) 事务执行前后,数据库都必须处于 一致的合法状态 ,满足约束(如主键、外键、唯一性、业务规则等)。 I(Isolation,隔离性) 并发执行的多个事务之间 相互隔离 ,一个事务未提交的中间结果对其他事务不可见(具体强弱由隔离级别决定)。 D(Durability,持久性) 一旦事务提交成功,其结果会被 永久保存 ,即使系统崩溃也不会丢失(通常依赖 WAL/redo log 等机制)。 一句话记忆: 要么全做完、前后不破坏规则、互不干扰、做完不丢。