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

在Go中处理异常

引言

程序遇到的错误大致分为两类:程序员预料到的错误和程序员没有预料到的错误。我们在前两篇关于[错误处理]的文章中介绍的error接口主要处理我们在编写Go程序时预期的错误。error接口甚至允许我们承认函数调用发生错误的罕见可能性,因此我们可以在这些情况下进行适当的响应。

异常属于第二类错误,是程序员没有预料到的。这些不可预见的错误会导致程序自动终止并退出正在运行的Go程序。常见的错误往往会造成异常。在本教程中,我们将研究Go中常见操作可能产生严重错误的几种方式,我们还将了解避免这些严重错误的方法。我们还将使用[defer]语句和recover函数来捕获错误,以免它们意外地终止我们正在运行的Go程序。

理解异常

Go中的某些操作会自动返回异常并停止程序。常见的操作包括超出[array]容量的索引、执行类型断言、调用nil指针的方法、错误地使用互斥量以及尝试使用闭合通道。这些情况大多是由编程时犯的错误导致的,而编译器在编译程序时无法检测到这些错误。

由于异常包含对解决问题有用的细节,开发人员通常将异常用作在程序开发过程中犯了错误的指示。

出界异常

当您试图访问超出切片长度或数组容量的索引时,Go运行时将生成一个异常。

下面的例子犯了一个常见的错误,即试图使用内置函数len返回的切片长度来访问切片的最后一个元素。试着运行这段代码,看看为什么会产生异常:

package mainimport ("fmt"
)func main() {names := []string{"lobster","sea urchin","sea cucumber",}fmt.Println("My favorite sea creature is:", names[len(names)])
}
Outputpanic: runtime error: index out of range [3] with length 3goroutine 1 [running]:
main.main()/tmp/sandbox879828148/prog.go:13 +0x20

异常的输出名称提供了一个提示:panic: runtime error: index out of range。我们制作了一个有三个海洋生物的切片。然后,我们尝试使用内置函数len将切片的长度作为索引来获取切片的最后一个元素。别忘了,切片和数组是从0开始的;所以这个切片的第一个元素是0,最后一个元素的索引是2。由于我们试图在第三个索引3处访问切片,因此切片中没有可以返回的元素,因为它超出了切片的边界。运行时别无选择,只能终止和退出,因为我们要求它做一些不可能的事情。Go也无法在编译期间证明此代码将尝试执行此操作,因此编译器无法捕获此操作。

还要注意,后续的代码没有运行。这是因为异常是一个会完全停止Go程序执行的事件。生成的消息包含多种有助于诊断异常原因的信息。

异常的剖析

严重错误由指示严重错误原因的消息和堆栈跟踪组成,后者可以帮助您定位代码中产生严重错误的位置。

任何异常的第一部分都是信息。它总是以字符串panic:开始,后面跟着一个根据异常原因而变化的字符串。上一个练习中的异常语句包含如下消息:

panic: runtime error: index out of range [3] with length 3

panic:前缀后面的字符串runtime error:告诉我们,异常是由语言运行时生成的。这个错误告诉我们,我们试图使用的索引[3]超出了切片长度3的范围。

此消息之后是堆栈跟踪。堆栈跟踪形成了一个map,我们可以根据它准确定位生成异常时正在执行的代码行,以及之前的代码如何调用该代码。

goroutine 1 [running]:
main.main()/tmp/sandbox879828148/prog.go:13 +0x20

前面例子中的堆栈跟踪显示,我们的程序从第13行文件/tmp/sandbox879828148/prog.go中生成了异常。它还告诉我们,此异常是在main包中的main()函数中生成的。

堆栈跟踪被分成多个独立的块——一个用于程序中的每个goroutine。每个Go程序的执行都是由一个或多个goroutines完成的,这些goroutines可以独立和同时执行Go代码的部分内容。每个块都以goroutine X [state]:开头。header给出了goroutine的ID号以及发生异常时它所处的状态。在header之后,堆栈跟踪显示了发生严重错误时程序正在执行的函数,以及该函数执行的文件名和行号。

前面例子中的异常是由对切片的越界访问产生的。当对未设置的指针调用方法时,也可能产生异常。

Nil接收器

