前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript

前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript

这是「前端开发转 Go 全栈」系列的第五篇。

上一篇学习了命名返回值、裸 returndefer,并通过代码实验验证了多个 defer 的执行顺序、参数求值时机,以及 defer 对命名返回值的影响。

这一篇继续学习 Go 的函数,进入一个前端开发者非常熟悉的领域:匿名函数、闭包与高阶函数。

前言:学了这么久 Go,终于碰到点熟悉的东西了

前面几篇学下来,我对 Go 最大的感受就是:

熟悉的概念不少,但写法和思维方式经常与 JavaScript 不太一样。

比如变量:

go 复制代码
name := "Starry"

比如错误处理:

go 复制代码
result, err := doSomething()

if err != nil {
	return err
}

再比如上一篇刚学完的 defer

go 复制代码
defer file.Close()

这些内容虽然慢慢能够理解,但对于一个长期使用 JavaScript 和 TypeScript 的前端开发者来说,多少还是有点"进入别人主场"的感觉。

直到这一阶段开始学习匿名函数和闭包。

看到下面这种代码时:

go 复制代码
add := func(a, b int) int {
	return a + b
}

我的第一反应终于不是:

这又是什么 Go 特有语法?

而是:

这个我熟啊。

如果换成 JavaScript:

javascript 复制代码
const add = (a, b) => a + b;

除了语法长得不一样,背后的思想几乎就是同一回事。

继续往下学习闭包、返回函数、函数作为参数,我甚至开始产生一种错觉:

我是不是暂时从 Go 教程切回前端了?

这一篇就来记录一下这段难得"亲切"的学习过程。


函数也可以保存到变量中

前面学习函数时,一直都是这样定义:

go 复制代码
func add(a, b int) int {
	return a + b
}

然后调用:

go 复制代码
result := add(10, 20)

这当然没有问题。

但 Go 的函数也可以没有名字。

我第一次写了这样的代码:

go 复制代码
package main

import "fmt"

func main() {
	add := func(a, b int) int {
		return a + b
	}

	result := add(10, 20)

	fmt.Println("计算结果:", result)
}

运行:

bash 复制代码
go run .

输出:

text 复制代码
计算结果: 30

重点在这里:

go 复制代码
add := func(a, b int) int {
	return a + b
}

右边创建了一个没有名字的函数:

go 复制代码
func(a, b int) int {
	return a + b
}

然后将这个函数值保存到变量:

text 复制代码
add

之后就可以像调用普通函数一样,通过变量调用它:

go 复制代码
add(10, 20)

这里创建的,就是一个匿名函数。


这不就是 JavaScript 函数表达式吗?

如果换成 JavaScript,可以写成函数表达式:

javascript 复制代码
const add = function (a, b) {
  return a + b;
};

或者使用现在前端开发中更常见的箭头函数:

javascript 复制代码
const add = (a, b) => {
  return a + b;
};

再短一点:

javascript 复制代码
const add = (a, b) => a + b;

把 Go 和 JavaScript 放在一起对比:

css 复制代码
javascript
const add = (a, b) => a + b;
go 复制代码
add := func(a, b int) int {
	return a + b
}

虽然 Go 没有箭头函数这种简写,但核心思想是相同的:

text 复制代码
函数也是一种值
→ 可以创建
→ 可以保存到变量
→ 可以通过变量调用

这一点对于前端开发者来说,几乎没有理解成本。

需要注意的是:

匿名函数只是"没有名字的函数",并不代表所有匿名函数都是闭包。

只有当函数引用了它外层作用域中的变量时,才开始体现闭包的特征。


匿名函数也可以立即执行

既然函数可以直接创建,那么能不能在创建后马上执行?

可以。

go 复制代码
package main

import "fmt"

func main() {
	func() {
		fmt.Println("这个匿名函数立即执行了")
	}()
}

关键在最后:

go 复制代码
}()

前面的部分:

go 复制代码
func() {
	fmt.Println("这个匿名函数立即执行了")
}

只是定义了一个匿名函数。

最后的:

go 复制代码
()

才是真正调用它。

运行结果:

text 复制代码
这个匿名函数立即执行了

看到这里,前端开发者应该又会觉得很熟悉。

JavaScript 中经常会看到 IIFE,也就是立即调用函数表达式:

javascript 复制代码
(function () {
  console.log("立即执行");
})();

使用箭头函数也可以写成:

javascript 复制代码
(() => {
  console.log("立即执行");
})();

Go 只是换成了:

go 复制代码
func() {
	fmt.Println("立即执行")
}()

整体思路并没有太大区别:

text 复制代码
定义函数
→ 不保存到变量
→ 立即调用

立即执行函数也可以接收参数

我又试了一下给立即执行的匿名函数传递参数:

go 复制代码
package main

import "fmt"

func main() {
	func(name string) {
		fmt.Println("你好:", name)
	}("Starry")
}

输出:

text 复制代码
你好: Starry

这里的函数定义是:

go 复制代码
func(name string) {
	fmt.Println("你好:", name)
}

调用时传入:

go 复制代码
("Starry")

如果用 JavaScript,可以写成:

javascript 复制代码
javascript
((name) => {
  console.log("你好:", name);
})("Starry");

思路基本一致。

这一阶段开始让我明显感觉到:

Go 和 JavaScript 虽然是两门完全不同的语言,但很多编程思想其实是共通的。

真正需要重新学习的,很多时候只是这门语言如何表达这些思想。


闭包:函数可以访问并修改外层变量

接下来开始学习闭包。

第一段代码:

go 复制代码
package main

import "fmt"

func main() {
	count := 0

	increase := func() {
		count++
		fmt.Println("函数内部 count:", count)
	}

	increase()
	increase()
	increase()

	fmt.Println("最终 count:", count)
}

运行结果:

text 复制代码
函数内部 count: 1
函数内部 count: 2
函数内部 count: 3
最终 count: 3

这里有一个关键点:

go 复制代码
count := 0

是在匿名函数外部声明的。

但是下面的匿名函数不仅可以访问 count,还可以直接修改它:

go 复制代码
increase := func() {
	count++
}

调用三次以后:

text 复制代码
0
→ 1
→ 2
→ 3

这里的匿名函数引用了外层作用域中的 count,因此形成了闭包。


对前端来说,闭包真的太熟了

换成 JavaScript:

javascript 复制代码
let count = 0;

const increase = () => {
  count++;
  console.log(count);
};

increase();
increase();
increase();

同样会得到:

text 复制代码
1
2
3

所以这一部分几乎可以直接沿用以前对 JavaScript 闭包的理解:

一个函数可以访问它定义时所在词法作用域中的变量。

Go:

go 复制代码
count := 0

increase := func() {
	count++
}

JavaScript:

javascript 复制代码
let count = 0;

const increase = () => {
  count++;
};

这种熟悉感,和前面学习 defer 时完全是两种体验。

不过这里也需要区分两个概念。

匿名函数

go 复制代码
add := func(a, b int) int {
	return a + b
}

这个函数没有名称,所以它是匿名函数。

但是它没有访问任何外部变量,不需要依赖外层状态。

闭包

go 复制代码
count := 0

increase := func() {
	count++
}

这个函数引用并修改了外层的 count,因此形成了闭包。

可以简单理解为:

text 复制代码
匿名函数关注的是:函数有没有名字
闭包关注的是:函数有没有引用外层作用域中的变量

匿名函数经常被用来创建闭包,但二者不是完全相同的概念。


这样还没有完全体现闭包的价值

前面的 count 直接存在于 main 中:

go 复制代码
count := 0

如果只是这样,好像还看不出闭包到底有什么特别。

于是,我继续写了一个计数器:

go 复制代码
package main

import "fmt"

func createCounter() func() int {
	count := 0

	return func() int {
		count++
		return count
	}
}

func main() {
	counter := createCounter()

	fmt.Println(counter())
	fmt.Println(counter())
	fmt.Println(counter())
}

运行结果:

text 复制代码
1
2
3

这段代码就开始有意思了。


func createCounter() func() int 到底怎么读?

第一次看到下面的函数声明时:

go 复制代码
func createCounter() func() int

确实有点套娃。

可以将它拆开理解。

前面:

go 复制代码
func createCounter()

表示:

定义一个名为 createCounter 的函数,它不接收参数。

后面:

go 复制代码
func() int

createCounter 的返回值类型。

它表示:

返回一个没有参数、返回值类型为 int 的函数。

