当前位置: 首页 > news >正文

Go和Elixir极简HTTP服务对比

Go 和 Elixir 都是我非常喜欢的编程语言,这次来对比下它们实现一个原生极简 HTTP 服务的过程。

Go 语言标准库自带了网络服务库,只需要简单几行代码就可以实现一个网络服务,这也是一开始它吸引我的一个方面。而 Elixir 标准库本身没有网络服务的库,而是通过 Plug 库提供了一套标准网络服务编写规范,虽然它不是标准库,但也是由官方开发和维护的。

新建工程

首先我们从新建工程开始。Go 并没有什么严格的工程管理工具和规范,我们只需要新建一个 .go 文件就可以开始编程了。Elixir 程序的运行方式就比较多样了,既可以做为脚本直接运行,也可以在交互式环境中运行,还可以编译成 beam 文件加载到 Beam 虚拟机运行。而且 Elixir 还提供了工程管理工具 Mix ,用来创建和运行工程,以及打包测试等。这里我们需要一个 OTP 应用,因此我们使用 mix 来创建工程。

GoElixir
新建 main.gomix 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.exstart 函数中添加我们的应用。

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.ResponseWriterhttp.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 the Request.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 named httpmuxgo121. Set httpmuxgo121=1 to restore the old behavior.

为了保持兼容性, ServeMux 并没有提供诸如 Get(...)Post(...) 这样的方法,还是通过 HandleHandleFunc 来注册路由,只是将 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 用来匹配和分发路由。之后我们就可以使用 getpostmatch 等宏来编写处理函数了。除了直接用 :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。

http://www.lryc.cn/news/576054.html

相关文章:

  • Linux 挂载从入门到精通:mount 命令详解与实战指南
  • 创建一个简单入门SpringBoot3项目
  • Spring Boot项目开发实战销售管理系统——系统设计!
  • Formality:原语(primitive)的概念
  • 中科亿海微SoM模组——基于FPGA+RSIC-V的计算机板卡
  • AI助力游戏设计——从灵感到行动-靠岸篇
  • 《人间词话》PPT课件
  • LeRobot框架设计与架构深度剖析:从入门到精通
  • C#语言入门-task4 :C#语言的高级应用
  • 带标签的 Docker 镜像打包为 tar 文件
  • 七天学会SpringCloud分布式微服务——04——Nacos配置中心
  • Java-异常类
  • Windows Server 2019 查询远程登录源 IP 地址(含 RDP 和网络登录)
  • Spring Boot 性能优化与最佳实践
  • django-celery定时任务
  • Prism框架实战:WPF企业级开发全解
  • Greenplum
  • 鸿蒙OH南向开发 小型系统内核(LiteOS-A)【文件系统】上
  • uni-app uts 插件 android 端 科大讯飞离线语音合成最新版
  • 大模型在急性重型肝炎风险预测与治疗方案制定中的应用研究
  • 无线USB转换器TOS-WLink的无线USB助手配置文件详细胡扯
  • System.Threading.Tasks 库简介
  • Vulkan模型查看器设计:相机类与三维变换
  • Java底层原理:深入理解JVM内存模型与线程安全
  • Node.js到底是什么
  • Jmeter并发测试和持续性压测
  • IBW 2025: CertiK首席商务官出席,探讨AI与Web3融合带来的安全挑战
  • 记录一次飞书文档转md嵌入vitepress做静态站点
  • 时序数据库全面解析与对比
  • 基础RAG实现,最佳入门选择(十二)