Uber/Google Golang编码标准深度分析

良好的代码风格对于开发优秀的产品至关重要,本文通过分析比较三部流传甚广的Golang代码风格指南,介绍了Go代码风格要点,并介绍了通过工具实现代码检查的方式。原文: Mastering Go: In-Depth Analysis of Uber and Google’s Coding Standards[1]

题图来自Unsplash
题图来自Unsplash

在软件开发过程中,遵守代码风格指南和编码标准不仅是为了保持代码在视觉上的一致性,更重要的是为了使代码更易于理解、维护以及避免错误,以简单高效著称的 Golang 也不例外。本文通过深入研究从 Effective Go[2]Google Go Style Guide[3]Uber Go Style Guide[4] 等资料中获得的通用标准和实践,揭示 Go 编程风格指南的精髓,讨论有助于执行标准的工具,强调自动化检查的局限性,并指出开发人员应内化的关键方面。

简化Go:顶级风格指南的启示

三份指南的侧重点各有不同,但都是关于Go编码风格、格式和惯例的。

  • Effective Go 包含所有数据结构的基本使用、初始化、控制结构、并发和错误处理,对初学者更友好。例如,介绍了什么是 init() [5]
  • Uber Go Style Guide 深入探讨了现实中的 Go 编码实践。例如,它建议避免使用 init(),并解释了原因。
  • Google Go Style Guide 是 Uber 指南的升级版,进一步扩展了详细示例和最佳实践。例如,介绍了注释,并在格式、包注释和文档注释部分给出了示例。
关于编码约定的重要启示
命名约定

命名不仅是代码的外观特征,也是任何人阅读代码时的第一行文档,有效的命名可以让代码不言自明。

如果有其他语言的使用经验,你会发现这些指南有着几乎相同的标准,例如变量应该使用有意义的名称,常量应该全部使用大写字母,包名应该简洁明了。不过,有时"经验"恰恰相反,例如,Go语言中的Getter/Setter方法不需要以GetSet开头,而在Java中则需要;在Go语言中,最好不要在包中使用通用名称[6]

Uber 指南中只在包名[7]函数名[8]错误名[9]中提到了命名约定,并且没有提供任何示例,而在 Google 指南的第一章中,命名约定就涵盖了变量名[10]接收器名[11]常量名[12]等命名方法,以及需要避免的重复[13]getter函数[14]

错误处理

Go 采用了独特的错误处理方法,鼓励开发人员检查出现的错误,并通过及时、可预测的方式进行处理。正确的错误处理还包括为错误提供上下文,使调试更加简单。

无论是 Uber 指南还是 Google 指南,关于错误处理[15]的内容都大同小异,包括错误定义、错误返回和处理,以及panic处理。

格式化

清晰一致的格式使代码具有很高的可理解性和可读性。常见的代码格式,如缩进、括号对齐、组合变量定义、行的最大长度等,不仅适用于 Go,也适用于所有语言。Google 指南中的字面量格式[16]函数格式[17]以及条件和循环[18]提供了很好的参考。

数据结构的构建和使用

这三部指南都涵盖了数据结构,如mapsslicesarrayschannels,每种结构都有其特定用途。

  • 不同数据类型的零值。例如, slice 不需要初始化,而是通过声明 var s []int 直接使用,那么 s := []int{} 就是"坏"代码。
  • 不同数据类型的初始化。例如通过 new进行 make、声明 slicemap 的容量,以提高代码效率。在 Uber 指南中的 初始化结构 [19]初始化Maps [20]中,可以找到更多做法。
  • chanfile 等资源的使用和回收。例如,Uber 指南中的 channel大小为一或无 [21]章节会告诉你如何确定 chan 是否需要缓冲区以及设置多少缓冲区。

此外,我们还可以在 Google 指南中了解更多有关 Go 特定数据结构(如接口[22]goroutine、生命周期[23]泛型[24])的代码风格。

测试

