Go语言的100个错误使用场景(11-20)|项目组织和数据类型

前言

大家好,这里是白泽。 《Go语言的100个错误以及如何避免》 是最近朋友推荐我阅读的书籍,我初步浏览之后,大为惊喜。就像这书中第一章的标题说到的:“Go: Simple to learn but hard to master”,整本书通过分析100个错误使用 Go 语言的场景,带你深入理解 Go 语言。

我的愿景是以这套文章,在保持权威性的基础上,脱离对原文的依赖,对这100个场景进行篇幅合适的中文讲解。所涉内容较多,总计约 8w 字,这是该系列的第二篇文章,对应书中第11-20个错误场景。

🌟 当然,如果您是一位 Go 学习的新手,您可以在我开源的学习仓库中,找到针对 《Go 程序设计语言》 英文书籍的配套笔记,期待您的 star。

公众号【白泽talk】,聊天交流群:622383022,原书电子版可以加群获取。

前文链接:

《Go语言的100个错误使用场景(1-10)|代码和项目组织》

2. Code and project organization

🌟 章节概述:

  • 主流的代码组织方式
  • 高效的抽象:接口和范型
  • 构建项目的最佳实践

2.11 没有使用函数式选项模式(#11)

当设计一个 API 的时候,如何处理可选的配置项输入项是一个问题。下面先展示一些不好的例子。假设场景是创建一个 HTTP Server 服务,需要输入 IP 地址,端口等信息,但同时需要提供默认值,当不传入的时候也可以工作。

反例1(直接包含所有,配置参数):

func NewServer(addr string, port int) (*http.Server, error) {// ...
}

直接在参数列表罗列各种参数,然后在内部依次处理,但是使用时必须传入所有参数。

反例2(Config 存放可选参数):

package httplib
​
type Config struct {// 使用引用类型则未传入int则是nil,否则会和0值混淆Port *int
}func NewServer(addr string, cfg Confg) {
}
--------------------------------------------------
func main() {port := 0config := httplib.Config{Port: &port,}httplib.NewServer("localhost", config)
}

这种方式允许用户将可选的配置参数通过 Config 存放,然后在 NewServer 方法的内部读取 Config 结构的字段去初始化,但是有两个问题:

  1. 随着可选参数的增多,NewServer 内的初始化逻辑将无限扩大。
  2. 如果用户 Config 整个都选择默认参数,则必须传入一个空的 Config{},使得用户需要对 Config 的用法提高理解成本。
httplib.NewServer("localhost", httplib.Config{})

反例3(建造者模式):

package httplib
​
type Config struct {Port int
}type ConfigBuilder sruct {port *int
}func (b *ConfigBuilder) Port(port int) *ConfigBuilder {b.port = &portreturn b
}func (b *ConfigBuilder) Build() (Config, error) {cfg := Config{}if b.port == nil {cfg.Port = defaultHTTPPort} else {if *b.port == 0 {cfg.Port = randomPort()} else if *b.port < 0 {return Config{}, errors.New("port should be positive")} else {cfg.Port = *b.port}}reutrn cfg, nil
}func NewServer(addr string, config Config) (http.Server, error) {// ...
}
----------------------------------------------
// 用法
func main() {builder := httplib.ConfigBuilder{}builder.Port(8080)cfg, err := builder.Build()if err != nil {// ...}server, err := httplib.NewServer("localhost", cfg)if err != nil {// ...}
}

这种写法下仍然有两个问题:

  1. 当可选参数希望使用默认值的时候,需要传递 nil(虽然已经比示例2进步了)。
server, err := httplib.NewServer("localhost", nil)
  1. 使用建造者模式链式创建过程中,只允许返回一个参数。一旦过程中发生错误,为了确保链式调用继续,即使需要错误处理也只能内聚的具体一个个方法内部,并不能将 err 传递出来。从而只能在 Builder 方法内验证可能发生的错误,使得 err 的处理成本大大提高。
cfg, err := builder.Foo("foo").Bar("bar").Build()

🌟 推荐方法(函数式配置选项模式),核心思想如下:

  1. 用一个不对外倒出的结构存放配置:options
  2. 每一个 option 是一个函数返回同一个结构:Option

代码展示:

package httplib
​
type options struct {port *int
}type Option func(options *options) errorfunc WithPort(port int) Option {return func(options *options) error {if port < 0 {return errors.New("port should be positive")}options.port = &portreturn nil}
}

WithPort 接受一个 port 参数代表端口号,返回一个给 options 设置端口号的函数。这种形式的函数本质是一个匿名的闭包,持有外部的 options 配置集合。

func NewServer(addr string, opts ...Option) (*http.Server, error) {// 初始化配置集合 optionsvar options optionsfor _, opt := range opts {err := opt(&options)if err != nil {return nil, err}}// 针对配置字段内容,添加验证需要的逻辑var port intif options.port == nil {port = defaultHTTPPort} else {if *options.port == 0 {port = randomPort()} else {port = *options.port}}
}
---------------------------------------------
// 用法
int main() {server, err := httplib.NewServer("localhost", httplib.WithPort(8080), httplib.WithTimeout(time.second))
}

在 NewServer 内通过循环将 Option 的配置通过函数调用应用到 options 集合中,然后在编写针对 options 配置字段的验证逻辑,因为所有可选的 Option 都是外部传入的,NewServer 内需要为其进行二次校验。

🌟 这种方式也是 Go 的地道用法,在很多开源项目如 gRPC 中都大量使用。

2.12 项目缺乏组织(#12)

Go 语言是一个自由的语言,并不强制要求你选择某一种组织项目的模板,但是你需要为此行动。一个常见的模板展示:

/cmd # 主要的源代码位置,foo应用的入口文件位于 /cmd/foo/main.go
/internal # 内部使用的代码,不希望被导出使用
/pkg # 公共的代码,希望被导出
/test # 额外的外部测试代码和测试数据,Go的单测应该和源代码在同一个package内,但是集成测试等代码需要在这个目录
/configs # 配置文件
/docs # 设计文档和用户手册
/examples # 项目的使用示例代码
/api # API 文件,例如 Swagger,PB等
/web # Web应用拥有的资源文件,如静态文件等
/build # 打包和持续集成文件
/scripts # 各种脚本
/vendor # 当前项目的依赖文件

没有 src/ 目录因为它太泛用了,从而将其细分成了上述的各个目录。但这只是一个参考。

package 的组织方式:

/net/httpclient.go.../smtpauth.go...addrselect.go...

这是 Go 标准库中 net 包的组织结构,虽然 /http 位于 /net 之后,但是 net/http 这个包只能访问 net 包中被导出的内容(大写开头),使用子目录这种组织结构是为了使相关功能的包聚集在一起管理。

🌟 选择根据上下文进行组织项目还是分层组织项目都可以,只要你可以确保项目清晰:

  • 按上下文:将代码分成 customer,constract 等等模块。
  • 六边形架构(DDD):按功能进行分层。

🌟 最佳实践:

  1. 避免为时过早的 package 创建,允许演化,而不是一直遵守一开始的强制规划。
  2. 避免产生大量细粒度的 package,只包含个别文件。当然过大也是一个问题。
  3. 包的命名需要根据它提供的功能出发,用一个小写单词表示。
  4. 最小化包需要导出的内容,减少耦合,不确定就先不导出。
  5. 代码库的编码风格一致性。

2.13 创建公共设施包(#13)

一种常见的不好的实践:创建共享的包如 utils,common & base。

代码展示:

package util
​
func NewStringSet(...string) map[string]struct{} {// ...
}func SortStringSet(map[string]struct{}) []string {// ...
}
-----------------------------------------
// 用法
func main() {set := util.NewStringSet("a", "b", "c")fmt.Println(util.SortStringSet(set))
}

工具包内的两个函数实现了创建 string 集合和针对 key 进行排序输出的函数,但是此处包命名为 util 则没有任何意义,完全可以替换成 common,shared…

代替方案:

package stringset
​
type Set map[string]struct{}
func New(...string) Set {...}
func (s Set) Sort() []string {...}
-----------------------------------------
// 用法
set := stringset.New("a", "b", "c")
fmt.Println(set.Sort())

用 stringset 代替 util 这个包的名称,使其更具表达性。同时将方法的前缀去除,用一个结构 Set 去接收 Sort 方法,将所有逻辑内聚在一个用途明确的 stringset 包中。调用侧使用也收到了明确约束,更加方便。

2.14 忽略包名的冲突(#14)

示例代码:

package redis
​
type Client struct {...}func NewClient() *Client {...}func (c *Client) Get(key string) (string, error) {...}
----------------------------------------------------
// 冲突的场景
func main() {redis := redis.NewClient()v, err := redis.Get("Foo")
}

在这种场景下,虽然 redis 变量现在是可以工作的,但它本质是变量,会被修改。但是 redis 包将无法再在代码中访问。

  • 直观的解决方案:
func main() {redisClient := redis.NewClient()v, err := redisClient.Get("Foo")
}

修改变量名称,避免冲突。

  • 更推荐的解决方案:
import redisapi "mylib/redis"func main() {redis := redis.NewClient()v, err := redis.Get("Foo")
}

通过给 import 的 redis 包起别名的方式,避免与变量名的冲突,这是一种更推荐的做法。

📒 Tips:变量名的创建要避免与内置关键字或者函数同名

2.15 代码文档缺失(#15)

文档对于项目的开发者和使用者都十分重要,这里给出几个法则:

  1. 为每一个导出的对象都配备文档(通过注释)
// Customer is a customer representation
type Customer struct// ID returns the customer identifier
func (c Customer) ID() string {...}
  1. 注释需要是一个完整的句子,以.结尾。并且针对于描述对象的功能,而不是如何实现。确保提供足够的描述信息,使得用户无需阅读代码即可使用。
  2. 针对废弃的 API 使用注释:
// ComputePath returns the fastset path between two points
// Deprecated: This function uses a deprecated way to compute
// the fastest Path. Use ComputeFastestPath instead. func ComputePath () {}
func ComputePath() {}
  1. 为 package 添加文档:
// Package math provides basic constants and mathmatical functions
//
// This package ...
package math

第一行需要简洁,因为在文档中会展现:

image-20240130150051245

2.16 不使用 code-linter(#16)

Linter 是一个自动化代码分析工具,可以帮助我们分析代码,找到潜在的错误。所以 Code Linter 也是持续集成中必不可少的一环。

go vet: Go 内置的静态代码检查工具。

go vet ./...

Linters: 可以是外部工具,如 Golint、GolangCI-Lint、Staticcheck 等,它们通过外部安装,并提供更多的规则和功能。

通常情况下,建议同时使用 go vet 和 linters 以确保代码的质量和一致性。

示例代码:

func main() {unusedVariable := 42 // 未使用的变量
}

运行 go vet ./...

baize@baizedeMacBook-Air mistakes % go vet ./...
# mistake
vet: ./main.go:4:2: unusedVariable declared and not used

3. Data types

🌟 章节概述:

  • 基本类型涉及的常见错误
  • 掌握 slice 和 map 的基本概念,避免使用时产生 bug
  • 值的比较

3.1 八进制产生的混乱局面(#17)

在 Go 当中,以0字面量开头的数值表示8进制,因此:

sum := 100 + 010 // 结果为108

但是8进制也有其发挥作用的场景,如赋予文件对应 Linux 系统的权限:

file, err := os.OpenFile("foo", os.O_RDONLY, 0644)
// 0644可以替换成0o644或者0O644

Go 语言中的不同进制表示:

  • 二进制:使用 0b 或者 0B 为前缀
  • 十六进制:使用0x 或者 0X 为前缀
  • 虚数:使用 i 为后缀

Go 语言中支持用下划线作为数值的分隔符,提高可读性:

1_000_000_000 // 一百万
0b00_00_01 // 二进制也是可以的

3.2 忽略整型溢出(#18)

常见的整型类型:

image-20240131112800057

整型溢出场景:

var counter int32 = math.MaxInt32
counter++
fmt.Printf("counter=%d\n", counter)

整个程序编译和运行不会报错,但是:

counter = -2147483648 // counter++ 导致int32溢出

计算机存储有符号整数用二进制表示:

image-20240131114037048

针对有符号整数,第一位为符号位,0表示正数,1表示负数,全0表示0(约定)。

比如上述 int32 有符号整型最大值+1(左侧图片),得到右侧图片(是一个负数)。

计算时使用补码:正数的补码等于原码,负数的补码等于原码除去符号位外,所有位数取反,最后整体+1。

举例 int8 计算 8 + (-8) = 0 用补码的表示:

00001000 + 10001000 // 原码
00001000 + 11111000 = 100000000 // 补码,左侧0溢出,因为只有8位,得到0

如果希望手动检测整型溢出,这是一些模板代码:

func Inc32(counter int32) int32 {if counter == math.MaxInt32 {panic("int32 overflow")}return counter+1
}func addInt(a, b int) int {if a > math.MaxInt-b {panic("int overflow")}return a + b
}func MultiplInt(a, b int) int {if a == 0 || b == 0 {return 0}result := a * bif a == 1 || b == 1 {return result}if a == math.MinInt || b == math.MinInt {panic("integer overflow")}if result/b != a {panic("integer overflow")}return result
}

3.3 不理解浮点数(#19)

浮点数在计算机中的存储通常遵循 IEEE 754 标准,该标准定义了单精度和双精度浮点数的表示方式。

  1. 单精度浮点数(32位):

    • 符号位:1位
    • 指数位:8位
    • 尾数位:23位

    单精度浮点数的存储结构如下:

    SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
    
    • S:符号位,表示正负。
    • E:指数位,以偏移值(127)存储,范围为-126到+127。
    • M:尾数位,23位精度。

    具体数值表示为:(-1)^S * 1.M * 2^(E-127)

  2. 双精度浮点数(64位):

    • 符号位:1位
    • 指数位:11位
    • 尾数位:52位

    双精度浮点数的存储结构如下:

    SEEEEEEE EEEEMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM
    
    • S:符号位,表示正负。
    • E:指数位,以偏移值(1023)存储,范围为-1022到+1023。
    • M:尾数位,52位精度。

    具体数值表示为:(-1)^S * 1.M * 2^(E-1023)

需要注意的是,由于二进制浮点数的特性,某些十进制小数可能无法精确表示,会有舍入误差。在编程中,特别是涉及金融等需要高精度的领域,需要小心处理浮点数的精度问题。

🌟 举例 1.0001 在单精度和双精度下的存储:

  1. 单精度(32位):

    0 01111111 00010010000111101011100
    
    • 符号位:0(正数)
    • 指数位:01111111(127的二进制表示,表示偏移值为0)
    • 尾数位:00010010000111101011100(23位精度)

    具体数值表示为:(-1)^0 * 1.00010010000111101011100 * 2^(0-127) ≈ 1.0001

  2. 双精度(64位):

    0 01111111111 0001001000011110101110000101000111101011100010100011111
    
    • 符号位:0(正数)
    • 指数位:01111111111(1023的二进制表示,表示偏移值为0)
    • 尾数位:0001001000011110101110000101000111101011100010100011111(52位精度)

    具体数值表示为:(-1)^0 * 1.0001001000011110101110000101000111101011100010100011111 * 2^(0-1023) ≈ 1.0001

🌟 浮点数的使用准则:

  1. 比较大小需要在合理精度范围内
  2. 运算时注意相似精度优先运算,减少精度波动

3.4 不理解切片长度和容量(#20)

Go 的切片本质上是一个结构体,包含一个指向数组的指针,以及两个变量记录长度和容量。

🌟 切片创建于扩容场景分析:

s := make([]int, 3, 6) // 创建长度为3容量为6的int类型的切片
s[1] = 1 // 赋值s[1]=1

image-20240131123853071

访问切片大于长度范围的位置将触发 panic:

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

在 len 小于 cap 的时候,可以直接向切片添加元素:

s = append(s, 2)

image-20240131124253144

此时切片的 len 自动增加到4,因为容量足够,不会触发切片的扩容,但如果添加的元素数量超过 cap 的限制,则会触发切片的扩容:

s = append(s, 3, 4, 5)
fmt.Println(s) // 得到:[0 1 0 2 3 4 5]

image-20240131124546956

当添加5的时候,原底层数组的容量达到上限6,触发2倍扩容(创建新数组),拷贝原切片内容到新数组,并追加5。

大致的 Go 切片扩容规则:容量1024以下双倍扩容,以上扩容25%

原底层数组因为丢失了引用,如果在堆内存内,会被后续的 Go GC 回收(GC 相关场景将在全书后期讲解)。

🌟 切片截取场景分析:

s1 := make([]int, 3, 6) // 长度为3,容量为6
s2 := s1[1:3] // 从s1的索引1-3截取,左闭右开,此时s2的长度为2,容量是5

image-20240131125818202

此时如果更新 s1[1] 或者 s2[0] 为1,则由于共享底层数组的原因,导致另一方可以读取到变更内容。

如果追加内容,则底层共享的数组追加2。此时 s2 的 len 变为3,但是 s1 的 len 依旧为3。

image-20240131130436982

并且此时打印两个切片得到的内容如下:

s1 = [0 1 0], s2 = [1 0 2]

如果继续向 s2 追加元素直到 s2 的长度超过容量,则会触发扩容,创建新底层数组(二倍):

s2 = append(s2, 3)
s2 = append(s2, 4)
s2 = append(s2, 5)

image-20240131130649349

小结

已完成全书学习进度20/100,再接再厉。

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

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

相关文章

DevSecOps 参考模型介绍

目录 一、参考模型概述 1.1 概述 二、参考模型分类 2.1 DevOps 组织型模型 2.1.1 DevOps 关键特性 2.1.1.1 模型特性图 2.1.1.2 特性讲解 2.1.1.2.1 自动化 2.1.1.2.2 多边协作 2.1.1.2.3 持续集成 2.1.1.2.4 配置管理 2.1.2 DevOps 生命周期 2.1.2.1 研发过程划分…

leetcode刷题(剑指offer)54.螺旋矩阵

54.螺旋矩阵 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例 2&#xff1a; 输入&#xff1a;ma…

npm 安装时卡在 sill idealTree buildDeps 不动

网上很多博客给出的解决方案是将镜像源修改为淘宝镜像源&#xff0c;如下&#xff1a; 1. 查看当前的镜像网址 npm config get registry 2. 把npm的镜像地址改成taobao的 npm config set registry https://registry.npm.taobao.org3. 检验是否设置成功 npm config get regi…

Android升级版本兼容问题

1、JDK的选择 AndroidJavaAPI and language features supported14 (API 34)17Core libraries13 (API 33)11Core libraries12 (API 32)11Java API11 and lowerAndroid versions https://developer.android.com/build/jdks The following table lists which version of Gradle…

Java基础-集合框架

集合框架&#xff1a; 内存层面可考虑的数据存储容器&#xff1a;数组&#xff0c;集合 数组的特点&#xff1a;长度&#xff0c;存储元素类型确定&#xff0c;既可以放基本数据类型&#xff0c;也可以放引用数据类型 缺点&#xff1a;长度不可变&#xff0c;存储元素特点单…

从零开始 Linux(一):基础介绍与常用指令总结

从零开始 Linux 01. 概念理解 1.1 什么是 Linux&#xff1f; Linux 是一个开源免费的 操作系统&#xff0c;具有很好的稳定性、安全性&#xff0c;且有很强的处理高并发的能力 Linux 的应用场景&#xff1a; 可以在 Linux 下开发项目&#xff0c;比如 JavaEE、大数据、Python…

Arduino 官网上下载和使用开发板

在 Arduino 官网上下载和使用开发板可以按照以下步骤进行&#xff1a; 打开浏览器&#xff0c;访问 Arduino 官网&#xff08;https://www.arduino.cc/&#xff09;。在官网首页&#xff0c;可以看到各种型号的 Arduino 开发板和相关产品。根据自己的需求选 择合适的开发板型号…

3D词云图

工具库 tagcanvas.min.js vue3&#xff08;框架其实无所谓&#xff0c;都可以&#xff09; 实现 <script setup> import { onMounted, ref } from vue; import ./tagcanvas.min.js;const updateFlag ref(false);// 词云图初始化 const initWordCloud () > {let …

RabbitMQ快速实战

目录 什么是消息队列&#xff1f; 消息队列的优势 应用解耦 异步提速 削峰填谷 总结 主流MQ产品特点比较 Rabbitmq快速上手 创建用户admin Exchange和Queue Connection和Channel RabbitMQ中的核心概念总结 什么是消息队列&#xff1f; MQ全称Message Queue&#xf…

Python 开发学习目录

Python 开发教程的概述&#xff1a; 安装 Python&#xff1a;首先&#xff0c;你需要在你的计算机上安装 Python。你可以从 Python 的官方网站上下载并安装 Python。 学习基础语法&#xff1a;学习 Python 的基础语法&#xff0c;包括变量、数据类型、控制结构、函数等。 学习…

关于mysql 条线添加 子查询 包含groupby 导致问题

直接简约明确说明一下这个问题&#xff0c;发生这个问题我也很懵逼。然后各种怀疑自己&#xff0c;最终我猜测了一下可能是这个问题&#xff0c;结果直接再子查询中有嵌套了一个括号解决此问题 之前写的sql 由于公司安全问题不展示&#xff0c;写个伪sql代表一下 selectid from…

Redis - 多集群数据源配置

目录 前言依赖yml配置redis多集群数据源配置类思考 redis工具类 前言 工作时有一个项目配置了多个redis数据源&#xff0c;使用时出现了指定了使用副数据源&#xff0c;数据却依然使用了主数据源的情况。经过排查&#xff0c;发现配置流程较为繁琐易错&#xff0c;此处做一个记…

Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入)

Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入) 反转(转移)控制(IOC Inverse of Control) 控制&#xff1a;对于成员变量赋值的控制权 反转控制&#xff1a;把对于成员变量赋值的控制权&#xff0c;从代码中反转(转移)到Spring⼯⼚和配置⽂件中完成好处&#xff1a;…

七、并发工具(上)

一、自定义线程池 1&#xff09;背景&#xff1a; 在 QPS 量比较高的情况下&#xff0c;我们不可能说所有的访问都创建一个线程执行&#xff0c;这会导致内存占用过高&#xff0c;甚至有可能出现 out of memory另外也要考虑 cpu 核数&#xff0c;如果请求超过了cpu核数&#…

【bitonicSort学习】

bitonicSort学习 什么是Bitonic Sort核心 什么是Bitonic Sort https://zhuanlan.zhihu.com/p/53963918 这个是用来并行排序的一个操作 之前学过一些CPU排序&#xff0c;快排 冒泡 归并啥的&#xff0c;有一些能转成并行&#xff0c;有一些不适合 像快排这种二分策略就可以考虑…

2024美赛数学建模D题思路源码

比赛当天第一时间更新&#xff01; 赛题目的 赛题目的&#xff1a; 问题描述&#xff1a; 解题的关键&#xff1a; 问题一. 问题分析 问题解答 问题二. 问题分析 问题解答 问题三. 问题分析 问题解答 问题四. 问题分析 问题解答 问题五. 问题分析 问题解答

Vue3的自定义指令怎么迁移到nuxt3

一、找到Vue3中指令的源码 const DISTANCE 100; // 距离 const ANIMATIONTIME 500; // 500毫秒 let distance: number | null null,animationtime: number | null null; const map new WeakMap(); const ob new IntersectionObserver((entries) > {for (const entrie…

草图导入3d后模型贴材质的步骤?---模大狮模型网

3D模型在导入草图大师后出现混乱可能有多种原因&#xff0c;以下是一些可能的原因和解决方法&#xff1a; 模型尺寸问题&#xff1a;如果3D模型的尺寸在导入草图大师时与画布尺寸不匹配&#xff0c;可能导致模型混乱。解决方法是在3D建模软件中调整模型的尺寸&#xff0c;使其适…

深入理解 Java 变量类型、声明及应用

Java 变量 变量是用于存储数据值的容器。在 Java 中&#xff0c;有不同类型的变量&#xff0c;例如&#xff1a; String - 存储文本&#xff0c;例如 "你好"。字符串值用双引号引起来。int - 存储整数&#xff08;全数字&#xff09;&#xff0c;没有小数&#xff…

华为手表开发:WATCH 和GT系列,2.生成密钥和证书请求文件,生成签名和配置签名

华为手表开发:WATCH 3 Pro(2)生成密钥和证书请求文件,生成签名和配置签名 初环境与设备生成密钥生成签名初 希望能写一些简单的教程和案例分享给需要的人 鸿蒙可穿戴开发 环境与设备 系统:window 设备:HUAWEI WATCH 3 Pro 开发工具:DevEco Studio 3.1.0.100 外包开发…