行为传递:Java 与 Go 的两条路线

作者:lanran 发布于:2026年6月15日

编程里的行为传递

目录 6 节
  1. Java:接口函数化
  2. Go:函数类型接口化
  3. 为什么不直接传函数?
  4. 两种适配方向
  5. 一个有趣的反差
  6. 回到那个问题

很多时候,我们只是想把一段行为传进去。

比如:

  1. 收到一个请求时做什么?
  2. 遍历一个集合时怎么处理元素?
  3. 某个事件发生时执行哪段逻辑?

这听起来像函数式编程里的问题:函数是一等值,所以行为可以直接传递。

但 Java 和 Go 都需要结合自身的类型系统。它们都要回答一个很现实的问题:我怎么把一段行为,放进已有的类型系统里传递?

Java 和 Go 都给了答案,而且很有意思的是,它们解决的是同一个问题,但走的是两条相反的路。

Java 是“接口像函数”。 Go 是“函数像接口实现”。

Java:接口函数化

Java 8 以后可以写 lambda。

比如:

@FunctionalInterface
interface Handler {
    void handle(String name);
}

static void execute(Handler h) {
    h.handle("lanran");
}

execute(name -> System.out.println("hello " + name));

这里的 name -> System.out.println(...) 看起来像一个函数。

但在 Java 里,lambda 不能脱离目标类型单独存在。它必须被某个类型接住。

这个类型就是函数式接口。

Handler 只有一个抽象方法:

void handle(String name);

所以编译器知道,下面这个 lambda:

name -> System.out.println("hello " + name)

可以被适配成 Handler

换句话说:

lambda -> target type -> functional interface instance

Java 的中心仍然是接口。

不是函数自己突然变成了一个自由的值,而是接口先定义出“行为的形状”,lambda 再被放进这个形状里。

这也是为什么 Java 里会有很多这样的接口:

Runnable
Consumer<T>
Function<T, R>
Predicate<T>
Supplier<T>

它们看起来像函数类型,其实仍然是接口。

比如:

Function<String, Integer> length = s -> s.length();

Integer n = length.apply("hello");

这里的函数签名大概是:

String -> Integer

但 Java 不会真的给你一个裸的函数类型。它给的是:

Function<String, Integer>

然后通过 apply 调用。

Java 的函数式接口就像是:只要一个接口足够像函数,就允许 lambda 进来

Go:函数类型接口化

Go 的路线完全不同。

Go 没有 Java 那种 lambda target type 机制。Go 也没有 @FunctionalInterface

但 Go 有一个很特别的能力:定义的函数类型(defined function type)可以定义方法

这个机制在标准库里最经典的例子,就是 net/http

http.Handler 大概长这样:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

也就是说,一个 HTTP handler 只要能响应:

ServeHTTP(w, r)

就可以被当作 handler。

但很多时候,我们只是想写一个函数:

func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello")
}

它的签名和 ServeHTTP 需要的参数是一样的,只是它本身还不是一个 http.Handler

这时候 http.HandlerFunc 出场了。

它本质上就是一个函数类型:

type HandlerFunc func(ResponseWriter, *Request)

然后它给自己补了 ServeHTTP 方法:

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

这样 HandlerFunc 就满足了 Handler 接口。

关键链路是:

function -> defined function type -> method set -> interface

HandlerFunc 的底层是函数,但它已经是一个命名类型。它有了名字,就可以定义方法;它有了 ServeHTTP,就满足了 Handler

Go 这边的感觉是: 函数先变成一种类型,这种类型再通过方法集实现接口

这也是 Go 的隐式接口出现的地方。你不需要写 implements Handler,方法集对得上,它就是。

于是你可以这样写:

http.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello")
}))

很多时候甚至还会被封装得更自然:

http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello")
})

HandleFunc 只是把这个适配过程再包得顺手一点。

这里的核心仍然是:函数类型 + 方法 = 接口适配器

net/http 把它当成 Handler 调用 ServeHTTP 时,它转身执行函数本身。没有继承,也没有显式实现声明,只是在函数和接口之间做了一个小的桥接。

为什么不直接传函数?

这里还有一个很自然的问题:Go 明明可以直接把函数作为参数,为什么还要绕一圈接口?

比如:

func execute(f func(string)) {
	f("lanran")
}

如果只是一次性的 callback,这当然够用。

http.Handler 不是普通 callback。它抽象的不是“一段函数”,而是一个能处理 HTTP 请求的角色。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

它真正想表达的是:

我不关心你内部是什么。
只要你能 ServeHTTP,你就是一个 HTTP handler。

这个东西可以是函数:

http.HandlerFunc(fn)

也可以是带状态的结构体:

type Server struct {
	db *DB
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// use s.db
}

还可以是中间件、路由器、文件服务器、测试里的 mock。

比如中间件通常会写成:

func Logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

如果 net/http 只接受 func(ResponseWriter, *Request),它当然也能工作,但抽象会被压扁成“回调函数”。而 Handler 这个接口表达的是一个更稳定的能力边界。

所以根本答案不是“Go 为了融入类型系统,所以包装一下函数”。

更准确地说:

func(...)    表达的是一段可调用代码。
Handler      表达的是一种处理 HTTP 请求的能力。
HandlerFunc 让普通函数也能扮演这种能力。

HandlerFunc 的价值就在这里。它不是为了绕,而是为了让最轻的函数写法也能进入更大的组件抽象里。

两种适配方向

Java 和 Go 都在解决“如何传递行为”这个问题。

但方向刚好相反。

维度Java 函数式接口Go 函数类型实现接口
中心接口方法集
核心机制lambda 适配目标接口函数类型定义方法
类型系统味道名义类型 + target typing结构化接口 + 隐式满足
函数是否能有方法lambda 本身不能定义的函数类型可以
适配方向接口接纳函数函数类型满足接口
典型例子Runnable / Consumer / Functionhttp.HandlerFunc

Java 是:

interface Handler {
    void handle(...)
}

// lambda -> Handler

Go 是:

type HandlerFunc func(...)

func (f HandlerFunc) Handle(...) {
    f(...)
}

// HandlerFunc -> Handler

Java 的接口像一个函数槽位。只要接口只有一个抽象方法,lambda 就可以填进去。

Go 的函数类型像一个可以长方法的值。只要方法集对得上,它就可以进入接口世界。

一个有趣的反差

Java 的函数式接口,是把接口压缩成一个函数形状。

Go 的 HandlerFunc,是把函数抬升成一个有方法集的类型。

前者像是:接口降维成函数来用

后者像是:函数升维成接口实现来用

这两个说法不一定严谨到能进语言规范,但很适合理解它们的设计气质。

Java 很在意接口作为抽象边界。即使引入 lambda,它也没有抛开接口,而是让 lambda 服务于接口。

Go 很在意方法集和隐式接口。即使是一段函数,只要你给它一个命名类型,再补上方法,它也可以自然地进入接口系统。

所以两者表面上都能写:把一段行为传进去

但背后的模型完全不同。

回到那个问题

Java 和 Go 都是在解决同一个问题:怎么把一段行为放进已有的抽象里传递。

但它们不是在同一个维度上做这件事。

Java 是从接口侧开放入口:这个接口只有一个抽象方法,所以 lambda 可以进来。

Go 是从类型侧补齐能力:这个函数类型有某个方法,所以它满足接口。

一个让接口变得像函数。

一个让函数类型变得像接口实现。

这也是这两个语言很有意思的地方:它们都想让行为变成值,但都没有完全离开自己的类型系统。

看起来都只是传一个函数,实际上背后是两套语言哲学在轻轻握手。