Google 指南强调测试的清晰性和可维护性。

  • 为输出和关键功能编写测试。
  • 针对多种场景使用表格驱动测试。
  • Test 开头,对测试功能进行描述性命名。
  • 保持测试简单,避免测试 Go 标准库。
  • 记录复杂的测试逻辑,以便更好理解。

Uber 指南只提到了表格测试模式[25]

并发性

并发是 Go 的固有特性,其主要特点是使用 goroutines 和 channels,使代码高效且并行。不过,需要进行谨慎的同步和通信,从而避免死锁和竞争条件等常见陷阱。

在Uber 指南中,并发相关部分穿插在使用 go.uber.org/atomic[26]避免全局变量[27]不要忘记goroutines[28]接收器和接口[29]channel大小是1或空[30]等小节中。在Google 指南中,并发性在最佳实践章节中有详细介绍。

错误代码示例

请看下面的代码片段,其中充斥着许多常见错误,如无组织导入、命名不当、channel错误、无效代码等。

package main

import (
 "time"

  "fmt"       
 "io/ioutil" 
 "math/rand"
 "os"
)


var globalData int 

const a = 1
const b = 2

type user struct {
 id        int       `json: "id"` // BadSyntax: should be `json:"id"`(no extra space)
 nameStr   string    
 data      *userData 
 LinkedUrl string    
}

type userData struct {
 description string
 detailsID   int
}


func processdata(u *user, params ...string) { 
 if len(params) > 10 { 
  fmt.Println("Too many parameters")
  return
 }

 
 file, err := os.Open("data.txt")
 if err != nil {
  fmt.Println(err) 
  return
 }
 defer file.Close() 

 content, _ := ioutil.ReadAll(file) 
 fmt.Println("File content:"string(content))

 src := rand.NewSource(time.Now().UnixNano())
 rnd := rand.New(src)
 fmt.Println("Random number:", rnd.Intn(100))

 go func() {
  fmt.Println("Asynchronous operation")
  // Assume more complex logic...
 }()

 if u.id > 100 {
  fmt.Println("ID is high")
 } else {
  fmt.Println("ID is normal")
 }

 u.data = &userData{description: "", detailsID: 1}

 dataSlice := []int{} 
 for i := 0; i < 100; i++ {
  dataSlice = append(dataSlice, i)
 }

 ch := make(chan int10)
 ch <- 1 
 
 if u.nameStr == "" {
  fmt.Println("Name is empty")
 } else {
  fmt.Println("Name is not empty")
 }
 unreachableCode()

 if err := doSomething(12""3""4""5); err != nil {
  panic(err) 
 }
}

func unreachableCode() {
 return
 fmt.Println("This will never be called")
}

func uncalledFunc() {
 return
}

func doSomething(p1 int, p2 int, p3 string, p4 int, p5 string, p6 int, p7 string, p8 int) error {
 return nil
}

func main() {
 u := &user{id: 1, nameStr: "John Doe"}
 processdata(u, "param1""param2""param3")
}

你可以发现多少问题?可以在Github上找到我的答案[31]

工具化的合规之路
Go 工具

gofmtgovetgolint 的正式设计目的是促进 Go 代码的合规性。

gofmt[32] 主要用于格式化,确保代码遵循标准格式约定,例如:

  • 一致的缩进和间距:与推荐的代码结构和可读性做法保持一致。
  • 正确的换行和括号位置:遵守控制结构和复合类型的惯例。
  • 有组织的导入:与建议的导入语句分组和排序保持一致。

govet[33] 会检查代码是否存在潜在错误,如无法实现的代码或有问题的类型断言,并与风格指南的健壮性和可维护性目标保持一致。

  • 错误处理:检测无法访问的代码或可能被绕过的检查。
  • 并发:识别goroutine和channel使用中的常见错误。
  • 代码正确性:标记可疑结构,如格式字符串不正确的 Printf 调用。
  • 变量声明:警告变量可能无意中被覆盖。