所以完整的函数声明:

go 复制代码
func createCounter() func() int

可以读成:

createCounter 是一个函数,它会返回另一个"没有参数、返回一个 int"的函数。

如果换成 TypeScript,大概是:

typescript 复制代码
function createCounter(): () => number

这么一对比,一下就顺眼了。

Go:

go 复制代码
func createCounter() func() int

TypeScript:

typescript 复制代码
function createCounter(): () => number

本质上都在描述:

一个函数返回另一个函数。


createCounter 结束了,count 为什么还活着?

真正让我觉得闭包有意思的是这里。

createCounter 内部声明了一个局部变量:

go 复制代码
count := 0

按照之前对局部变量的理解,函数执行结束后:

go 复制代码
createCounter()

里面的局部变量似乎也应该无法再被访问。

但是,createCounter 返回了下面这个匿名函数:

go 复制代码
return func() int {
	count++
	return count
}

这个匿名函数仍然引用着 count

于是:

go 复制代码
counter := createCounter()

拿到的并不只是一段"让数字加一"的函数逻辑。

它还保留了对 count 的访问。

第一次调用:

go 复制代码
counter()

得到:

text 复制代码
1

第二次调用得到:

text 复制代码
2

第三次调用得到:

text 复制代码
3

也就是说,这个函数"记住"了之前的状态。

可以暂时把它想象成:

text 复制代码
counter
├── 执行逻辑:count++
└── 持续引用的状态:count

这就是闭包非常典型的用途:

将一段函数逻辑和它需要持续访问的状态绑定在一起。

至于 count 最终存储在哪里、编译器具体如何处理变量的生命周期,这一阶段暂时不深入。

现在只需要知道:

只要返回的函数仍然引用 count,后续调用这个函数时,就依然能够读取和修改这份状态。


JavaScript 版本几乎一模一样

Go:

go 复制代码
func createCounter() func() int {
	count := 0

	return func() int {
		count++
		return count
	}
}

JavaScript:

javascript 复制代码
function createCounter() {
  let count = 0;

  return function () {
    count++;
    return count;
  };
}

或者使用箭头函数:

javascript 复制代码
const createCounter = () => {
  let count = 0;

  return () => {
    count++;
    return count;
  };
};

调用:

javascript 复制代码
const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

到了这里,对前端开发者来说,Go 闭包已经基本没有陌生感了。

语法虽然不同,但执行思路几乎一致:

text 复制代码
调用外层函数
→ 创建一份局部状态
→ 返回一个引用该状态的函数
→ 后续调用返回的函数
→ 持续读取和修改这份状态

两个 Counter 会共享同一个 count 吗?

接着,我又做了一个很重要的实验:

go 复制代码
package main

import "fmt"

func createCounter() func() int {
	count := 0

	return func() int {
		count++
		return count
	}
}

func main() {
	counterA := createCounter()
	counterB := createCounter()

	fmt.Println("A:", counterA())
	fmt.Println("A:", counterA())

	fmt.Println("B:", counterB())
	fmt.Println("B:", counterB())

	fmt.Println("A:", counterA())
}

最终运行结果:

text 复制代码
A: 1
A: 2
B: 1
B: 2
A: 3

这一点很重要。

如果 counterAcounterB 共享同一份 count,结果应该是:

text 复制代码
A:1
A:2
B:3
B:4
A:5

但实际结果并不是这样。

原因是:

go 复制代码
counterA := createCounter()

执行了一次 createCounter(),产生了一份新的:

go 复制代码
count := 0

接着:

go 复制代码
counterB := createCounter()

又重新执行了一次 createCounter(),再次产生另一份:

go 复制代码
count := 0

所以它们可以理解成:

text 复制代码
counterA
└── 引用自己的一份 count

counterB
└── 引用自己的一份 count

两次对 createCounter 的调用分别创建了独立的状态,因此互不影响。


这让我想到前端里的"状态工厂"

JavaScript 中同样如此:

javascript 复制代码
const counterA = createCounter();
const counterB = createCounter();

counterA(); // 1
counterA(); // 2

counterB(); // 1
counterB(); // 2

counterA(); // 3

如果写过 Vue Composition API,这种"每调用一次函数,就创建一份独立状态"的思想其实也很熟悉。

