Programming notes / JavaScript × Go
柯里化
先得到函数,再得到结果。
从一行看不懂的代码,到一种可复用的思路。
f(a, b)→
g(a)(b)01 / The idea
一次,只收一个参数
柯里化(Currying)把接收多个参数的函数,转换成一连串每次接收一个参数的函数。 最后一个参数传入后,才得到最终结果。定义 ↗
先看加法。左边的函数一次收齐两个数,右边先收第一个数,返回一个等待第二个数的函数。
// 普通形式:接收两个数,返回和
const add = (a, b) => a + b;
add(2, 3); // 5
// 柯里化形式:接收一个数,返回函数
const curriedAdd = (a) => (b) => a + b;
curriedAdd(2)(3); // 5中间可以停下来,给返回的函数起个名字:
const add2 = curriedAdd(2); // 得到函数
add2(3); // 得到 5
add2(8); // 得到 1002 / JavaScript
把两个箭头拆开看
下面这行的意思是:先给一个门槛,再给一个数,判断这个数是否达到门槛。min 是门槛,value 是待判断的数。
const atLeast = (min) => (value) => value >= min;箭头函数可以写成 参数 => 表达式,表达式的值就是返回值。因此,给内层函数加上括号,就能看清谁被返回了:
const atLeast = min => (value => value >= min);
// 外层 └─ 返回的内层函数 ─┘在这个例子里,展开成普通函数写法,计算行为相同:
function atLeast(min) {
return function (value) {
return value >= min;
};
}- 传入门槛
const isAdult = atLeast(18);外层运行,min 是 18;返回内层函数,尚未执行比较。
- 保留函数
isAdult它的判断逻辑相当于 value => value >= 18,可以反复调用。
- 传入数值
isAdult(20); // true内层运行,value 是 20;计算 20 >= 18,返回 true。
为什么外层调用结束后,内层还能使用 min?因为闭包:函数可以继续访问它创建时所在作用域里的变量。这里没有修改 min,所以这个判断函数一直使用门槛 18。闭包 ↗
这里用普通函数展开,是为了看清结构。箭头函数与普通函数在 this 等语义上有区别,不能在所有场景中直接互换。箭头函数 ↗
03 / Go
返回类型,把答案写出来了
Go 没有 JavaScript 的箭头写法,但同样可以把函数作为返回值。普通版本返回 bool,柯里化版本先返回 func(int) bool。
// 普通形式
func atLeastPlain(min, value int) bool {
return value >= min
}
// 柯里化形式
func atLeast(min int) func(int) bool {
return func(value int) bool {
return value >= min
}
}func atLeast(min int) func(int) bool
接收一个 int → 返回一个“接收 int、返回 bool”的函数。
下面是可直接保存为 main.go、用 go run main.go 运行的完整程序:
package main
import "fmt"
func atLeast(min int) func(int) bool {
return func(value int) bool {
return value >= min
}
}
func main() {
isAdult := atLeast(18)
fmt.Println(isAdult(20)) // true
fmt.Println(isAdult(16)) // false
fmt.Println(atLeast(18)(20)) // true
}Go 的函数字面量也是闭包。返回的函数仍然可以访问外层的 min,这一点与上面的 JavaScript 例子一致。Go 语言规范 ↗
04 / Why use it
把配置留住,把数据传进来
当很多次调用使用相同门槛时,可以先配置一次,再反复使用。通常把配置放在前面,把变化的数据放在后面,使用起来更顺手。
JavaScript:生成筛选条件
const atLeast = (min) => (value) => value >= min;
const isPassing = atLeast(60);
const scores = [45, 60, 72, 90];
scores.filter(isPassing); // [60, 72, 90]
scores.filter(atLeast(80)); // [90]filter 需要一个判断函数。atLeast(60) 恰好生成了它,读起来就是“保留至少 60 分的分数”。不需要复用时,直接写 scores.filter(value => value >= 60) 也很清楚。
Go:在循环中复用判断函数
使用前面定义的 atLeast,把下面代码放进 main:
isPassing := atLeast(60)
scores := []int{45, 60, 72, 90}
passed := []int{}
for _, score := range scores {
if isPassing(score) {
passed = append(passed, score)
}
}
fmt.Println(passed) // [60 72 90]这里的收益是:给配置后的行为起名字,减少重复传参,并让函数更容易交给其他代码使用。 普通闭包也能实现这些目标;是否采用柯里化,取决于调用代码是否更清楚。
05 / Three different ideas
柯里化、部分应用、闭包
这三个概念经常一起出现,但各自回答不同的问题。柯里化与部分应用 ↗
- 柯里化 · 改变形式
把
f(a, b, c)转换成g(a)(b)(c):每层接收一个参数。- 部分应用 · 固定参数
先传入一部分参数,得到接收剩余参数的函数。调用
atLeast(60)就是在部分应用。- 闭包 · 访问作用域
- 函数连同它能访问的词法环境,让返回的函数仍可使用外层变量。闭包本身不等于柯里化。
部分应用不要求先柯里化。例如,先固定 a,让返回的函数一次接收 b 和 c:
const sum3 = (a, b, c) => a + b + c;
const with10 = (b, c) => sum3(10, b, c);
with10(2, 3); // 15:部分应用,没有变成逐个传参什么时候不用?
如果参数总是同时到齐,也没有复用中间函数的需求,add(2, 3) 通常更直接。柯里化不保证更快;多层函数也可能增加理解成本。尤其在 Go 中,要权衡额外的函数类型和嵌套是否值得。
检验一下:atLeast(60) 返回 true,还是一个函数?
返回一个函数。它还不知道待判断的数。再调用 atLeast(60)(60) 才得到 true
;调用 atLeast(60)(59) 得到 false。
06 / The name
为什么叫「柯里化」?
Currying 以数学家、逻辑学家 Haskell Curry(哈斯凯尔·柯里) 的姓氏命名。“柯里化”是对应的中文译名,所以单看名字,很难猜出它在做什么。命名来源 ↗
编程语言 Haskell 也以他命名:一个用了名字,一个用了姓氏。Haskell 报告序言 ↗
记住调用的两步就够了:
先给配置,得到函数。
再给数据,得到结果。
资料核对于 2026 年 9 月 28 日。文中示例为本页编写;链接指向语言文档与概念说明。