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

Go-知识测试-工作机制

Go-知识测试-工作机制

  • 生成test的main
  • test的main如何启动case
  • 单元测试 runTests
    • tRunner
    • testing.T.Run
  • 示例测试 runExamples
    • runExample
    • processRunResult
  • 性能测试 runBenchmarks
    • runN
    • testing.B.Run

在 Go 语言的源码中,go test 命令的实现主要在 src/cmd/go/internal/test 包中。当你运行 go test 命令时,Go 的命令行工具会调用这个包中的代码来执行测试。
以下是 go test 命令的大致执行流程:

  1. 首先,go test 命令会解析命令行参数,获取需要测试的包和测试选项。
  2. 然后,go test 命令会构建一个测试的二进制文件。这个二进制文件包含了需要测试的包和测试用例,以及测试用例的运行环境和测试框架。
  3. 接着,go test 命令会启动这个二进制文件,并将命令行参数传递给它。这个二进制文件会运行测试用例,并将测试结果输出到标准输出。
  4. 最后,go test 命令会读取这个二进制文件的输出,解析测试结果,并将测试结果显示给用户。

在 src/cmd/go/internal/test 包中,runTest 函数是 go test 命令的主要入口点。这个函数负责解析命令行参数,构建测试的二进制文件,启动这个二进制文件,以及读取和解析测试结果。
在 runTest 函数中,runTest 函数会调用 load.TestPackagesFor 函数来获取需要测试的包,然后调用 builder.runTest 函数来构建和运行测试的二进制文件。builder.runTest 函数会调用 builder.runOut 函数来启动这个二进制文件,并将这个二进制文件的输出连接到 go test 命令的标准输出。
在 builder.runTest 函数中,builder.runTest 函数会调用 builder.compile 函数来编译需要测试的包,然后调用 builder.link 函数来链接这个包和测试框架,生成测试的二进制文件。

生成test的main

详细的来说:
首先执行 go test命令,是一个内部命令,在源码的cmd/go
在这里插入图片描述

在这里有个main入口
在这里插入图片描述

在main函数里面执行 invoke 函数
在这里插入图片描述

在invoke里面执行Run
在这里插入图片描述

针对 go test 执行是初始化的test命令
在这里插入图片描述

在test中执行的是runTest
在这里插入图片描述

runTest的内容如下
在这里插入图片描述

会解析入参等
然后会执行
在这里插入图片描述

在builderTest中,构建test程序
在这里插入图片描述

在load包中打包
在这里插入图片描述

在这里插入图片描述

为什么go的测试都是 _test 结尾呢?

在这里插入图片描述

在打包test的时候,会将 path_test 也加入
针对test的程序,会构造一个main入口
在这里插入图片描述

真正的go test main 生成
在这里插入图片描述

使用模板生成
在这里插入图片描述

其中 testmainTmpl 是一个模板
在这里插入图片描述

也是有一个main入口
在这里插入图片描述

在go 1.17 中,渲染后的代码如下

package mainimport ("os""testing""testing/internal/testdeps"_test "mypackage"
)var tests = []testing.InternalTest{{"TestFunc1", _test.TestFunc1},{"TestFunc2", _test.TestFunc2},
}var benchmarks = []testing.InternalBenchmark{{"BenchmarkFunc1", _test.BenchmarkFunc1},
}var examples = []testing.InternalExample{{"ExampleFunc1", _test.ExampleFunc1, "", false},
}func main() {testdeps.ImportPath = "mypackage"m := testing.MainStart(testdeps.TestDeps, tests, benchmarks, examples)os.Exit(m.Run())
}

通过main方法,直到实际上是调用 testing.MainStart获取了一个*testing.M
然后调用m.Run
这就是 Main 测试的执行原理。

test的main如何启动case

接下来看看testing.M是什么
MainStart 初始化并生成了一个testing.M
在这里插入图片描述