golint[34] 侧重于风格,标记不理想的代码模式或偏离Go风格的代码,特别是处理以下问题:

  • 命名约定:确保变量、常量、函数和其他标识符按照 Go 的大小写敏感规则正确命名。
  • 注释格式化:检查导出类型、函数和方法的注释是否格式正确、位置恰当。
  • 导出实体:验证导出的函数、变量和类型是否有正确的文档记录。
  • 代码简化:标记可简化的不必要的复杂结构。
golangci-lint

golangci-lint 包含 golint,并在社区支持下引入了更多扩展,解决了 Effective Go、Uber 和 Google 指南中强调的各种问题。

  • 错误处理:确保正确检查和处理错误。
  • 代码复杂性:标记过于复杂的函数,提高可读性和可维护性。
  • 并发问题:检测并发原语的竞争条件和不当使用。
  • 性能优化:识别低效代码模式,加以改进以提高性能。
  • 编码风格:执行命名约定和其他与风格相关的准则,与惯用的 Go 实践保持一致。
实践

现在我们尝试使用工具来检查"错误代码示例"。

首先使用 gofmtgovetgolint,分别运行以下脚本。

#!/bin/bash

echo "Running gofmt..."
# List & Write formatting differes and results to stdout
gofmt -l -w .

echo "Running go vet..."
go vet ./...

echo "Running golint..."
# Show as many warnings as possible (default threshold min_confidence=0.8)
golint -min_confidence=0.1 ./...
alt

gofmt 没有输出。原因是我们在使用集成开发环境(IDE)时没有额外添加格式化功能,例如,当我使用 VSCode 和 Golang 扩展时,一些 gofmt 功能(如缩进和导入排序)会默认提供,而下面的导入问题超出了 gofmt 的能力范围。

import (
 "time"
 // 额外的空行,gofmt无法解决
 "fmt"       // BadImportOrdering: "fmt"应该与其他标准库导入分组
 "io/ioutil" // 弃用api: io/ioutil自Go 1.19起已弃用
 "math/rand"
 "os"
)

govet 只能发现两个问题,即 JSON 标记语法和无法访问的代码。

golint 还发现了两个小的编码规范问题,在将阈值调整到最低后,软件包注释和命令都不见了。

golangci-lint 性能怎么样?

首先,我们配置一下 golangci-lint 的执行,有两种方法:一种是通过命令行启用参数 --enable-all ,然后执行以下命令将所有警告和错误信息导入 issues.txt 文件。

golangci-lint run --enable-all --out-format=json ./... | jq 'del(.Report)' > issues.txt

另一种方法是配置 .golangci.yml 文件以启用所有检查,然后执行 golangci-lint run --out-format=json ./...| jq 'del(.Report)' > issues.txt

run:
  timeout: 5m
  modules-download-mode: readonly

linters:
  enable-all: true

issues:
  exclude-use-default: false
  max-issues-per-linter: 0
  max-same-issues: 0

太棒了!golangci-lint 返回了 37 个问题,例如无效代码,如 struct 中的 globalData、全局常量 a、b 和函数 uncalledFuncerrorHandling里的未处理错误;格式化问题,如注释中缺少句号和结尾的空白;API 使用问题,如使用 math/rand 而非 crypto/rand;不可调用代码,如 unreachableCode 方法中的代码;命名问题,如 LinkedUrl 等。如果感兴趣,请查看完整问题列表[35]

超越工具
工具有局限性

golangci-lint 在查找问题方面表现出色,但仍有局限性,更不用说其他 Go 工具了。

例如,在上面的示例中,doSomething 方法传递了 8 个参数,但却没有检测到过长的参数列表,这无疑违反了代码约定。

此外,在第 72 行中,代码使用 []int{} 来初始化切片,根据 Uber 和 Google 指南,应该避免使用这种方法,因为 nil 是有效切片[36],我们应该在声明后直接使用切片。

工具无法做到的
  • 命名的上下文:工具无法判断名称是否反映了变量的目的,选择有意义的名称取决于开发者。
  • 正确处理错误:虽然工具可以捕捉被忽略的错误,但提供适当的上下文和优雅的处理错误是开发人员的责任。
  • 优化数据结构:了解复杂性和选择正确的数据结构超出了自动化工具的范畴。
  • 并发模式:要正确实现并发模式、避免死锁并确保 goroutines 之间的高效通信,就必须深入了解 Go 的并发模型。
  • 设计选择:何时使用接口、指针或特定数据结构等决定取决于开发人员的判断。