Go编程语言有指针,指向运行时存在于计算机内存中的某种类型的特定实例。指针可以假定值为nil,表示它们不指向任何东西。当我们试图在一个为nil的指针上调用方法时,Go运行时将生成一个异常。类似地,接口类型的变量在被方法调用时也会产生错误。要查看在这些情况下产生的严重错误,请尝试以下示例:

package mainimport ("fmt"
)type Shark struct {Name string
}func (s *Shark) SayHello() {fmt.Println("Hi! My name is", s.Name)
}func main() {s := &Shark{"Sammy"}s = nils.SayHello()
}
Outputpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfebagoroutine 1 [running]:
main.(*Shark).SayHello(...)/tmp/sandbox160713813/prog.go:12
main.main()/tmp/sandbox160713813/prog.go:18 +0x1a

在这个例子中,我们定义了一个名为Shark的结构体。Shark在它的指针接收器上定义了一个名为SayHello的方法,当被调用时,它会向标准输出打印一条问候语。在我们的main函数体内,我们创建了这个Shark结构体的一个新实例,并使用&操作符请求一个指向它的指针。这个指针被赋值给s变量。然后我们使用s = nil语句将s变量重新赋值为nil。最后,我们尝试在变量s上调用SayHello方法。我们没有收到来自Sammy的友好消息,而是收到了一个警告,表示我们试图访问一个无效的内存地址。因为s变量是nil,当调用SayHello函数时,它尝试访问*Shark类型的字段Name。因为这是一个指针接收器,在这个例子中接收器是nil,它无法解引nil指针,所以出现了问题。

虽然我们在这个例子中显式地将s设置为nil,但实际上这种情况并不明显。当你看到涉及nil pointer dereference的严重错误时,请确保你已经正确地为任何你可能创建的指针变量赋值。

由nil指针产生的严重错误和越界访问是运行时产生的两种常见严重错误。也可以使用内置函数手动生成异常。

使用内置函数panic

我们也可以使用内置函数panic来生成我们自己的异常。它接受一个字符串作为参数,即异常将产生的消息。通常,这条消息比重写代码返回错误要简洁。此外,我们可以在我们自己的包中使用它,以表明开发人员在使用我们的包的代码时可能犯了错误。只要有可能,最佳实践是尝试向我们的包的使用者返回error值。

运行以下代码,查看从另一个函数调用的函数中生成的异常:

package mainfunc main() {foo()
}func foo() {panic("oh no!")
}
Outputpanic: oh no!goroutine 1 [running]:
main.foo(...)/tmp/sandbox494710869/prog.go:8
main.main()/tmp/sandbox494710869/prog.go:4 +0x40

在这里,我们定义了一个函数foo,它使用字符串"oh no!"调用内置的panic。这个函数被我们的main函数调用。注意输出的信息panic: oh no!,堆栈跟踪显示了一个单独的goroutine,其中有两行代码:一行用于main()函数,另一行用于我们的foo()函数。

我们已经看到,异常似乎会在产生它们的地方终止程序。当需要关闭开放资源时,这可能会产生问题。Go提供了一种始终执行某些代码的机制,即使在出现紧急情况时也是如此。

延迟函数

你的程序可能有必须正确清理的资源,即使在运行时处理异常时也是如此。Go允许您推迟函数调用的执行,直到调用它的函数完成执行。延迟函数即使在紧急情况下也会运行,它被用作一种安全机制,以防范紧急情况带来的混乱。通过像往常一样调用函数,然后用defer关键字前缀整个语句来延迟函数,就像defer sayHello()一样。运行下面的例子,看看在发生异常事件时如何打印消息:

package mainimport "fmt"func main() {defer func() {fmt.Println("hello from the deferred function!")}()panic("oh no!")
}
Outputhello from the deferred function!
panic: oh no!goroutine 1 [running]:
main.main()/Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

在这个例子中的main函数中,我们首先defer了对一个匿名函数的调用,该匿名函数会打印出消息"hello from the deferred function!"。然后main函数立即使用panic函数产生一个异常。在这个程序的输出中,我们首先看到deferred函数被执行了,并打印了它的消息。接下来是我们在main中生成的异常。