Init操作是解析 go test的命令行参数
在这里插入图片描述

testing.M的结构如下

type M struct {deps       testDepstests      []InternalTestbenchmarks []InternalBenchmarkexamples   []InternalExampletimer     *time.TimerafterOnce sync.OncenumRun intexitCode int
}

从上面的结构体可以看出,主要是三类测试用例:单元测试,性能测试和示例测试。
接下来看下Run方法:
在这里插入图片描述

首先根据命令参数,执行不同的逻辑:
*matchList 表示执行 go test -list regStr 表示不是真的执行测试,而是列出 regStr 匹配的case 列表:
在匹配的时候,会对三类用例的name都进行匹配
在这里插入图片描述

*shuffle 表示洗牌,也就是随机,使用随机包rand的Shuffle方法进行洗牌
接着执行befor,在befor里面,主要是对执行环境的一些初始化,或者对命令参数的设置等
在这里插入图片描述

在befor执行后,依次执行三类用例
在这里插入图片描述

等用例执行完成后,执行after,after是对执行结果的汇总等
在这里插入图片描述

最核心的就是三个方法:runTests,runExamples,runBenchmarks

单元测试 runTests

func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest, deadline time.Time) (ran, ok bool) {ok = truefor _, procs := range cpuList {runtime.GOMAXPROCS(procs)for i := uint(0); i < *count; i++ {if shouldFailFast() {break}ctx := newTestContext(*parallel, newMatcher(matchString, *match, "-test.run"))ctx.deadline = deadlinet := &T{common: common{signal:  make(chan bool, 1),barrier: make(chan bool),w:       os.Stdout,},context: ctx,}if Verbose() {t.chatty = newChattyPrinter(t.w)}tRunner(t, func(t *T) {for _, test := range tests {t.Run(test.Name, test.F)}})select {case <-t.signal:default:panic("internal error: tRunner exited without sending on t.signal")}ok = ok && !t.Failed()ran = ran || t.ran}}return ran, ok
}

如果指定了cpu并且指定了count,那么会对单元测试执行 cpu数量乘以count次
接着初始化 TestContext
在这里插入图片描述

然后初始化testing.T
在这里插入图片描述

testing.T组合了TestContext,并且组合了testing.common
testing.common初始化了两个信号channel,用于控制单元测试执行。
最后调用tRunner执行单元测试

tRunner