总结

虽然 gofmtgo vetgolintgolangci-lint 是维护干净、可读性和标准代码库不可或缺的工具,但对 Go 最佳实践和常见陷阱的细致入微的理解才是精通 Go 的开发人员的与众不同之处。优秀的开发者会利用工具提高工作效率,但依靠自己的判断力和知识实现卓越。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

参考资料
[1]

Mastering Go: In-Depth Analysis of Uber and Google’s Coding Standards: https://laiyuanyuan-sg.medium.com/mastering-go-in-depth-analysis-of-uber-and-googles-coding-standards-3b3fb9391ee3

[2]

Effective Go: https://go.dev/doc/effective_go

[3]

Google Go Style Guide: https://google.github.io/styleguide/go/decisions

[4]

Uber Go Style Guide: https://github.com/uber-go/guide/blob/master/style.md

[5]

Effective Go: init: https://go.dev/doc/effective_go#init

[6]

Avoid package names like base util or common: https://dave.cheney.net/2019/01/08/avoid-package-names-like-base-util-or-common

[7]

Uber Go Sytle Guide: package name: https://github.com/uber-go/guide/blob/master/style.md#package-names

[8]

Uber Go Style Guide: function name: https://github.com/uber-go/guide/blob/master/style.md#function-names

[9]

Uber Go Style Guide: error naming: https://github.com/uber-go/guide/blob/master/style.md#error-naming

[10]

Google Go Style Guide: variable names: https://google.github.io/styleguide/go/decisions#variable-names

[11]

Google Go Style Guide: receiver names: https://google.github.io/styleguide/go/decisions#receiver-names

[12]

Google Go Style Guide: constant names: https://google.github.io/styleguide/go/decisions#constant-names

[13]

Google Go Style Guide: variable names: https://google.github.io/styleguide/go/decisions#variable-names

[14]

Google Go Style Guide: getters: https://google.github.io/styleguide/go/decisions#getters

[15]

Uber Go Style Guide: errors: https://github.com/uber-go/guide/blob/master/style.md#errors

[16]

Google Go Style Guide: literal formatting: https://google.github.io/styleguide/go/decisions#literal-formatting

[17]

Google Go Style Guide: func formatting: https://google.github.io/styleguide/go/decisions#func-formatting

[18]

Google Go Style Guide: conditionals and loops: https://google.github.io/styleguide/go/decisions#conditionals-and-loops

[19]

Uber Go Style Guide: initializing structs: https://github.com/uber-go/guide/blob/master/style.md#initializing-structs

[20]

Uber Go Style Guide: initializing maps: https://github.com/uber-go/guide/blob/master/style.md#initializing-maps

[21]

Uber Go Style Guide: channel size is one or none: https://github.com/uber-go/guide/blob/master/style.md#channel-size-is-one-or-none

[22]

Google Go Style Guide: interfaces: https://google.github.io/styleguide/go/decisions#interfaces

[23]

Google Go Style Guide: goroutine lifetimes: https://google.github.io/styleguide/go/decisions#goroutine-lifetimes

[24]

Google Go Style Guide: generics: https://google.github.io/styleguide/go/decisions#generics

[25]

Uber Go Style Guide: test tables: https://github.com/uber-go/guide/blob/master/style.md#test-tables

[26]

Uber Go Style Guide: use go.uber.org/atomic: https://github.com/uber-go/guide/blob/master/style.md#use-gouberorgatomic

[27]

Uber Go Style Guide: Avoid Mutable Globals: https://github.com/uber-go/guide/blob/master/style.md#avoid-mutable-globals

[28]

Don't fire-and-forget goroutines: https://github.com/uber-go/guide/blob/master/style.md#dont-fire-and-forget-goroutines

[29]