延迟函数可以防止意外情况的发生。在延迟函数中,Go还为我们提供了使用另一个内置函数阻止异常终止我们的Go程序的机会。

处理异常

异常只有一个恢复机制——内置函数recover。这个函数允许你在异常通过调用栈的过程中拦截它,防止它意外地终止你的程序。它有严格的使用规则,但在生产应用程序中可能是非常宝贵的。

由于它是builtin包的一部分,因此可以在不导入任何其他包的情况下调用recover:

package mainimport ("fmt""log"
)func main() {divideByZero()fmt.Println("we survived dividing by zero!")}func divideByZero() {defer func() {if err := recover(); err != nil {log.Println("panic occurred:", err)}}()fmt.Println(divide(1, 0))
}func divide(a, b int) int {return a / b
}
Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!

在这个例子中,我们的main函数调用了我们定义的函数divideByZero。在这个函数中,我们defer调用一个匿名函数,该函数负责处理在执行divideByZero时可能出现的任何错误。在这个延迟匿名函数中,我们调用recover内置函数,并将它返回的错误赋值给一个变量。如果divideByZero处于异常状态,这个error值将被设置,否则它将是nil。通过比较err变量和nil变量,我们可以检测是否发生了异常,在这种情况下,我们使用log.Println函数记录异常,就像它是其他任何error一样。

在这个延迟匿名函数之后,我们调用另一个我们定义的函数divide,并尝试使用fmt.Println打印它的结果。提供的参数将导致divide执行除数为0的运算,这将产生一个异常。

在这个示例的输出中,我们首先看到恢复panic的匿名函数的日志消息,然后是消息 we survived dividing by zero!。我们确实做到了这一点,感谢内置函数recover阻止了将终止我们的Go程序的灾难性异常。

recover()返回的err值正好是调用panic()时提供的值。因此,当异常没有发生时,确保err值仅为nil至关重要。

recover检测异常

recover函数依赖于错误的值来确定是否发生了严重错误。因为panic函数的参数是一个空接口,所以它可以是任何类型。任何接口类型的0值,包括空接口,都是nil。必须注意避免将nil作为panic的参数,如下例所示:

package mainimport ("fmt""log"
)func main() {divideByZero()fmt.Println("we survived dividing by zero!")}func divideByZero() {defer func() {if err := recover(); err != nil {log.Println("panic occurred:", err)}}()fmt.Println(divide(1, 0))
}func divide(a, b int) int {if b == 0 {panic(nil)}return a / b
}```go```shell
Outputwe survived dividing by zero!

这个例子与前面的例子相同,涉及到recover,只是做了一些细微的修改。divide函数被修改为检查它的除数b是否等于0。如果是,它将使用内置的panic并传入nil参数来生成一个异常。这一次的输出不包括显示发生了严重错误的日志消息,即使divide创建了一个严重错误。这种静默行为就是为什么确保panic内置函数的参数不是nil非常重要的原因。

总结

我们已经看到了Go中创建panic的多种方式,以及如何使用内置的recover恢复它们。虽然您自己可能不需要使用panic,但从panic中正确恢复是使Go应用程序可用于生产的重要步骤。

相关文章:

在Go中处理异常

引言 程序遇到的错误大致分为两类:程序员预料到的错误和程序员没有预料到的错误。我们在前两篇关于[错误处理]的文章中介绍的error接口主要处理我们在编写Go程序时预期的错误。error接口甚至允许我们承认函数调用发生错误的罕见可能性,因此我们可以在这些情况下进行…...

rust 全局变量

文章目录 编译期初始化静态常量静态变量原子类型 运行期初始化lazy_staticBox::leak从函数中返回全局变量 标准库中的 OnceCell 编译期初始化 静态常量 const MAX_ID: usize usize::MAX / 2; fn main() {println!("用户ID允许的最大值是{}",MAX_ID); }关键字是co…...

使用Python的qrcode库生成二维码

使用Python的qrcode库生成二维码 此二维码直接跳转对应的网址。 1、首先安装qrcode包 pip install qrcode2、运行代码 import qrcode# 需要跳转的URL url "https://blog.csdn.net/weixin_45092662?typeblog" img qrcode.make(url) img.save("qrcode.png&…...

MSQL系列(四) Mysql实战-索引分析Explain命令详解