func tRunner(t *T, fn func(t *T)) {t.runner = callerName(0) // 获取当前测试函数的名称//当这个goroutine完成时,要么是因为fn(t)//正常返回或由于触发测试失败//对运行时的调用。Goexit,记录持续时间并发送//表示测试完成的信号。defer func() {// 测试失败,那么将失败数+1if t.Failed() {atomic.AddUint32(&numFailed, 1)}// 如果测试惊慌失措,请在终止之前打印任何测试输出。err := recover()signal := true// 读锁定t.mu.RLock()// 获取完成状态finished := t.finished// 读锁定解锁t.mu.RUnlock()// 如果测试未完成,但是异常信息为空if !finished && err == nil {// 将错误信息赋值为空错误或空异常err = errNilPanicOrGoexit// 如果有父测试,当前是子测试for p := t.parent; p != nil; p = p.parent {p.mu.RLock()finished = p.finishedp.mu.RUnlock()if finished {t.Errorf("%v: subtest may have called FailNow on a parent test", err)err = nilsignal = falsebreak}}}// 使用延迟调用以确保我们报告测试// 完成,即使清除函数调用t.FailNow。请参见第41355期。didPanic := falsedefer func() {if didPanic {return}if err != nil {panic(err)}//只有在没有恐慌的情况下才报告测试完成,//否则,测试二进制文件可以在死机之前退出//报告给用户。请参见第41479期。t.signal <- signal}()doPanic := func(err interface{}) {// 设置测试失败t.Fail()if r := t.runCleanup(recoverAndReturnPanic); r != nil {t.Logf("cleanup panicked with %v", r)}//在终止之前将输出日志刷新到根目录。for root := &t.common; root.parent != nil; root = root.parent {root.mu.Lock()// 计算时间root.duration += time.Since(root.start)d := root.durationroot.mu.Unlock()root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))if r := root.parent.runCleanup(recoverAndReturnPanic); r != nil {fmt.Fprintf(root.parent.w, "cleanup panicked with %v", r)}}didPanic = truepanic(err)}if err != nil {doPanic(err)}t.duration += time.Since(t.start)// 如果有子测试,当前是父测试if len(t.sub) > 0 {// 停止测试t.context.release()// 释放平行的子测验。close(t.barrier)// 等待子测验完成。for _, sub := range t.sub {<-sub.signal}cleanupStart := time.Now()err := t.runCleanup(recoverAndReturnPanic)t.duration += time.Since(cleanupStart)if err != nil {doPanic(err)}// 如果不是并发的if !t.isParallel {// 等待开始t.context.waitParallel()}} else if t.isParallel { // 如果是并发的//仅当此测试以并行方式运行时才释放其计数 测验请参阅Run方法中的注释。t.context.release()}// 测试执行结束上报日志t.report()t.done = true// 如果有父测试,那么设置执行标志if t.parent != nil && atomic.LoadInt32(&t.hasSub) == 0 {t.setRan()}}()defer func() {if len(t.sub) == 0 {t.runCleanup(normalPanic)}}()t.start = time.Now()t.raceErrors = -race.Errors()fn(t)// code beyond here will not be executed when FailNow is invokedt.mu.Lock()t.finished = truet.mu.Unlock()
}

在tRunner中执行的是 fn(t),其中t就是*testing.T,这也是单元测试的写法标准:

func TestXx(t *testing.T){}

而fn并不是我们在testing.M中指定的单元测试键值对,而是在runTests中进行二次包装的
在这里插入图片描述

换句话说,我们自己写的单元测试,被测试框架经过模板生成test的main启动,然后在进行了初始化后,
进行了按照参数进行分批,接着在goroutine中,按照分配的case进行逐个执行。

testing.T.Run

// 将运行f作为名为name的t的子测试。它在一个单独的goroutine中运行f
// 并且阻塞直到f返回或调用t。并行成为并行测试。
// 运行报告f是否成功(或者至少在调用t.Parallel之前没有失败)。
//
// Run可以从多个goroutine同时调用,但所有此类调用
// 必须在t的外部测试函数返回之前返回。
func (t *T) Run(name string, f func(t *T)) bool {// 将子测试的数量+1atomic.StoreInt32(&t.hasSub, 1)// 获取匹配的测试nametestName, ok, _ := t.context.match.fullName(&t.common, name)// 如果没有配置,那么直接结束if !ok || shouldFailFast() {return true}//记录此调用点的堆栈跟踪,以便如果子测试//在单独的堆栈中运行的函数被标记为助手,我们可以//继续将堆栈遍历到父测试中。var pc [maxStackLen]uintptr// 获取调用者的函数namen := runtime.Callers(2, pc[:])t = &T{ // 创建一个新的 testing.T 用于执行子测试common: common{barrier: make(chan bool),signal:  make(chan bool, 1),name:    testName,parent:  &t.common,level:   t.level + 1,creator: pc[:n],chatty:  t.chatty,},context: t.context,}t.w = indenter{&t.common}if t.chatty != nil {t.chatty.Updatef(t.name, "=== RUN   %s\n", t.name)}//而不是在调用之前减少此测试的运行计数//tRunner并在之后增加它,我们依靠tRunner保持//计数正确。这样可以确保运行一系列顺序测试//而不会被抢占,即使它们的父级是并行测试。这//如果*parallel==1,则可以特别减少意外。go tRunner(t, f)if !<-t.signal {//此时,FailNow很可能是在//其中一个子测验的家长测验。继续中止链的上行。runtime.Goexit()}return !t.failed
}

