Go和Elixir极简HTTP服务对比
Go 和 Elixir 都是我非常喜欢的编程语言,这次来对比下它们实现一个原生极简 HTTP 服务的过程。
Go 语言标准库自带了网络服务库,只需要简单几行代码就可以实现一个网络服务,这也是一开始它吸引我的一个方面。而 Elixir 标准库本身没有网络服务的库,而是通过 Plug
库提供了一套标准网络服务编写规范,虽然它不是标准库,但也是由官方开发和维护的。
新建工程
首先我们从新建工程开始。Go 并没有什么严格的工程管理工具和规范,我们只需要新建一个 .go
文件就可以开始编程了。Elixir 程序的运行方式就比较多样了,既可以做为脚本直接运行,也可以在交互式环境中运行,还可以编译成 beam
文件加载到 Beam 虚拟机运行。而且 Elixir 还提供了工程管理工具 Mix
,用来创建和运行工程,以及打包测试等。这里我们需要一个 OTP 应用,因此我们使用 mix
来创建工程。
Go | Elixir |
---|---|
新建 main.go | mix new example --sup |
对于 Elixir 来说,我们还需要在 mix.exs
中添加 plug_cowboy
依赖:
defp deps do[{:plug_cowboy, "~> 2.0"}]
end
然后运行 mix deps.get
来获取依赖。
处理器
Web 应用的关键是”处理器”,用来处理具体的 http 请求。
在 Go 语言中,处理器是一个接口:
type Handler interface {ServeHTTP(http.ResponseWriter, *http.Request)
}
我们只需要实现一个签名为 func(w http.ResponseWriter, r *http.Request)
的函数即可。
package mainimport "net/http"func main() {http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("Hello World from Go!\n"))},))
}
而在 Elixir 中,我们需要实现的是 Plug
行为,它包含以下两个函数:
@callback init(opts) :: opts
@callback call(conn :: Plug.Conn.t(), opts) :: Plug.Conn.t()
首先我们在 lib/example
目录下创建一个 hello_world_plug.ex
文件,然后定义一个模块来实现 Plug
行为。
defmodule Example.HelloWorldPlug doimport Plug.Conndef init(options), do: optionsdef call(conn, _opts) doPlugconn|> put_resp_content_type("text/plain")|> send_resp(200, "Hello World from Elixir!\n")end
end
然后在 application.ex
的 start
函数中添加我们的应用。
def start(_type, _args) dochildren = [# Starts a worker by calling: Example.Worker.start_link(arg)# {Example.Worker, arg}{Plug.Cowboy, scheme: :http, plug: Example.HelloWorldPlug, options: [port: 8081]}]# See https://hexdocs.pm/elixir/Supervisor.html# for other strategies and supported optionsopts = [strategy: :one_for_one, name: Example.Supervisor]Supervisor.start_link(children, opts)end
最后运行 mix run --no-halt
启动应用即可。
可以看到在 Go 语言中,对于 HTTP 连接的读写分别由 http.ResponseWriter
和 http.Request
承担,而在 Elixir 中则全部统一到了 Plug.Conn
结构体中。实际上这也是许多第三方 Go 语言 Web 框架的实现方式,它们通常叫做 Context
。
路由
路由器用来分别处理不同路径下的 HTTP 请求,也就是将不同路径的请求分发给不同的处理器,它本身本质上也是一个处理器。
在 Go 1.22 之前,标准库的路由器功能都还比较简单,只能匹配 HTTP 路径,不能匹配 HTTP 方法。直到 Go 1.22, http.ServeMux
终于迎来了升级,支持匹配 HTTP 方法,路径参数,通配符等。那些以前只能通过第三方库实现的功能,也能通过标准库实现了。以下是摘自官网的 Go 1.22 release note:
Enhanced routing patterns
HTTP routing in the standard library is now more expressive. The patterns used by
net/http.ServeMux
have been enhanced to accept methods and wildcards.Registering a handler with a method, like
"POST /items/create"
, restricts invocations of the handler to requests with the given method. A pattern with a method takes precedence over a matching pattern without one. As a special case, registering a handler with"GET"
also registers it with"HEAD"
.Wildcards in patterns, like
/items/{id}
, match segments of the URL path. The actual segment value may be accessed by calling theRequest.PathValue
method. A wildcard ending in “…”, like/files/{path...}
, must occur at the end of a pattern and matches all the remaining segments.A pattern that ends in “/” matches all paths that have it as a prefix, as always. To match the exact pattern including the trailing slash, end it with
{$}
, as in/exact/match/{$}
.If two patterns overlap in the requests that they match, then the more specific pattern takes precedence. If neither is more specific, the patterns conflict. This rule generalizes the original precedence rules and maintains the property that the order in which patterns are registered does not matter.
This change breaks backwards compatibility in small ways, some obvious—patterns with “{” and “}” behave differently— and some less so—treatment of escaped paths has been improved. The change is controlled by a
GODEBUG
field namedhttpmuxgo121
. Sethttpmuxgo121=1
to restore the old behavior.
为了保持兼容性, ServeMux
并没有提供诸如 Get(...)
, Post(...)
这样的方法,还是通过 Handle
和 HandleFunc
来注册路由,只是将 HTTP 方法集成到了路径中。
目前 Go 的最新版本是 1.24,离 Go 1.22 也已过去了两个大版本,因此我们就以新版本为例来进行对比。
package mainimport "net/http"func main() {http.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("Hello World from Go!\n"))})http.ListenAndServe(":8080", nil)
}
对 Elixir 来说,路由器也是 Plug
,我们需要使用 use Plug.Router
来导入一些有用的宏。在 lib/example
目录下新建一个 router.ex
文件。
defmodule Example.Router douse Plug.Routerplug(:match)plug(:dispatch)get "/" dosend_resp(conn, 200, "Welcome!")endget("/hello", to: Example.HelloWorldPlug)match _ dosend_resp(conn, 404, "Oops!")end
end
这里我们首先用 plug(:match)
和 plug(:dispatch)
插入两个内置的 Plug
用来匹配和分发路由。之后我们就可以使用 get
, post
和 match
等宏来编写处理函数了。除了直接用 :do
来书写处理程序,还可以通过 :to
来指定 Plug
。除了支持模块 Plug
,也可以是函数 Plug
。函数 Plug
是一个签名与 call
函数相同的函数。
最后我们把 application.ex
中的 start
函数中的 Plug
换成 Example.Router
。
def start(_type, _args) dochildren = [# Starts a worker by calling: Example.Worker.start_link(arg)# {Example.Worker, arg}# {Bandit, plug: Example.HelloWorldPlug, scheme: :http, port: 8080}{Plug.Cowboy, scheme: :http, plug: Example.Router, options: [port: 8081]}]# See https://hexdocs.pm/elixir/Supervisor.html# for other strategies and supported optionsopts = [strategy: :one_for_one, name: Example.Supervisor]Supervisor.start_link(children, opts)
end
Elixir 的路由十分灵活,表达能力也更强。
中间件
Go 的中间件是一个接收 http.HandlerFunc
并返回 http.HandlerFunc
的函数。比如我们实现一个记录日志的中间件。
func main() {http.HandleFunc("GET /hello", logHttp(func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("Hello World from Go!\n"))},))http.ListenAndServe(":8080", nil)
}func logHttp(handler http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {log.Printf("%s %s\n", r.Method, r.URL.Path)handler(w, r)}
}
Elixir 的中间件还是 Plug
,这里我们实现一个叫 log_http
的函数 Plug
。
defmodule Example.Router douse Plug.Routerplug(:match)plug(:dispatch)plug(:log_http)get "/" dosend_resp(conn, 200, "Welcome!")endget("/hello", to: Example.HelloWorldPlug)match _ dosend_resp(conn, 404, "Oops!")enddef log_http(conn, _opts) dorequire LoggerLogger.info("#{conn.method} #{conn.request_path}")connend
end
总结
以上就是原生极简 HTTP 服务在 Go 和 Elixir 中的实现。虽然 Elixir 的代码量更多,但是其功能和表现力也更强。Go 胜在简洁,但是过于简洁,相比于 Elixir,语言表现力还是差了一点。
如果要实现更庞大的 Web 应用,Go 有许多优秀的 Web 框架可供选择,比如 Gin,Echo等等,太多了。而 Elixir 则有鼎鼎大名的大杀器 Phoenix。