例如:

javascript 复制代码
function useCounter() {
  let count = 0;

  return {
    increase() {
      count++;
    },
    getCount() {
      return count;
    },
  };
}

每调用一次:

javascript 复制代码
const a = useCounter();
const b = useCounter();

都可以拥有自己的一份状态。

当然,Vue 的响应式系统和 Go 闭包并不是一回事。

这里仅仅是从下面这个角度进行类比:

text 复制代码
调用一个函数
→ 创建一份新的内部状态
→ 返回可以操作这份状态的函数
→ 不同调用之间的状态相互独立

这个思路在 JavaScript 中很常见,在 Go 中同样可以通过闭包实现。


函数不只可以返回,还可以作为参数传入

既然函数也是一种值,那么自然会有下一个问题:

函数能不能作为参数传给另一个函数?

可以。

于是我写了下面的代码:

go 复制代码
package main

import "fmt"

func calculate(a, b int, operation func(int, int) int) int {
	return operation(a, b)
}

func add(a, b int) int {
	return a + b
}

func multiply(a, b int) int {
	return a * b
}

func main() {
	addResult := calculate(10, 20, add)
	multiplyResult := calculate(10, 20, multiply)

	fmt.Println("加法结果:", addResult)
	fmt.Println("乘法结果:", multiplyResult)
}

运行结果:

text 复制代码
加法结果: 30
乘法结果: 200

重点看这里:

go 复制代码
operation func(int, int) int

operation 是一个函数参数。

它要求传入的函数必须满足下面的类型:

go 复制代码
func(int, int) int

也就是:

text 复制代码
接收两个 int
返回一个 int

所以:

go 复制代码
func add(a, b int) int

符合要求。

下面这个函数:

go 复制代码
func multiply(a, b int) int

也符合要求。

于是既可以写:

go 复制代码
calculate(10, 20, add)

也可以写:

go 复制代码
calculate(10, 20, multiply)

calculate 不关心具体要进行加法还是乘法。

它只负责:

text 复制代码
接收两个数字
→ 接收一段符合要求的计算逻辑
→ 调用这段逻辑
→ 返回计算结果

传入的函数签名必须匹配

calculate 要求 operation 的类型是:

go 复制代码
func(int, int) int

这意味着传入的函数必须满足:

text 复制代码
参数数量相同
参数类型相同
返回值数量相同
返回值类型相同

例如,下面的函数无法直接传入:

go 复制代码
func greet(name string) string {
	return "你好," + name
}

因为它的函数类型是:

go 复制代码
func(string) string

calculate 需要的是:

go 复制代码
func(int, int) int

二者不是同一种函数类型。

所以 Go 不只是允许"把函数传进去",还会在编译阶段检查:

传入的函数是否符合参数要求的函数签名。


前端开发看到 callback 应该就懂了

这和 JavaScript 中的回调函数非常接近:

javascript 复制代码
function calculate(a, b, operation) {
  return operation(a, b);
}

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

calculate(10, 20, add);
calculate(10, 20, multiply);

TypeScript 的写法甚至和 Go 更像:

typescript 复制代码
function calculate(
  a: number,
  b: number,
  operation: (a: number, b: number) => number
): number {
  return operation(a, b);
}

Go:

go 复制代码
func calculate(
	a, b int,
	operation func(int, int) int,
) int {
	return operation(a, b)
}

对比一下函数参数的类型。

TypeScript:

typescript 复制代码
operation: (a: number, b: number) => number

Go:

go 复制代码
operation func(int, int) int

两者都在描述:

operation 必须是一个接收两个数字并返回一个数字的函数。


甚至可以直接把匿名函数传进去

不一定要提前定义:

go 复制代码
func add(a, b int) int

也可以在调用 calculate 时直接传入匿名函数:

go 复制代码
package main

import "fmt"

func calculate(a, b int, operation func(int, int) int) int {
	return operation(a, b)
}

func main() {
	result := calculate(10, 20, func(a, b int) int {
		return a - b
	})

	fmt.Println("计算结果:", result)
}

运行结果:

text 复制代码
计算结果: -10

如果换成前端代码:

javascript 复制代码
calculate(10, 20, (a, b) => {
  return a - b;
});

Go:

