client-go中watch机制的一些陷阱
Reference
- https://stackoverflow.com/questions/51399407/watch-in-k8s-golang-api-watches-and-get-events-but-after-sometime-doesnt-get-an
问题描述
最近在使用 client-go 的 watch 机制监听 k8s 中的 deployment 资源时,发现一个奇怪的现象
先看下代码:
- 服务启动时调用 watchDeployment 新建一个 watcher 监听对应的资源
- for 循环,select 处理 watcher.ResultChan 返回的事件
func WatchDeployment(ctx context.Context, namespace string, options metav1.ListOptions, handler EventHandler) error {watcher, err := KubeCli.AppsV1().Deployments(namespace).Watch(ctx, options)if err != nil {log.Errorf("watching deployments err: %+v", err)return err}defer watcher.Stop()// 处理事件for {select {case event, ok := <-watcher.ResultChan():if !ok {log.Errorf("Watcher channel closed")return nil}deployment, ok := event.Object.(*appsv1.Deployment)if !ok {log.Errorf("Error casting to Deployment")continue}switch event.Type {case watch.Added:if handler.OnAdd != nil {handler.OnAdd(ctx, deployment)}case watch.Modified:if handler.OnModify != nil {handler.OnModify(ctx, deployment)}case watch.Deleted:if handler.OnDelete != nil {handler.OnDelete(ctx, deployment)}}}}
}
在运行了一段时间后,watch 监听的通道会自动关闭,日志:“ERROR [trace-] Watcher channel closed”
检视完代码,唯一存在问题的就是 watcher.ResultCha( ) 如果出问题,则会直接 return 导致 for 循环退出了,所以我改了第二版的代码,将 return 替换为了 continue
if !ok {log.Errorf("Watcher channel closed")continue
}
再运行一段时间,日志疯狂报错 “ERROR [trace-] Watcher channel closed” ;
疑问:为什么错误已经continue了,为什么无法再继续监听了?
排查过程
具体debug看下 watch 机制的源码,只展示重要流程代码,细节忽略:
// watch 其实是可以设置 timeout 时间,具体用在哪,继续往下看看
func (c *deployments) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {var timeout time.Durationif opts.TimeoutSeconds != nil {timeout = time.Duration(*opts.TimeoutSeconds) * time.Second}opts.Watch = truereturn c.client.Get().Namespace(c.ns).Resource("deployments").VersionedParams(&opts, scheme.ParameterCodec).Timeout(timeout).Watch(ctx)
}// Timeout makes the request use the given duration as an overall timeout for the
// request. Additionally, if set passes the value as "timeout" parameter in URL.
// 这里就将timeout 设置为了 rquest 请求的超时时间
func (r *Request) Timeout(d time.Duration) *Request {if r.err != nil {return r}r.timeout = dreturn r
}
从 watch 可以看到,client-go 提供的 watch 方法,就是使用 net/http 发起一个 http 请求 https://10.96.0.1:443/apis/apps/v1/namespaces/xxx/deployments?fieldSelector=metadata.name%3Dabc&watch=true,并启用 watch 机制,成功后则返回一个 实现了 watch.Interface 这个接口的 StreamWatcher 的结构体
// Watch attempts to begin watching the requested location.
// Returns a watch.Interface, or an error.
func (r *Request) Watch(ctx context.Context) (watch.Interface, error) {// We specifically don't want to rate limit watches, so we// don't use r.rateLimiter here.if r.err != nil {return nil, r.err}client := r.c.Clientif client == nil {client = http.DefaultClient}isErrRetryableFunc := func(request *http.Request, err error) bool {// The watch stream mechanism handles many common partial data errors, so closed// connections can be retried in many cases.if net.IsProbableEOF(err) || net.IsTimeout(err) {return true}return false}retry := r.retryFn(r.maxRetries)url := r.URL().String()for {if err := retry.Before(ctx, r); err != nil {return nil, retry.WrapPreviousError(err)}req, err := r.newHTTPRequest(ctx)if err != nil {return nil, err}resp, err := client.Do(req)updateURLMetrics(ctx, r, resp, err)retry.After(ctx, r, resp, err)if err == nil && resp.StatusCode == http.StatusOK {return r.newStreamWatcher(resp)}// 重试机制...}
}
StreamWatcher 就是启了一个协程接受 wacth 中的事件变化,进行处理
func (r *Request) newStreamWatcher(resp *http.Response) (watch.Interface, error) {contentType := resp.Header.Get("Content-Type")mediaType, params, err := mime.ParseMediaType(contentType)if err != nil {klog.V(4).Infof("Unexpected content type from the server: %q: %v", contentType, err)}objectDecoder, streamingSerializer, framer, err := r.c.content.Negotiator.StreamDecoder(mediaType, params)if err != nil {return nil, err}handleWarnings(resp.Header, r.warningHandler)frameReader := framer.NewFrameReader(resp.Body)watchEventDecoder := streaming.NewDecoder(frameReader, streamingSerializer)return watch.NewStreamWatcher(restclientwatch.NewDecoder(watchEventDecoder, objectDecoder),// use 500 to indicate that the cause of the error is unknown - other error codes// are more specific to HTTP interactions, and set a reasonerrors.NewClientErrorReporter(http.StatusInternalServerError, r.verb, "ClientWatchDecoding"),), nil
}
// NewStreamWatcher creates a StreamWatcher from the given decoder.
func NewStreamWatcher(d Decoder, r Reporter) *StreamWatcher {sw := &StreamWatcher{source: d,reporter: r,// It's easy for a consumer to add buffering via an extra// goroutine/channel, but impossible for them to remove it,// so nonbuffered is better.result: make(chan Event),// If the watcher is externally stopped there is no receiver anymore// and the send operations on the result channel, especially the// error reporting might block forever.// Therefore a dedicated stop channel is used to resolve this blocking.done: make(chan struct{}),}go sw.receive()return sw
}// StreamWatcher turns any stream for which you can write a Decoder interface
// into a watch.Interface.
type StreamWatcher struct {sync.Mutexsource Decoderreporter Reporterresult chan Eventdone chan struct{}
}// Interface can be implemented by anything that knows how to watch and report changes.
type Interface interface {// Stop stops watching. Will close the channel returned by ResultChan(). Releases// any resources used by the watch.Stop()// ResultChan returns a chan which will receive all the events. If an error occurs// or Stop() is called, the implementation will close this channel and// release any resources used by the watch.ResultChan() <-chan Event
}
看下具体是怎么进行处理的
- 从 source 中解码得到k8s中监听到的事件变化的action(动作)
- 将结果写入 result 这个 channel 中
- result 这个channel 就是我们最开始 watch.ResultChan 函数的返回结果
// receive reads result from the decoder in a loop and sends down the result channel.
func (sw *StreamWatcher) receive() {defer utilruntime.HandleCrash()defer close(sw.result)defer sw.Stop()for {action, obj, err := sw.source.Decode()if err != nil {switch err {case io.EOF:// watch closed normallycase io.ErrUnexpectedEOF:klog.V(1).Infof("Unexpected EOF during watch stream event decoding: %v", err)default:if net.IsProbableEOF(err) || net.IsTimeout(err) {klog.V(5).Infof("Unable to decode an event from the watch stream: %v", err)} else {select {case <-sw.done:case sw.result <- Event{Type: Error,Object: sw.reporter.AsObject(fmt.Errorf("unable to decode an event from the watch stream: %v", err)),}:}}}return}select {case <-sw.done:returncase sw.result <- Event{Type: action,Object: obj,}:}}
}// ResultChan implements Interface.
func (sw *StreamWatcher) ResultChan() <-chan Event {return sw.result
}
回顾一下我们接受channel的写法,我们拿到channel后,从里面读取数据,会根据bool值来判断channel是否已经关闭,关闭则不处理;
ch := watcher.ResultChan()
event, ok := <-ch:
那为什么channel会关闭呢,猜测一下?
- 客户端超时断开了?但是我们没设置timeout,则默认为0,就是无限制时间,不会主动断开
- 服务端主动断开了?有可能
在watcher建立后,我们通过 lsof -p 查看对应进程打开的连接,可以看到与 k8s 建立的 https 的连接,就是对应的 watcher 发起的http请求建立的 tcp 长链接;net/http 发起的 http 请求,是使用了 transport 连接池进行管理的,所以会默认维持长链接,感兴趣可以看下这篇文章 [[net-http-transport]]
xxx-75df5b458c-hj6qr:45006->kubernetes.default.svc.cluster.local:https (ESTABLISHED)
知道了对端域名和端口后,就能通过 tcpkill -i eth0 host kubernetes.default.svc.cluster.local and port 443 命令,来手动中断这个连接,看下 streamWatch 是怎么处理的;
在 streamWatcher 的 receive 函数 select 中打断点调试,最后发现是在 net.IsProbableEOF 函数中命中了 “connection reset by peer”
// IsProbableEOF returns true if the given error resembles a connection termination
// scenario that would justify assuming that the watch is empty.
// These errors are what the Go http stack returns back to us which are general
// connection closure errors (strongly correlated) and callers that need to
// differentiate probable errors in connection behavior between normal "this is
// disconnected" should use the method.
func IsProbableEOF(err error) bool {if err == nil {return false}var uerr *url.Errorif errors.As(err, &uerr) {err = uerr.Err}msg := err.Error()switch {case err == io.EOF:return truecase err == io.ErrUnexpectedEOF:return truecase msg == "http: can't write HTTP request on broken connection":return truecase strings.Contains(msg, "http2: server sent GOAWAY and closed the connection"):return truecase strings.Contains(msg, "connection reset by peer"):return truecase strings.Contains(strings.ToLower(msg), "use of closed network connection"):return true}return false
}
并且我们将日志级别设置为0,就能直接打印对应的infof日志:
I1226 09:38:19.316831 16623 streamwatcher.go:114] Unable to decode an event from the watch stream: read tcp 10.244.1.96:33006->10.96.0.1:443: read: connection reset by peer
ok,连接被对端关闭了,然后按照代码逻,就会直接return,在返回之前,会执行 defer 进行一些操作,receive 在方法开始就定义了 defer 资源回收
- 明确声明了会关闭 sw.result 这个channel
- stop 中则是将 source 这个 streamDecoder 关闭,最后调用到 http.transportResponseBody 进行关闭,这也是 net-http 源码 transport 的设计,不过k8s-apiserver 貌似用的http2的协议;
defer close(sw.result)
defer sw.Stop()
// Stop implements Interface.
func (sw *StreamWatcher) Stop() {// Call Close() exactly once by locking and setting a flag.sw.Lock()defer sw.Unlock()// closing a closed channel always panics, therefore check before closingselect {case <-sw.done:default:close(sw.done)sw.source.Close()}
}// 最终的close函数,会把未读的数据都flush出来再关闭
func (b transportResponseBody) Close() error {cs := b.cscc := cs.cccs.bufPipe.BreakWithError(errClosedResponseBody)cs.abortStream(errClosedResponseBody)unread := cs.bufPipe.Len()if unread > 0 {cc.mu.Lock()// Return connection-level flow control.connAdd := cc.inflow.add(unread)cc.mu.Unlock()// TODO(dneil): Acquiring this mutex can block indefinitely.// Move flow control return to a goroutine?cc.wmu.Lock()// Return connection-level flow control.if connAdd > 0 {cc.fr.WriteWindowUpdate(0, uint32(connAdd))}cc.bw.Flush()cc.wmu.Unlock()}select {case <-cs.donec:case <-cs.ctx.Done():// See golang/go#49366: The net/http package can cancel the// request context after the response body is fully read.// Don't treat this as an error.return nilcase <-cs.reqCancel:return errRequestCanceled}return nil
}
再梳理一下整个流程:
- 我们通过client-go提供的方法创建一个watcher,监听对应的资源
- watcher 会先向 kube-apiserver 发起一个 http 请求,告知 apiserver 启用 watch 机制监听某类型的资源
- 服务与apiserver建立了连接后,就通过FD进行读写传输
- 最终变更的事件,是通过 channel 与我们的服务进行通信
- 当apiserver关闭了连接,streamwatcher就会return并进行资源回收,从而关闭 channel
问题原因
- apiserver 主动关闭了 TCP 连接,客户端 streamWatcher 将channel回收关闭了,所以,我们通过 watcher.ResultChan 获取到的 channel 永远都是关闭的
- apiserver 主动关闭连接有几个可能原因
- 监听的资源被删除了,尝试了手动删除,发现watcher还是存在不会关闭
- 长时间没有事件变更,TCP连接会自动断开(大概在30min左右)(事实证明就是这个原因)
- 其他xxx
解决办法
- 如果发现 channel 被关闭了,则重新建立一个 watcher 进行监听即可
改进后的代码:
func WatchDeployment(ctx context.Context, namespace string, options metav1.ListOptions, handler EventHandler) {log.Infof("start watch deployment: %+v", options)for {func() {defer func() {if r := recover(); r != nil {log.Warnf("The Kubernetes deployment watcher is attempting to restart for recovery. err: %v", r)}}()if err := runLoop(ctx, namespace, options, handler); err != nil {log.Errorf("Kubernetes deployment watcher has exited in runLoop: %v", err)}}()time.Sleep(5 * time.Second) // 等待一段时间后重试}
}func runLoop(ctx context.Context, namespace string, options metav1.ListOptions, handler EventHandler) error {watcher, err := KubeCli.AppsV1().Deployments(namespace).Watch(ctx, options)if err != nil {return err}ch := watcher.ResultChan()for {select {case event, ok := <-ch:if !ok {// channel 关闭,重启 watcherlog.Infof("Kubernetes hung up on us, restarting deployment watcher")return nil}deployment, ok := event.Object.(*appsv1.Deployment)if !ok {log.Errorf("Error casting to Deployment")continue}// 处理事件switch event.Type {case watch.Added:if handler.OnAdd != nil {handler.OnAdd(ctx, deployment)}case watch.Modified:if handler.OnModify != nil {handler.OnModify(ctx, deployment)}case watch.Deleted:if handler.OnDelete != nil {handler.OnDelete(ctx, deployment)}}case <-time.After(30 * time.Minute):// 超时,重启 watcherlog.Infof("Timeout, restarting deployment watcher")return nilcase <-ctx.Done():log.Info("Context done, stopping watch")return nil}}
}
其他疑问
1、为什么发起一个 http 请求,apiserver 就能与这个请求建立连接,进行 watch 并增量通知,apiserver 是怎么实现的?
- 推荐阅读:
- https://cloud.tencent.com/developer/article/1991054
- etcd教程(五)—watch机制原理分析
2、为什么 list-watch 机制不会每隔一段时间就关闭连接?(貌似有探活?)
3、StreamWatcher 中包装的 Decoder 是怎么与TCP连接的描述符关联上的,读写是怎么传输的?
相关文章:
client-go中watch机制的一些陷阱
Reference https://stackoverflow.com/questions/51399407/watch-in-k8s-golang-api-watches-and-get-events-but-after-sometime-doesnt-get-an 问题描述 最近在使用 client-go 的 watch 机制监听 k8s 中的 deployment 资源时,发现一个奇怪的现象 先看下代码&a…...
Chrome访问https页面显示ERR_CERT_INVALID,且无法跳过继续访问
在访问网页的时候,因为浏览器自身的安全设置问题, 对于https的网页访问会出现安全隐私的提示, 甚至无法访问对应的网站,尤其是chrome浏览器, 因此本文主要讲解如何设置chrome浏览器的设置,来解决该问题&…...
Jenkins pipeline 发送邮件及包含附件
Jenkins pipeline 发送邮件及包含附件 设置邮箱开启SMTP服务 此处适用163 邮箱 开启POP3/SMTP服务通过短信获取TOKEN (保存TOKEN, 后面Jenkins会用到) Jenkins 邮箱设置 安装 Build Timestamp插件 设置全局凭证 Dashboard -> Manage Jenkins …...
怎么把word试题转成excel?
在教育行业、学校管理以及在线学习平台中,试题库的高效管理是一项核心任务。许多教育工作者和系统开发人员常常面临将 Word 中的试题批量导入 Excel 的需求。本文将详细介绍如何快速将试题从 Word 转换为 Excel,帮助您轻松解决繁琐的数据整理问题&#x…...
【机器学习】量子机器学习:当量子计算遇上人工智能,颠覆即将来临?
我的个人主页 我的领域:人工智能篇,希望能帮助到大家!!!👍点赞 收藏❤ 在当今科技飞速发展的时代,量子计算与人工智能宛如两颗璀璨的星辰,各自在不同的苍穹闪耀,正以前…...
IDEA配置maven和git并如何使用maven打包和git推送到gitlab
首先找到设置 在里面输入maven然后找到点击 然后点击右边两个选项 路径选择下载的maven目录下的settings文件和新建的repository文件夹 点击apply应用 然后在搜索框里搜git点击进去 此路径为git的exe执行文件所在目录,选好之后点击test测试下方出现git版本号表…...
Supermaven 加入 Cursor:AI 编码新篇章
引言 2024 年 11 月 11 日,我们迎来了一个激动人心的时刻——Supermaven 正式加入 Cursor! 这一合作标志着 AI 编程工具进入了一个新的发展阶段,为开发者提供更智能、更高效的编码体验。本文将带您了解此次合并的背景、意义以及未来的发展方…...
【2024华为OD-E卷-100分-boss的收入】(题目+思路+JavaC++Python解析)
题目描述 题目:boss的收入 在一个公司中,有一个老板(boss)和若干名员工(employees)。老板和员工的收入信息存储在一个数组中,其中数组的每个元素表示一个人的收入。数组的第0个元素表示老板的…...
《Java8实战》汇总
参考书籍:《Java8 实战》 一、Lambda表达式 Lambda 是一个匿名函数。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。 1.1、Lambda表达式的关键:从匿名类到 Lambda 的转换 示例: <span style="background-color:#…...
Elasticsearch:搜索相关性
这里写目录标题 一、相关性的概述二、自定义评分策略1、TF-IDF算法2、BM25算法 三、自定义评分策略1、Index Boost:在索引层面修改相关性2、boosting:修改文档相关性3、negative_boost:降低相关性4、function_score:自定义评分5、…...
LeetCode 热题 100_二叉树展开为链表(46_114_中等_C++)(二叉树;先序遍历(递归+数组);先序遍历(递归))
LeetCode 热题 100_二叉树展开为链表(46_114) 题目描述:输入输出样例:题解:解题思路:思路一(先序遍历(递归数组)):思路二(先序遍历&am…...
uniapp实现在card卡片组件内为图片添加长按保存、识别二维码等功能
在原card组件的cover属性添加图片的话,无法在图片上面绑定 show-menu-by-longpress"true"属性,通过将图片自定义添加可使用该属性。 代码: <uni-card title"标题" padding"10px 0" :thumbnail"avata…...
最好用的图文识别OCR -- PaddleOCR(2) 提高推理效率(PPOCR模型转ONNX模型进行推理)
在实际推理过程中,使用 PaddleOCR 模型时效率较慢,经测试每张图片的检测与识别平均耗时超过 5 秒,这在需要大规模自动化处理的场景中无法满足需求。为此,我尝试将 PaddleOCR 模型转换为 ONNX 格式进行推理,以提升效率。…...
Redis--20--大Key问题解析
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 大Key问题1.什么是 Redis 大 Key?在 Redis 中,大 Key 是指单个键值对的数据量非常大,可能包含大量数据。 2. Redis大Key的危害3.…...
新版2024AndroidStudio项目目录结构拆分
如题 下载了最新版的android studio 发现目录结构和以前不一样 自动帮你合并了 如何层层抽丝剥茧呢 按照一下步骤即可解决问题!...
STM32内置Flash
一、原理 利用flash存储用户数据需要注意查看,用户数据是否会覆盖芯片运行程序。 IAP(在程序中编程)利用程序修改程序本身,和OTA是一个原理。IAP在程序中编程支持任意一种通信下载。 ICP(在电路中编程,通…...
华为路由器、交换机、AC、新版本开局远程登录那些坑(Telnet、SSH/HTTP避坑指南)
关于华为设备远程登录配置开启的通用习惯1、HTTP/HTTPS相关服务 http secure-server enablehttp server enable 2、Telnet服务telnet server enable3、SSH服务stelnet server enablessh user admin authentication-type password 「模拟器、工具合集」复制整段内容 链接&…...
【Linux】深入理解进程信号机制:信号的产生、捕获与阻塞
🎬 个人主页:谁在夜里看海. 📖 个人专栏:《C系列》《Linux系列》《算法系列》 ⛰️ 时间不语,却回答了所有问题 目录 📚前言 📚一、信号的本质 📖1.异步通信 📖2.信…...
前端基础技术全解析:从HTML前端基础标签语言开始,逐步深入CSS样式修饰、JavaScript脚本控制、Ajax异步通信以及WebSocket持久通信
目录 前言: 1.前端技术html简单了解: 1.1HTML代码是由标签构成的。 1.2.HTML 文件基本结构 1.3.HTML 常见标签 标题标签: 段落标签: p 文本格式化标签 图片标签: 超链接标签: a 测试代码: 展示效果: 表单…...
Linux存储管理之核心秘密(The Core Secret of Linux Storage Management)
Linux存储管理之核心秘密 如果你来自Windows环境,那么Linux处理和管理存储设备的方式对你而言可能显得格外不同。我们知道,Linux的文件系统并不采用Windows那样的物理驱动器表示方式(如C:、D:或E:),而是构建了一个以&…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