Receiver and Interfaces: https://github.com/uber-go/guide/blob/master/style.md#receivers-and-interfaces,

[30]

Channel Size is One or None: https://github.com/uber-go/guide/blob/master/style.md#channel-size-is-one-or-none,

[31]

Bad Go code example: https://gist.github.com/slaise/c473bb6ca996d5f8a627c1bdafc27fb0

[32]

gofmt: https://pkg.go.dev/cmd/gofmt

[33]

govet: https://pkg.go.dev/github.com/golangci/govet

[34]

golint: https://pkg.go.dev/golang.org/x/lint/golint

[35]

错误代码示例问题列表: https://github.com/slaise/goguides/blob/main/issues.txt

[36]

Uber Go Style Guide: nil is a valid slice: https://github.com/uber-go/guide/blob/master/style.md#nil-is-a-valid-slice

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/731810.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++矢量运算与java矢量运算

矢量运算 概述&#xff1a; 矢量运算是一种基于向量的数学运算&#xff0c;它遵循特定的法则。以下是矢量运算的一些基本原理&#xff1a; 矢量加法&#xff1a;可以使用平行四边形法则或三角形法则来执行。当两个矢量相加时&#xff0c;可以将它们的起点放在同一个点上&…

RabbitMQ篇

1.初始MQ 1.1. 同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&#xff0c;…

初阶数据结构:排序(学习笔记)

目录 1. 各种排序算法的分类2. 插入排序2.1 直接插入排序2.2 希尔排序 3. 选择排序3.1 选择排序3.2 堆排序4. 交换排序4.1 冒泡排序4.2 快速排序4.2.1 霍尔法&#xff08;hoare&#xff09;4.2.2 挖坑法&#xff08;hole&#xff09;4.4.3 前后指针法4.4.4 补充&#xff1a;非递…

存货计价方式 比较-移动平均和批次计价

SAP常用的存货计价方式有 标准价格移动平均价格批次计价 标准价格常用于制造企业&#xff0c;今天的方案比较主要集中在销售型企业常用的移动平均价和批次计价 批次计价&#xff1a; 移动平均&#xff1a; 两种计价方式的Pros&Cons 比较 批次计价 移动平均优点 1…

超好用的一键生成原创文案方法

在现代社会中&#xff0c;原创文案不管是在营销中&#xff0c;还是在品牌推广中都起着至关重要的作用。然而&#xff0c;对于许多人来说&#xff0c;创作出令人印象深刻且引人注目的原创文案并不容易。但随着技术的发展&#xff0c;我们现在可以利用一键生成原创文案的方法来帮…

黑马java-JavaSE进阶-java高级技术

1.单元测试 就是针对最小的功能单元方法&#xff0c;编写测试代码对其进行正确性测试 2.Junit单元测试框架 可以用来对方法进行测试&#xff0c;它是第三方公司开源出来的 优点&#xff1a; 可以灵活的编写测试代码&#xff0c;可以针对某个方法执行测试&#xff0c;也支持一键…

基于springboot的水果购物商城管理系统(程序+文档+数据库)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

Pinctrl子系统_04_Pinctrl子系统主要数据结构

引言 本节说明Pinctrl子系统中主要的数据结构&#xff0c;对这些数据结构有所了解&#xff0c;也就是对Pinctrl子系统有所了解了。 前面说过&#xff0c;要使用Pinctrl子系统&#xff0c;就需要去配置设备树。 以内核面向对象的思想&#xff0c;设备树可以分为两部分&#x…

rabbitmq3

指定通过通道将消息发送给哪个消息队列 同一个通道可以向不同的队列发送消息的&#xff0c;如果你绑定的队列和发布消息的队列不一致也是可以的&#xff0c;这个才是真正的发布消息去具体的某一个队列&#xff1a; 如果队列没有持久化&#xff0c;就不会把这个消息队列保存在磁…

stm32学习记录-5.2PWM输出控制sg90舵机角度