Mysql实战-索引分析Explain命令详解 前面我们讲解了索引的存储结构,我们知道了BTree的索引结构,也了解了索引最左侧匹配原则,到底最左侧匹配原则在我们的项目中有什么用?或者说有什么影响?今天我们来实战操作一下&…...

FPGA软件【紫光】

软件:编程软件。 注册账号需要用到企业邮箱 可以使用【企业微信】的邮箱 注册需要2~3天,会收到激活邮件 授权: 找到笔记本网卡的MAC, 软件授权选择ADS 提交申请后,需要2~3天等待邮件通知。 使用授权: 文…...

饲料化肥经营商城小程序的作用是什么

我国农牧业规模非常高,各种农作物和养殖物种类多,市场呈现大好趋势,随着近些年科学生产养殖逐渐深入到底层,专业的肥料及饲料是不少从业者需要的,无论城市还是农村都有不少经销店。 但在实际经营中,经营商…...

AI系统ChatGPT源码+详细搭建部署教程+支持GPT4.0+支持ai绘画(Midjourney)/支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统AI绘画系统,支持OpenAI GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美,可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署…...

vue项目优雅降级,es6降为es5,适应低版本浏览器渲染

非vue项目 ECMAScript 6(ES6)的发展速度非常之快,但现代浏览器对ES6新特性支持度不高,所以要想在浏览器中直接使用ES6的新特性就得借助别的工具来实现。 Babel是一个广泛使用的转码器,babel可以将ES6代码完美地转换为ES5代码,所…...

运放供电设计

文章目录 运放供电设计如何产生负电压BUCK电路BOOST电路产生负电压FLYBUCK产生负电压 运放供电设计 注:使用0.1u跟10u并联 如何产生负电压 问题:电流小,使用并联方式改善,缺点价格贵,淘宝上买的都是假货ICL7662多是用…...

vue2-org-tree 树型结构的使用

vue2-org-tree 用于创建和显示组织结构树状图,帮助开发者轻松地可视化组织结构,例如公司的层级、部门之间的关系、团队成员等。其主要功能有:自定义节点、可折叠节点、支持拖放、搜索、导航等功能。 这里我们主要使用 vue2-org-tree 进行多次…...

【计算机网络】(面试问题)路由器与交换机的比较

路由器与交换机比较 内容主要参考总结自《计算机网络自顶向下第七版》P315 两者均为存储-转发设备: 路由器: 网络层设备 (检测网络层分组首部) 交换机: 链路层设备 (检测链路层帧的首部) 二者均使用转发表: 路由器: 利用路由算法(路由协议)计算(设置), 依据IP地址 交换机…...