示例测试 runExamples

func runExamples(matchString func(pat, str string) (bool, error), examples []InternalExample) (ran, ok bool) {ok = truevar eg InternalExamplefor _, eg = range examples {matched, err := matchString(*match, eg.Name)if err != nil {fmt.Fprintf(os.Stderr, "testing: invalid regexp for -test.run: %s\n", err)os.Exit(1)}if !matched {continue}ran = trueif !runExample(eg) {ok = false}}return ran, ok
}

示例测试就简单一点了,首先根据正则进行匹配,匹配到了就执行,否则就跳过,出错就退出

runExample

在runExample中,首先对标准输出进行拷贝,将控制输出进行解析
在这里插入图片描述

然后在defer中对输出进行比对
在这里插入图片描述

processRunResult

输出结果比对就简单,主要是字符串的一些比较
在这里插入图片描述

在示例测试中,输出结果的行不需要顺序一致,是因为在比对前,会进行排序

性能测试 runBenchmarks

性能测试和单元测试差不多,只是结构体不同,性能测试的结构体是testing.B
在这里插入图片描述

同样的,也是先创建了一个main的testing.B用于启动性能测试,相当于作为初始case
在这里插入图片描述

然后启动初始case的runN启动

runN

runN作为启动性能测试的初始测试,也是逐个执行用户定义的性能测试case
在这里插入图片描述

实际执行的是testing.B.Run方法
在这里插入图片描述

testing.B.Run

testing.B.Runtesting.T.Run类似,主要是对子测试等做处理,然后执行用户的case
在这里插入图片描述

相关文章:

Go-知识测试-工作机制

Go-知识测试-工作机制 生成test的maintest的main如何启动case单元测试 runTeststRunnertesting.T.Run 示例测试 runExamplesrunExampleprocessRunResult 性能测试 runBenchmarksrunNtesting.B.Run 在 Go 语言的源码中&#xff0c;go test 命令的实现主要在 src/cmd/go/internal…...

【小程序静态页面】猜拳游戏大转盘积分游戏小程序前端模板源码

猜拳游戏大转盘积分游戏小程序前端模板源码&#xff0c; 一共五个静态页面&#xff0c;首页、任务列表、大转盘和猜拳等五个页面。 主要是通过做任务来获取积分&#xff0c;积分可以兑换商品&#xff0c;也可用来玩游戏&#xff1b;通过玩游戏既可能获取奖品或积分也可能会消…...

JupyterServer配置

1. 安装jupyter ​pip install jupyter -i https://pypi.tuna.tsinghua.edu.cn/simple --default-timeout1000 2. 生成配置 jupyter notebook --generate-config 3. 修改配置&#xff0c;设置密码 获取密码的方式&#xff1a;命令行输入python后&#xff0c;用以下方式获…...

信息检索(57):MINIMIZING FLOPS TO LEARN EFFICIENT SPARSE REPRESENTATIONS

MINIMIZING FLOPS TO LEARN EFFICIENT SPARSE REPRESENTATIONS 摘要1 引言2 相关工作3 预期 FLOPS 次数4 我们的方法5 实验6 结论 发布时间&#xff08;2020&#xff09; 最小化 Flop 来学习高效的稀疏表示 摘要 1&#xff09;学习高维稀疏表示 2&#xff09;FLOP 集成到损失…...

Python 面试【中级】

欢迎莅临我的博客 &#x1f49d;&#x1f49d;&#x1f49d;&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…...

[Open-source tool]Uptime-kuma的簡介和安裝於Ubuntu 22.04系統

[Uptime Kuma]How to Monitor Mqtt Broker and Send Status to Line Notify Uptime-kuma 是一個基於Node.js的開軟軟體&#xff0c;同時也是一套應用於網路監控的開源軟體&#xff0c;其利用瀏覽器呈現直觀的使用者介面&#xff0c;如圖一所示&#xff0c;其讓使用者可監控各種…...

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 灰度图像恢复(100分) - 三语言AC题解(Python/Java/Cpp)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f…...