go 复制代码
calculate(10, 20, func(a, b int) int {
	return a - b
})

写到这里,已经非常像平时在 JavaScript 中使用的代码:

javascript 复制代码
array.map((item) => ...)
array.filter((item) => ...)
array.find((item) => ...)
button.addEventListener("click", () => ...)

它们背后的共同思路都是:

text 复制代码
把一段函数逻辑
→ 作为参数
→ 交给另一个函数
→ 由另一个函数在合适的时机调用

函数作为参数,不一定就是闭包

这里也需要再次区分几个概念。

下面的 add 是一个普通函数:

go 复制代码
func add(a, b int) int {
	return a + b
}

将它传给 calculate

go 复制代码
calculate(10, 20, add)

这里使用了"函数作为参数"的能力,但 add 并没有引用外层变量,所以它不是闭包。

而下面的匿名函数:

go 复制代码
offset := 10

operation := func(a, b int) int {
	return a + b + offset
}

引用了外层变量 offset,所以它形成了闭包。

如果把它传入:

go 复制代码
result := calculate(10, 20, operation)

这里同时使用了:

text 复制代码
匿名函数
闭包
函数作为参数

这些概念经常一起出现,但它们分别关注不同的问题:

text 复制代码
匿名函数
→ 函数有没有名字?

闭包
→ 函数有没有引用外层作用域中的变量?

函数参数
→ 函数能不能作为值传给另一个函数?

高阶函数
→ 一个函数是否接收函数或返回函数?

我甚至自己写了一个类似 filter 的函数

学到这里,我顺手尝试模拟前端非常熟悉的 filter

JavaScript:

javascript 复制代码
const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = numbers.filter((number) => {
  return number % 2 === 0;
});

console.log(evenNumbers);

结果:

text 复制代码
[2, 4, 6]

Go 版本:

go 复制代码
package main

import "fmt"

func filter(numbers []int, condition func(int) bool) []int {
	result := []int{}

	for _, number := range numbers {
		if condition(number) {
			result = append(result, number)
		}
	}

	return result
}

func main() {
	numbers := []int{1, 2, 3, 4, 5, 6}

	evenNumbers := filter(numbers, func(number int) bool {
		return number%2 == 0
	})

	fmt.Println(evenNumbers)
}

最终输出:

text 复制代码
[2 4 6]

看到结果以后,已经非常有 JavaScript filter 的感觉了。

不过,这段代码出现了几个目前还没有正式学习的内容:

text 复制代码
[]int
range
append

所以这一篇暂时不展开它们。

现阶段只关注:

go 复制代码
condition func(int) bool

这表示:

condition 是一个接收 int、返回 bool 的函数。

调用时传入的:

go 复制代码
func(number int) bool {
	return number%2 == 0
}

就相当于前端里的:

javascript 复制代码
(number) => number % 2 === 0

filter 会把每个数字交给 condition

text 复制代码
condition(1) → false
condition(2) → true
condition(3) → false
condition(4) → true
condition(5) → false
condition(6) → true

只有返回 true 的数字会进入最终结果。

至于:

text 复制代码
[]int 到底是什么?
为什么不是 [6]int?
range 是什么?
append 为什么能够增加元素?

这些问题留到下一阶段正式解决。

这里的 filter 只是用来验证:

Go 的函数也可以像 JavaScript 回调函数一样,被传入另一个函数并参与逻辑判断。


什么是高阶函数?

学到这里,其实已经在使用高阶函数了。

只要一个函数满足下面任意一种情况:

text 复制代码
接收函数作为参数
或者
返回另一个函数

通常就可以称为高阶函数。

例如:

go 复制代码
func createCounter() func() int

它返回另一个函数,所以 createCounter 是高阶函数。

再比如:

go 复制代码
func calculate(
	a, b int,
	operation func(int, int) int,
) int

它接收函数作为参数,所以 calculate 也是高阶函数。

刚才写的 filter 同样如此:

go 复制代码
func filter(numbers []int, condition func(int) bool) []int

它接收 condition 函数,因此也是高阶函数。

对于前端开发者来说,这个词应该并不陌生。

JavaScript 中的:

text 复制代码
map
filter
reduce
sort
find
some
every

这些大量接收回调函数的 API,背后都使用了高阶函数的思想。

所以 Go 虽然没有直接使用:

javascript 复制代码
numbers.filter(...)

这种写法,但语言本身完全支持把函数当作值进行保存、传递和返回。


可以给复杂的函数类型起名字

当函数类型比较简单时,直接写:

go 复制代码
operation func(int, int) int

还比较清楚。

但如果同一种函数类型会重复出现,Go 也可以使用 type 给它起一个名字:

go 复制代码
type Operation func(int, int) int

然后将 calculate 改成:

go 复制代码
func calculate(a, b int, operation Operation) int {
	return operation(a, b)
}

完整代码:

go 复制代码
package main

import "fmt"

type Operation func(int, int) int

func calculate(a, b int, operation Operation) int {
	return operation(a, b)
}

func add(a, b int) int {
	return a + b
}

func main() {
	result := calculate(10, 20, add)

	fmt.Println("计算结果:", result)
}

这里的:

go 复制代码
type Operation func(int, int) int

表示定义了一个名为 Operation 的函数类型。

以后看到:

go 复制代码
operation Operation

就可以知道它需要的是:

text 复制代码
接收两个 int
返回一个 int

这一篇暂时不继续展开自定义类型,只把它作为一个补充认识。

目前直接使用:

go 复制代码
func(int, int) int

已经足够完成函数参数练习。


Go 和 JavaScript 的函数能力对照

这一阶段可以先整理成下面这张表:

表格

Go JavaScript / TypeScript
func add() {} function add() {}
f := func() {} 函数表达式
没有箭头函数语法 () => {}
func() {}() IIFE
闭包访问外层变量 JavaScript 闭包
函数返回函数 函数返回函数
func(int) bool (value: number) => boolean
函数作为参数 callback
接收或返回函数 高阶函数
type Operation func(...) TypeScript 函数类型别名

对于我来说,这是到目前为止,Go 和前端思维最接近的一部分。


这一阶段最容易混淆的几个概念

1. 匿名函数就是闭包吗?

不一定。

下面是匿名函数:

go 复制代码
add := func(a, b int) int {
	return a + b
}

它没有名称,但也没有引用外层变量。

下面的匿名函数引用了外层 count

go 复制代码
count := 0

increase := func() {
	count++
}

它才形成了闭包。

所以:

text 复制代码
匿名函数不一定是闭包
闭包也经常通过匿名函数创建

2. 函数可以像变量一样传递吗?

可以。

go 复制代码
func add(a, b int) int {
	return a + b
}

result := calculate(10, 20, add)

这里传入的是函数值:

go 复制代码
add

而不是调用结果:

go 复制代码
add(10, 20)

二者不要混淆:

text 复制代码
add
→ 函数本身

add(10, 20)
→ 调用函数后得到的结果

3. 为什么传入 add 时不加括号?

因为 calculate 需要的是一个函数:

go 复制代码
calculate(10, 20, add)

如果写成:

go 复制代码
calculate(10, 20, add(10, 20))

第三个参数就不再是函数,而是 add 执行后得到的整数:

text 复制代码
30

calculate 的第三个参数要求是:

go 复制代码
func(int, int) int

不是 int


4. 返回函数是什么意思?

下面的函数:

go 复制代码
func createCounter() func() int

不会直接返回一个整数。

它返回的是另一个函数。

所以:

go 复制代码
counter := createCounter()

此时 counter 是一个函数。

需要继续调用:

go 复制代码
result := counter()

才能得到整数结果。

调用关系是:

text 复制代码
createCounter()
→ 返回函数

counter()
→ 调用返回的函数
→ 得到 int

5. 每次调用 createCounter 都会共享状态吗?

不会。

go 复制代码
counterA := createCounter()
counterB := createCounter()

createCounter() 分别执行了两次,也分别创建了两份独立的 count

因此:

text 复制代码
counterA 修改自己的 count
counterB 修改自己的 count

两者互不影响。


6. 什么是高阶函数?

如果一个函数:

text 复制代码
接收另一个函数作为参数
或者
返回另一个函数

就可以称为高阶函数。

例如:

go 复制代码
func createCounter() func() int

以及:

go 复制代码
func calculate(a, b int, operation func(int, int) int) int

都是高阶函数。


这一阶段让我最大的感受

前面学习 Go 时,经常是在努力适应差异:

text 复制代码
JavaScript 的 number
→ Go 分成 int、float64 等具体类型

JavaScript 常见的异常处理
→ Go 常见 result + error

前端中没有直接对应 defer 的语法
→ Go 可以在函数退出前执行延迟调用

到了闭包和高阶函数这里,方向突然反了过来。

不再是:

Go 为什么这样设计?

而是:

原来这个概念我早就在 JavaScript 里天天用了。

这也让我意识到,转语言的时候没必要把自己当成完全从零开始。

语法确实需要重新学习。

标准库、工程体系和运行机制也需要重新理解。

但是很多已经掌握的编程思想是可以继续复用的:

text 复制代码
作用域
闭包
函数作为值
回调函数
高阶函数
状态封装

区别只是它们在另一门语言中的表达方式。

当然,这也不代表 Go 闭包和 JavaScript 闭包在所有细节上都完全相同。

目前只能说:

text 复制代码
核心思想非常接近
→ 语法表达有所不同
→ 更深入的运行机制还需要以后继续学习

这一阶段的知识点总结

这次实际学习的内容可以整理成下面这张表:

表格

内容 示例
创建匿名函数 func() {}
函数保存到变量 add := func(a, b int) int {}
通过变量调用函数 add(10, 20)
立即执行匿名函数 func() {}()
立即执行时传参 func(name string) {}("Starry")
闭包访问外层变量 func() { fmt.Println(count) }
闭包修改外层变量 func() { count++ }
返回函数 func createCounter() func() int
独立闭包状态 多次调用 createCounter()
函数作为参数 operation func(int, int) int
匿名函数作为参数 calculate(..., func(...) ...)
高阶函数 接收函数或返回函数
函数类型 func(int, int) int

整体学习路径是:

text 复制代码
匿名函数
→ 函数保存到变量
→ 匿名函数立即执行
→ 函数访问外层变量
→ 闭包保存状态
→ 函数返回函数
→ 函数作为参数
→ 高阶函数

写在最后

这一阶段实际完成了:

text 复制代码
匿名函数
→ 函数保存到变量
→ 匿名函数立即执行
→ 闭包访问外层变量
→ 闭包修改外层状态
→ 返回函数
→ createCounter
→ 多个闭包拥有独立状态
→ 函数作为参数
→ 匿名函数作为参数
→ 高阶函数

到这里,函数这一整个章节也算可以真正告一段落了。

从最开始:

go 复制代码
func sayHello() {
}

一路学到了:

go 复制代码
func createCounter() func() int

以及:

go 复制代码
func calculate(
	a, b int,
	operation func(int, int) int,
) int

函数已经不再只是:

text 复制代码
传入参数
→ 执行代码
→ 返回结果

函数本身也可以像数据一样:

text 复制代码
保存
传递
返回
组合

而最后那个模拟 filter 的实验,也顺便给下一阶段留下了几个问题:

text 复制代码
[]int
range
append

下一篇将正式进入 Go 的集合类型。

作为一个每天都在写下面这些代码的前端开发者:

javascript 复制代码
const list = [];

list.push(...);
list.map(...);
list.filter(...);

我现在最想弄清楚的问题就是:

Go 的 Array 和 Slice,到底哪个才真正对应 JavaScript Array?

下一篇见。

相关推荐
SomeB1oody1 小时前
【RustyML入门】2.6. 线性判别分析
开发语言·后端·机器学习·rust·教程
用户61595868000221 小时前
从 0 写一个迷你 wujie:用原生 Web API 实现微前端
前端
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
fairyly1 小时前
接手新项目不用硬啃源码,我的 TRAE Work 快速上手实战工作流
前端·ai编程·trae
Yao8061 小时前
SpringAI + Ollama + DeepSeek + RAG 搭建 AI 航线规划系统
后端
搬砖记录员1 小时前
用 PyAutoGUI 手搓录屏"老板键":从原理到四套可跑源码,附工程化落地思考
前端
windliang1 小时前
Claude Code 源码分析(七):Skill 如何进入 Agent
前端·人工智能·面试
大勇前进1 小时前
SQL Server 分页查询多种写法对比:哪种性能最高?
后端
栀鸢ouo1 小时前
《Vue 3 实现多主题切换和自定义主题》
前端