源码连接&#xff1a;https://gitee.com/HL12334/stm32-learning-code 前提知识&#xff1a; 1.定时器中断 1.关键概念 1.1pwm输出 1.常用术语 OC&#xff08;output compare&#xff09;输出比较CNT&#xff08;counter&#xff09;&#xff0c;定时器中用于计数的寄存器…

时序报告Report_timing_summary之一步精通配置选项使用

目录 一、前言 二、配置选项概览图 三、配置选项 3.1 Options 3.1.1 report 3.1.2 path limits 3.1.3 path display 3.2 Advanced 3.2.1 report 3.2.3 miscellaneous 3.3 Timer Settings 3.4 共有部分 四、工程示例 4.1 工程设计代码 4.2 约束文件 4.3 Option…

Linux系统架构----nginx的访问控制

nginx的访问控制 一、nginx基于授权的访问控制概述 Nginx与Apache一样&#xff0c;可以实现基于用户权限的访问控制&#xff0c;当客户端想要访问相应的网站或者目录时&#xff0c;要求用户输入用户名和密码&#xff0c;才能正常访问配置步骤生成用户密码认证文件 &#xff1…

qt带后缀单位的QLineEdit

QLineEditUnit.h #pragma once #include <QLineEdit> #include <QPushButton>class QLineEditUnit : public QLineEdit {Q_OBJECT public:QLineEditUnit(QWidget* parent Q_NULLPTR);~QLineEditUnit();//获取编辑框单位QString UnitText()const;//设置编辑框单位…

STM32的启动流程分析 和 一些底层控制的原理

阅读引言&#xff1a; 阅读本文之后&#xff0c; 你将对单片机&#xff0c; 甚至是嵌入式系统&#xff0c; 或者是传统的PC机系统的启动流程有一个大致的了解&#xff0c; 本文更加偏向于单片机的启动流程分析。 目录 一、基础知识 1.STM32系列的微控制器&#xff08;mcu&…

自研cloud框架专题–web模块(三)

项目特点一:框架集成 1.引入核心依赖2.配置相关功能 二:功能介绍 1.swagger支持并提供swagger快速配置2.knife增强swagger支持3.全局请求参数校验(Validation)支持4.字段脱敏支持5.默认jackson序列化6.xss,cors支持7.访问日志支持8.全局异常处理,统一返回结果9.系统关键及常用信…

数据结构之deque双端队列

一、概念&#xff1a; 众所周知&#xff0c;数据结构是用来存储数据&#xff0c;deque也不例外&#xff0c;他是集结了队列和栈的性质而成的结构&#xff0c;他几乎拥有所有数据结构能有的操作&#xff0c;看似已经大杀四方&#xff0c;可实际情况如何呢&#xff0c;那就带者这…

Day 8.TCP包头和HTTP

TCP包头 1.序号&#xff1a;发送端发送数据包的编号 2.确认号&#xff1a;已经确认接收到的数据的编号&#xff08;只有当ACK为1时、确认号才有用&#xff09;&#xff1b; TCP为什么安全可靠 1.在通信前建立三次握手 SYP SYPACK ACK 2.在通信过程中通过序列号和确认号和…

前端javascript的DOM对象操作技巧,全场景解析

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属的专栏&#xff1a;前端泛海 景天的主页&#xff1a;景天科技苑 文章目录 1.js的DOM介绍2.节点元素层级关系3.通过js修改&#xff0c;清空节点…

【棘手问题】Spring JPA一级缓存导致获取不到数据库表中的最新数据,对象地址不发生改变

【棘手问题】Spring JPA一级缓存导致获取不到数据库表中的最新数据&#xff0c;对象地址不发生改变 一、问题背景二、解决步骤2.1 debug2.2 原因分析2.2.1 数据步骤2.2.2 大模型解释2.2.3 解释举例2.2.4 关键函数 2.3 解决方案 三、Spring JPA一级缓存 一、问题背景 项目的数据…

STM32 通过Modbus协议更改内部Flash(模仿EEPROM)的运行参数

main.c测试 uint8_t uart1RxBuf[64]{0};uint8_t Adc1ConvEnd0; uint8_t Adc2ConvEnd0;int main(void) {/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initial…