leetcode494. 目标和

1.思想方法 2.代码 class Solution { public int findTargetSumWays(int[] nums, int target) {int sum 0;for(int num : nums)sum num;if(sum < Math.abs(target) || (targetsum)%2 ! 0)return 0;int x (targetsum) / 2,n nums.length;//基于滚动数组的方法int[] dp…...

数据结构简介

在容器的基础之上&#xff0c;java引入了数据结构的概念。数据结构可以简单地理解成是一个以特定的布局方式来存储数据的容器。但是我个人觉得这种理解方式不太合理&#xff0c;根据我们学的数据结构的内容&#xff0c;我更倾向于数据结构是数据在容器中的布局方式&#xff0c;…...

PyScript:在浏览器中释放Python的强大

PyScript&#xff1a;Python代码&#xff0c;直接在网页上运行。- 精选真开源&#xff0c;释放新价值。 概览 PyScript是一个创新的框架&#xff0c;它打破了传统编程环境的界限&#xff0c;允许开发者直接在浏览器中使用Python语言来创建丰富的网络应用。结合了HTML界面、Pyo…...

巴黎成为欧洲AI中心 大学开始输出AI创始人

来自Dealroom 的数据显示&#xff0c;在欧洲和以色列AI创业公司中&#xff0c;法国的AI创业公司资金最充裕。Mistral、Owkin、Hugging Face等法国企业已经融资23亿美元&#xff0c;比英国、德国AI创业公司都要多。 一名大学生走出校门凭借聪明才智和一个黄金点子成为富豪&#…...

完全离线的本地问答模型LocalGPT如何实现无公网IP远程连接提问

文章目录 前言环境准备1. localGPT部署2. 启动和使用3. 安装cpolar 内网穿透4. 创建公网地址5. 公网地址访问6. 固定公网地址 前言 本文主要介绍如何本地部署LocalGPT并实现远程访问&#xff0c;由于localGPT只能通过本地局域网IP地址端口号的形式访问&#xff0c;实现远程访问…...

【算法专题--栈】栈的压入、弹出序列 -- 高频面试题(图文详解,小白一看就懂!!)

目录 一、前言 二、题目描述 三、解题方法 &#x1f4a7;栈模拟法&#x1f4a7;-- 双指针 ⭐ 解题思路 ⭐ 案例图解 四、总结与提炼 五、共勉 一、前言 栈的压入、弹出序列 这道题&#xff0c;可以说是--栈专题--&#xff0c;最经典的一道题&#xff0c;也是在…...

如何高效安全的开展HPC数据传输,保护数据安全?

高性能计算&#xff08;HPC&#xff09;在多个行业和领域中都有广泛的应用&#xff0c;像科学研究机构、芯片IC设计企业、金融、生物制药、能源、航天航空等。HPC&#xff08;高性能计算&#xff09;环境中的数据传输是一个关键环节&#xff0c;它涉及到将数据快速、安全地在不…...

Java部分复习笔记整理

一、Java常用类 1.String类 表示字符串&#xff0c;不可变&#xff0c;常用方法包括length(), charAt(), substring(), indexOf(), equals()等。 2.ArrayList类 基于数组实现的动态数组&#xff0c;可变大小&#xff0c;常用方法包括add(), get(), set(), remove(), size()…...

GoLang语言

基础 安装Go扩展 go build 在项目目录下执行go build go run 像执行脚本文件一样执行Go代码 go install go install分为两步&#xff1a; 1、 先编译得到一个可执行文件 2、将可执行文件拷贝到GOPATH/bin Go 命令 go build :编译Go程序 go build -o "xx.exe"…...

ctfshow web入门 sqli-labs web517--web524

web517 注入点id ?id-1’union select 1,2,3– 确认是否能够注入 ?id-1union select 1,database(),3-- 爆出库名 security爆出表名 ?id-1union select 1,(select group_concat(table_name) from information_schema.tables where table_schemasecurity),3-- emails,refer…...

Spring Cloud Gateway 跨域配置和跨服务请求跟踪

文章目录 引言I Spring Cloud Gateway 跨域配置1.1 网关统一处理:配置文件-推荐1.2 网关统一处理:配置类方式1.3 微服务处理,网关侧不用处理CORS。1.4 子服务依赖配置1.5 网关服务的依赖配置II 跨服务请求日志跟踪2.1 feign 依赖配置2.2 feign子模块将请求头中的参数,全部作…...

动手学深度学习(Pytorch版)代码实践 -卷积神经网络-29残差网络ResNet

29残差网络ResNet import torch from torch import nn from torch.nn import functional as F import liliPytorch as lp import matplotlib.pyplot as plt# 定义一个继承自nn.Module的残差块类 class Residual(nn.Module):def __init__(self, input_channels, num_chan…...

解锁音乐潮流:使用TikTok API获取平台音乐信息

一、引言 TikTok&#xff0c;作为全球领先的短视频社交平台&#xff0c;不仅为用户提供了展示自我、分享生活的舞台&#xff0c;还为用户带来了丰富多样的音乐体验。在TikTok上&#xff0c;音乐与视频内容的结合&#xff0c;为用户带来了全新的视听盛宴。对于音乐制作人、品牌…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

回溯算法学习

一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...

现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?

现有的 Redis 分布式锁库&#xff08;如 Redisson&#xff09;相比于开发者自己基于 Redis 命令&#xff08;如 SETNX, EXPIRE, DEL&#xff09;手动实现分布式锁&#xff0c;提供了巨大的便利性和健壮性。主要体现在以下几个方面&#xff1a; 原子性保证 (Atomicity)&#xff…...

OD 算法题 B卷【正整数到Excel编号之间的转换】

文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的&#xff1a;a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...

Chrome 浏览器前端与客户端双向通信实战

Chrome 前端&#xff08;即页面 JS / Web UI&#xff09;与客户端&#xff08;C 后端&#xff09;的交互机制&#xff0c;是 Chromium 架构中非常核心的一环。下面我将按常见场景&#xff0c;从通道、流程、技术栈几个角度做一套完整的分析&#xff0c;特别适合你这种在分析和改…...

LOOI机器人的技术实现解析:从手势识别到边缘检测

LOOI机器人作为一款创新的AI硬件产品&#xff0c;通过将智能手机转变为具有情感交互能力的桌面机器人&#xff0c;展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家&#xff0c;我将全面解析LOOI的技术实现架构&#xff0c;特别是其手势识别、物体识别和环境…...

【WebSocket】SpringBoot项目中使用WebSocket

1. 导入坐标 如果springboot父工程没有加入websocket的起步依赖&#xff0c;添加它的坐标的时候需要带上版本号。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dep…...

在Zenodo下载文件 用到googlecolab googledrive

方法&#xff1a;Figshare/Zenodo上的数据/文件下载不下来&#xff1f;尝试利用Google Colab &#xff1a;https://zhuanlan.zhihu.com/p/1898503078782674027 参考&#xff1a; 通过Colab&谷歌云下载Figshare数据&#xff0c;超级实用&#xff01;&#xff01;&#xff0…...

使用python进行图像处理—图像滤波(5)

图像滤波是图像处理中最基本和最重要的操作之一。它的目的是在空间域上修改图像的像素值&#xff0c;以达到平滑&#xff08;去噪&#xff09;、锐化、边缘检测等效果。滤波通常通过卷积操作实现。 5.1卷积(Convolution)原理 卷积是滤波的核心。它是一种数学运算&#xff0c;…...