基于下垂控制的孤岛双机并联逆变器环流抑制模型(Simulink仿真实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

第十九章 文件操作

程序运行时产生的数据都属于临时数据&#xff0c;程序一旦运行结束都会被释放 通过文件可以将数据持久化 C中对文件操作需要包含头文件 < fstream > 文件类型分为两种&#xff1a; 文本文件 - 文件以文本的ASCII码形式存储在计算机中 二进制文件 - 文件以文本的二进制…...

防火墙管理工具增强网络防火墙防御

防火墙在网络安全中起着至关重要的作用。现代企业具有多个防火墙&#xff0c;如&#xff1a;电路级防火墙、应用级防火墙和高级下一代防火墙&#xff08;NGFW&#xff09;的复杂网络架构需要自动化防火墙管理和集中式防火墙监控工具来确保边界级别的安全。 网络防火墙安全和日…...

34 机器学习(二):数据准备|knn

文章目录 数据准备数据下载数据切割转换器估计器 kNN正常的流程网格多折交叉训练原理讲解距离度量欧式距离(Euclidean Distance)曼哈顿距离(Manhattan Distance)切比雪夫距离 (Chebyshev Distance)还有一些自定义的距离 就请读者自行研究 再识K-近邻算法API选择n邻居的思辨总结…...

企业工厂车间台式电脑经常有静电导致开不开机,如何彻底解决?

环境: HP 480G7 Win10 专业版 问题描述: 企业工厂车间台式电脑经常有静电导致开不开机,如何彻底解决? 开机电源指示灯闪,显示器黑屏没有画面开不了机,一般是把主机电源断了,把主机盖打开 把内存条拔了之后长按开机按键10秒以上然后插上内存条开机正常 相对与有些岗…...

【数之道 05】走进神经网络模型、机器学习的世界

神经网络 神经网络&#xff08;ANN&#xff09;神经网络基础激活函数 神经网络如何通过训练提高预测准确度逆向参数调整法 &#xff08;BackPropagation&#xff09;梯度下降法链式法则增加一层 b站视频连接 神经网络&#xff08;ANN&#xff09; 最简单的例子&#xff0c;视…...

C现代方法(第7章)笔记——基本类型

文章目录 第7章 基本类型7.1 整数类型7.1.1 C99中的整数类型7.1.2 整型常量7.1.3 C99中的整型常量7.1.4 整数溢出7.1.5 读/写整数 7.2 浮点类型7.2.1 浮点常量7.2.2 读/写浮点数 7.3 字符类型7.3.1 字符操作7.3.2 有符号字符和无符号字符7.3.3 算术类型7.3.4 转义序列7.3.5 字符…...

ON DUPLICATE KEY UPDATE 导致自增ID跳跃式增长

1. 语法 INSERT INTO table_name VALUES(null,param,..) ON DUPLICATE KEY UPDATE param_name VALUES(param_name);2. 介绍 ON DUPLICATE KEY UPDATE 会根据主键或唯一索引检索当前记录是否已经存在&#xff0c;存在更新&#xff0c;不存在插入&#xff1b; 优先级&#xff…...

python学习笔记5-堆

题目链接 heapify(q) 初始化一个列表q成为小根堆这道题取反使之成为大根堆heappop(q) 弹出堆顶heappush(q, e) 将e插入堆中 class Solution:def maxKelements(self, nums: List[int], k: int) -> int:q [-x for x in nums]heapify(q)ans 0for _ in range(k):x heappop(…...

【微服务 SpringCloud】实用篇 · Eureka注册中心

微服务&#xff08;3&#xff09; 文章目录 微服务&#xff08;3&#xff09;1. Eureka的结构和作用2. 搭建eureka-server2.1 创建eureka-server服务2.2 引入eureka依赖2.3 编写启动类2.4 编写配置文件2.5 启动服务 3. 服务注册1&#xff09;引入依赖2&#xff09;配置文件3&am…...

WebSocket学习笔记

一篇文章理解WebSocket原理 1.HTTP协议(半双工通信)&#xff1a; HTTP是客户端向服务器发起请求&#xff0c;服务器返回响应给客户端的一种模式。 特点&#xff1a; 1.只能是客户端向服务器发起请求&#xff0c;是单向的。 2.服务器不能主动发送数据给客户端。 半双工通信…...

centos 内核对应列表 内核升级 linux

近期服务器频繁出现问题&#xff0c;找运维同事排查&#xff0c;说是系统版本和内核版本和官方不一致&#xff0c;如下&#xff1a; Release 用的是7.8, kernal 用的是 5.9 我一查确实如此&#xff1a; 内核&#xff1a; Linux a1messrv1 5.9.8-1.el7.elrepo.x86_64 发行版 Cen…...

如何判断a类b类c类ip地址

在计算机网络中&#xff0c;IP地址用于标识和定位网络上的设备。IP地址根据其范围和结构划分为A类、B类和C类等不同类型。了解如何判断IP地址所属的类型对于理解网络结构和进行网络管理非常重要。虎观代理小二二将介绍如何判断IP地址的类别&#xff0c;以帮助读者更好地理解和应…...

SNAP对Sentinel-1预处理

SNAP对Sentinel-1预处理 一、导入数据 二、轨道校正 点击run开始处理 三、噪声去除 打开S-1 Thermal Noise Removal工具 如果选中了VH&#xff0c;就只会输出一个VH极化结果 四、辐射定标 Run 五、滤波处理 六、地形校正 这边的dem需要自己下载 dem下载地址 如果一格…...

GEE案例——指定区域纯净森林提取分析(红和近红外波段)阈值法提取森林面积

本教程主要是利用影像波段的近红外和红波段的指数作为森林区域的筛选,利用大津法进行指定区域的森林夏季的遥感影像的红波段和近红外波段。 简介: 提取森林范围是遥感影像处理中的一项常见任务。以下是可能用到的一些步骤: 1. 数据预处理:首先,需要进行数据预处理,包括…...

JavaScript从入门到精通系列第二十一篇:JavaScript中的原型对象详解

文章目录 前言 一&#xff1a;原型对象 1&#xff1a;什么是原型对象 2&#xff1a;原型对象的作用 3&#xff1a;通过原型对象实现工厂方法 二&#xff1a;原型对象咋说 1&#xff1a;in和原型对象 2&#xff1a;hasOwnProperty()函数 3&#xff1a;hasOwnProperty()来…...

app.json: [“usingComponents“][“van-icon“]: “@vant/weapp/icon/index“ 未找到

维护一个微信小程序的项目&#xff0c;运行报错如下&#xff1a; app.json: ["usingComponents"]["van-icon"]: "vant/weapp/icon/index" 未找到解决办法 我只说我用到的&#xff0c;如果解决不了你的问题&#xff0c;详细的可以参照官方文档&…...

Kotlin中循环语句

在Kotlin中&#xff0c;循环语句有多种形式&#xff0c;包括while循环、do-while循环、for循环等。下面将逐个说明每种形式的使用。 while循环&#xff1a; var n: Int 5 while (n > 0) {println("n$n")n-- }上述代码中&#xff0c;使用while循环打印n的值&…...

Java String之正则表达式

Java String之正则表达式 导言 最近做项目时&#xff0c;遇到了限制输入字符格式的问题&#xff0c;采用了Java String的正则表达式&#xff0c;下面针对正则表达式使用进行概述 正则表达式 正则表达式类似可以通俗的理解为字符模板&#xff0c;通过符号的方式进行表述&…...

mc做地图画网站/百度竞价优化排名

我们可以把Block当做Objective-C的匿名函数。Block允许开发者在两个对象之间将任意的语句当做数据进行传递&#xff0c;往往这要比引用定义在别处的函数直观。另外&#xff0c;block的实现具有封闭性(closure)&#xff0c;而又能够很容易获取上下文的相关状态信息。Block的创建…...

怎样做网站内链/宁德市人民医院

转&#xff1a;https://blog.csdn.net/u013673437/article/details/80534839 在编写MATLAB程序过程中&#xff0c;有时会遇到当程序运行到不满足if条件时让程序跳出&#xff0c;停止运行的情况&#xff0c;在MATLAB中&#xff0c;使用return语句实现程序跳出。 只将以上程序中变…...

怎么建立免费的网站/专业网站建设

感谢M_Studio的无私分享&#xff0c;下面是他的主页 M_Studio的个人空间_哔哩哔哩_Bilibili Photon&#xff1a; https://www.photonengine.com/zh-CN/Photon 游戏素材&#xff1a; https://assetstore.unity.com/publishers/44925 一、注册创建应用 创建 导入Photon和游…...

WordPress用AFC制作主题/广州网站优化价格

WindowBuilder是可视化Java GUI编程的eclipse插件。有了它的帮助&#xff0c;我们可以通过拖拽来编辑Java程序界面。在最新版的Eclipse中安装最新版插件WindowBuilder&#xff0c;可以有两种方式&#xff1a; 一、直接安装方式&#xff1a; 1.启动Eclipse4.21。选择“帮助”-…...

企业网站推广形式有/太原seo服务

之前写过一篇gridpanel有关动态列的博客&#xff0c;当时只是实验性的写写&#xff0c;实际项目中也没有用&#xff0c;因为是实验性的写&#xff0c;所以对实际项目考虑的问题不是很多&#xff0c;比如&#xff0c;如果是动态列&#xff0c;数据也是动态的&#xff0c;而且可能…...

做网站的公司上海/竞价运营是做什么的

研发过程中&#xff0c;文档很重要&#xff0c;但更重要的可能是「惯性思维」 开发到底要不要写文档&#xff08;注释&#xff09;&#xff0c;要写多少文档&#xff0c;要怎么写文档&#xff0c;想必在大家工作的各个阶段都会有不同的体会&#xff0c;不同人也会有不同的意见。…...