Go-知识泛型

Go-知识泛型

  • 1. 认识泛型
    • 1.1 不使用泛型
    • 1.2 使用泛型
  • 2. 泛型的特点
    • 2.1 函数泛化
    • 2.2 类型泛化
  • 3. 类型约束
    • 3.1 类型集合
    • 3.2 interface 类型集合
      • 3.2.1 内置interface类型集合
      • 3.2.2 自定义interface类型集合
        • 3.2.2.1 任意类型元素
        • 3.2.2.2 近似类型元素
        • 3.2.2.3 联合类型元素
      • 3.2.3 interface类型集合运算
      • 3.2.4 基于操作的类型集合
  • 4. 小例子
    • 4.1 map.Keys 获取map的全部key
    • 4.2 Set
    • 4.3 排序 Sort
  • 5. 总结

泛型是程序设计语言的一种风格或范式,允许程序员在编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
Java,C++等多种编程语言都支持泛型,Go语言从1.18版本起也开始支持泛型。

1. 认识泛型

1.1 不使用泛型

实现两个函数对map的value值进行累加,一个是int64类型的值,一个是float64类型的值

func SumInt64(m map[string]int64) int64 {
	var sum int64
	for _, v := range m {
		sum += v
	}
	return sum
}

func SumFloat64(m map[string]float64) float64 {
	var sum float64
	for _, v := range m {
		sum += v
	}
	return sum
}

接着使用两个函数

func TestSum(t *testing.T) {
	ints := map[string]int64{
		"one":   234,
		"two":   6755,
		"three": 78675,
	}
	floats := map[string]float64{
		"one":   123.456,
		"two":   7865.9658,
		"three": 87906.865,
	}
	t.Logf("sum ints : %v , floats : %v", SumInt64(ints), SumFloat64(floats))
}

执行如下:
在这里插入图片描述

实现同样的功能,就因为处理的数据类型不一样,就需要为每种类型编写类似的重复代码。

1.2 使用泛型

泛型函数,就是吧函数的参数和返回值“泛化”,使逻辑通用。 通用并不是对所有类型都使用,所以在声明泛型函数时,需要声明适用的“参数类型”(类型约束).

func SumValue[K comparable, V int64 | float64](m map[K]V) V {
	var sum V
	for _, v := range m {
		sum += v
	}
	return sum
}

SumValue泛型函数通过[K comparable, V int64|fload64]声明了两个类型参数K,V,供函数参数和返回值使用。
类型参数K的类型必须为comparable类型,因为被用作map的key值,在Go语言中map的key值必须是可比较的类型。
类型参数V的类型可以是int64或float64,在声明时使用|组合支持的类型。
普通参数m 表示一个泛化的map,相应的返回值也是一个泛化的类型。
使用:

func TestSumValue(t *testing.T) {
	ints := map[string]int64{
		"one":   234,
		"two":   6755,
		"three": 78675,
	}
	floats := map[string]float64{
		"one":   123.456,
		"two":   7865.9658,
		"three": 87906.865,
	}
	t.Logf("sum ints : %v , floats : %v", SumValue(ints), SumValue[string, float64](floats))
}

执行结果
在这里插入图片描述

在调用泛型函数的地方,编译器会将泛型函数实例化,即使用真实的类型来替换类型参数,在调用时有两种方式:

  • 隐式调用: 调用泛型函数时使用缺省类型参数,让编译器根据实际的参数进行推导。(SumValue(ints))
  • 显示调用: 调用泛型函数时显式的指明类型参数。(SumValuestring, float64)

需要注意的是,编译器之所以能够推导出参数类型是因为函数存在入参,编译器通过传入的变量和函数的参数声明可以推导出参数类型,但对于没有函数参数的泛型函数来说,编译器
无法进行推导,也就无法实例化泛型函数,此时必须显式地调用并指定类型参数。
比如:
在这里插入图片描述

就无法推导出来返回值的类型了
而且显式调用,必须全部指定,不能指定部分类型
在这里插入图片描述

2. 泛型的特点

泛型的英文表述为generic,即一般化,泛化,具体来讲就是函数和类型的泛化。在泛型被引入之前一个函数所能接收的参数类型及
所能处理的数据类型是确定的,但泛型函数却能接受和处理多种类型,对于类型也是同样的道理。
泛型主要包含三方面的内容:

  1. 函数的泛化
  2. 类型的泛化
  3. 接口的扩展

2.1 函数泛化

为了支持泛型,Go语言函数扩展为可以接受一个使用方括弧表示的类型参数;
func SumValue1[K comparable, V int64 | float64, T int64 | float64](m map[K]V) T
同普通的函数参数类似,类型参数中的每个参数也有一个类型,比如参数K,V的类型分别为 comparable(伴随泛型而引入的内置interface类型,表示可比类型)
和int64|float64(表示int64或float64)。
函数中的类型参数是可选的,没有类型参数的函数是传统的函数,带有类型参数的函数则是泛型函数。
即便Go 1.18 引入了泛型并且扩展了函数,但仍然保持兼容(在这里小小的谴责一下 python 和 scala )。
类型参数中的类型正式的名称是类型约束,用来约束类型的范围。 上面额函数可以接受多种类型的map为参数,考虑到所有的map的key的类型都是comparable类型,
那么只要一个map的值类型是int64或float64就能调用泛型函数。
比如: map[int]int64,map[int64]int64,map[float64]float64,...

2.2 类型泛化

泛型同样扩展了类型的表示方法,允许在创建自定义类型时也能接受一个使用方括弧表示的类型参数。
type arr[T int|int64] []T
声明arr类型,可以容纳int或者int64的切片。
这种声明中带有类型参数的类型被称为泛型类型。
泛型类型必须通过类型参数实例化后才可以使用。
var arrInt arr[int]
var arrInt64 arr[int64]
但是当实例化允许范围之外的类型时,会编译异常
在这里插入图片描述

在实例化一个泛型类型时,必须指定类型参数(编译器无法自动推导)

泛型类型同普通类型一样,同样允许定义方法,但是其类型必须带上类型参数:

func (a *arr[T]) add(x T) {
	*a = append(*a, x)
}
func TestArrAdd(t *testing.T) {
	var a arr[int]
	a.add(3)
	a.add(4)
	t.Logf("res : %v", a)
}

执行结果
在这里插入图片描述

未泛型类型定义方法时,必须指定类型参数,但参数名可以与泛型类型声明不同。

func (a *arr[X]) add1(x X) {
	*a = append(*a, x)
}

定义时使用T,但是在使用的时候,可以与声明时的名字不同。
如果方法体中并未使用类型参数,甚至可以使用_省略

func (a *arr[_]) add2(x _) {
	*a = append(*a, x)
}

但是不管是换个名字还是使用_并没有任何好处,反而降低了可读性。

3. 类型约束

无论函数和类型如何泛化,都需要类型参数来限定其泛化的范围,类型参数使用类型的集合表示范围。

3.1 类型集合

func SumValue1[K comparable, V int64 | float64, T int64 | float64](m map[K]V) T
该函数的类型参数中K的类型限定为comparable,V 的类型限定为int64或float64。
comparable是interface类型,int64|float64是组合类型,都代表一个类型集合,用于约束泛化的范围。
使用|来组合多个类型,从而形成一个类型集合。

3.2 interface 类型集合

在泛型特性被引入之前,interface仅表示一个方法集合,实现了该方法结合的所有类型都可认为实现了这个interface。
在泛型的设计中,对interface进行了扩展,interface将不在仅仅表示方法集合,它还可用于表示类型集合,同理,集合内所有类型都可认为实现了这个interface。

3.2.1 内置interface类型集合

comparable就是跟随泛型被引入的内置interface类型
在这里插入图片描述

除了comparable还有any。
comparable表示可比较类型的集合,仅能用于类型参数中。
any不仅在类型参数中表示任意类型的集合,还可以在非泛型场景中作为interface{}的别名使用。

3.2.2 自定义interface类型集合

除了内置的comparable和any两种类型可作为类型约束使用,用户还可以使用interface来定义类型集合。
在泛型之前,interface类型中仅允许包含方法或内嵌interface两种元素,引入泛型后,interface类型将允许使用另外三种元素以表示一个类型集合:

  1. 任意类型元素(如 int)
  2. 近似类型元素(使用表示法,如int)
  3. 联合类型元素(使用|表示法,如int|int64)

需要注意的是,如果interface类型中使用了这三种元素的任意一种,那么这个interface只能用于泛型的类型参数

3.2.2.1 任意类型元素

任意类型(包含interface类型)都可以出现在一个新的interface类型中,用于表示一个仅用于类型参数的集合

type Mint interface {
	int
}

func addMint[M Mint](m1, m2 M) M {
	return m1 + m2
}
func TestMint(t *testing.T) {
	t.Log(addMint(3, 4))
}

在这里插入图片描述

此时该interface表示的数据集仅包含一种类型,且仅能用于泛型场景中的类型参数中。
interface类型和自定义类型都可以出现在interface中从而表示一个类型集合。
可以定义新的泛型interface,不能使用泛型interface定义interface 方法集合
interface泛型用于interface
在这里插入图片描述

但是不能将interface泛型用于interface方法集合
在这里插入图片描述

但是如果显式的声明了泛型,那么就可以使用
在这里插入图片描述

并且该interface的方法集合也能像之前一样实现
在这里插入图片描述

因为在定义interface泛型的时候,限定是int,所以只有int类型才算是实现了方法
在这里插入图片描述

这样来看,使用interface泛型类型,可以限定什么样的方法算是实现。

如果将float64加入到interface泛型中,那么float64的方法也算是实现
在这里插入图片描述

3.2.2.2 近似类型元素

在使用 interface声明类型集合时,可以使用~<type>来制定一组类型,只要其底层类型为同一类型即包含在这个集合中。
因为在Go中,可以通过type取别名,而泛型又时通用这个含义。
比如创建一个string的泛型函数,但是因为使用了type对string取了别名,结果别名类型就无法使用泛型函数。
近似类型元素就是可以让type取了别名的类型也能使用
不使用近似类型
在这里插入图片描述

使用近似类型
在这里插入图片描述

只要底层类型相同,就能使用泛型函数

需要注意的是,~之后的类型必须是某个底层类型,换句话说,一个类型的底层类型不是自身就不能使用~
在这里插入图片描述

另外,interface 自身也不能用于定义近似类型集合,因为interface的底层类型并不确定。

3.2.2.3 联合类型元素

前面使用~定义的元素集合仅能包括一组底层类型一致的类型,又是可能需要联合多种类型,甚至联合多种底层类型一致的类型,此时可以用
|定义一个更宽泛的类型集合

type MInteger interface {
	int | int8 | int16 | int32 | int64
}

但是上述定义仅能支持底层类型,不支持别名
更进一步,可以把所有底层类型也包含进来

type MAnyInteger interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

这样即使是别名类型,也能支持。

3.2.3 interface类型集合运算

前面使用interface声明类型集合时,均使用一行代码制定一个集合(一个子集),事实上interface支持按行制定多个自己和,这些自己和取交集形成最终的集合

type NewString interface {
	~string
	string
}

NewString的类型集合由两个子集组成,一个是所有底层类型为string的集合,另一个是string单一类型,两个子集取交集,最终的类型集合将只包含string单一类型
在这里插入图片描述

3.2.4 基于操作的类型集合

假设顶一个泛型函数来比较元素大小

type Ordered interface {
	~int|~int8|~int16|~int32|~int64|
		~uint|~uint8|~uint16|~uint32|~uint64|
		~float32|~float64|
		~string
}

func Equals[T Ordered](a, b T) bool {
	if a == b {
		return true
	}
	return false
}

定义的Ordered泛型类型是全部可以使用==比较的底层类型,并且包含别名类型,泛型函数限定了Ordered泛型类型。
copmarable和Ordered类似,范围更大。

4. 小例子

4.1 map.Keys 获取map的全部key

将map中的所有key取出来,然后存入切片中返回

func Keys[K comparable, V any](m map[K]V) []K {
	res := make([]K, 0, len(m))
	for k, _ := range m {
		res = append(res, k)
	}
	return res
}

类型参数K被用于声明了一个泛型的切片,然后把遍历到的key添加到切片中并返回。
任意的map都能使用Keys泛型函数

func TestKeys(t *testing.T) {
	t.Log(Keys(map[string]struct{}{
		"one":   {},
		"tow":   {},
		"three": {},
	}))
	t.Log(Keys(map[int]int{
		1: 1,
		2: 2,
		3: 3,
	}))
}

在这里插入图片描述

4.2 Set

Set可以存储一组不重复的数据,广泛用于需要去重的场景。很多编程语言比如Java,C++都提供了相应的实现,但是在Go语言中并没有Set类型。
有了泛型可以自己实现了

// 定义 Set
type Set[T comparable] map[T]struct{}

// 创建 Set
func MakeSet[T comparable]() Set[T] {
	return make(Set[T])
}

// 添加元素
func (s Set[T]) Add(k T) {
	s[k] = struct{}{}
}

// 删除元素
func (s Set[T]) Delete(k T) {
	delete(s, k)
}

// 判断是否包含
func (s Set[T]) Contains(k T) bool {
	_, ok := s[k]
	return ok
}

// 返回长度
func (s Set[T]) Len() int {
	return len(s)
}

// 遍历
func (s Set[T]) Iterate(f func(T)) {
	for k := range s {
		f(k)
	}
}

func TestSet(t *testing.T) {
	set := MakeSet[int]()
	set.Add(1)
	set.Add(2)
	set.Add(1)
	set.Add(3)
	t.Log(set.Len()) // 预期 3
	t.Log(set.Contains(2)) // 预期 true
	set.Delete(1) 
	t.Log(set.Len()) // 预期 2
	t.Log(set.Contains(1)) // 预期 false
	sum := 0
	set.Iterate(func(i int) {
		sum += i
	})
	t.Log(sum) // 预期 5
}

在这里插入图片描述

使用泛型实现的Set可适用于任意的可比较类型,行为与其他语言实现的Set基本类似,但是只能函数调用,不能使用下标操作访问元素。
上面实现的Set底层使用一个map实现,并不是线程安全的,还可以进一步使用自定义扩展

type SyncSet[C comparable] struct {
	l sync.RWMutex
	m map[C]struct{}
}

在读操作的时候,加读锁,在写操作的时候加写锁。

4.3 排序 Sort

要对切片中的元素进行排序,在标准库提供sort.Slice之前,每种类型的切片都需要实现sort.Interface接口中的三个方法
在这里插入图片描述

然后使用sort.Sort方法进行排序,即便后来标准库中引入了sort.Slice,但是使用时仍然需要提供一个排序函数。
使用泛型实现一个针对切片的通用排序函数

type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64 |
		~string
}

type orderSlice[O Ordered] []O

func (o orderSlice[O]) Len() int {
	return len(o)
}
func (o orderSlice[O]) Less(i, j int) bool {
	return o[i] < o[j]
}
func (o orderSlice[O]) Swap(i, j int) {
	o[i], o[j] = o[j], o[i]
}
func OrderSlice[O Ordered](s []O) {
	// s 被转换为 []Ordered 类型,也就是 orderSlice,然后排序
	// 因为 orderSlice已经实现了排序的接口,不需要额外实现了
	sort.Sort(orderSlice[O](s))
}
func TestOrder(t *testing.T) {
	is := []int{3, 4, 5, 1, 2}
	OrderSlice(is)
	t.Log(is)
	ss := []string{"he", "ww", "ss"}
	OrderSlice(ss)
	t.Log(ss)
}

在这里插入图片描述

使用泛型实现排序幻术也有一定的局限性,因为不容易处理复杂的符合类型,比如自定义的struct类型。

5. 总结

反形式衡量编程语言技术完备度的一个重要参考指标,但是也是一个比较争议的技术。
泛型的缺失导致开发者不得不编写重复的代码,或者编写相对通用但缺少类型安全的代码,甚至有些项目使用代码自动生成技术来摆脱编写
重复代码的烦恼,从这方面来看,Go确实需要泛型。
但是引入泛型也是有一定成本的,比如泛型的三个困局:

  • 没有泛型(C语言)会降低程序员的生产力,但不会增加语言的复杂度
  • 泛型会增加编译器的负担(C++),可能会编译出很多冗余的代码,进而拖慢编译时间
  • 泛型会降低运行时的性能(Java),避免编译大量冗余代码的后果是增加运行时的开销

Go语言早在1.17版本时就推出了试用版本,但在1.18中还是用了极大的篇幅说明泛型的种种风险。
https://golang.google.cn/doc/go1.18#generics

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux网络命令:用于配置防火墙规则的一个用户友好的工具ufw详解

目录 一、概述 二、安装 UFW 三、启动、重启和关闭 UFW 1、启动 2、关闭UFW 3、 重启 UFW 四、查看 UFW 状态 五、UFW 基本命令 1. 允许端口 &#xff08;1&#xff09;单个 TCP 端口 &#xff08;2&#xff09;允许单个 UDP 端口 &#xff08;3&#xff0…

音频响度归一化 - python 实现

在处理音频样本时&#xff0c;往往我们的音频样本由于录制设备&#xff0c;环境&#xff0c;人发音的音量大小的不同影响&#xff0c;会造成音频响度不统一&#xff0c;分布在一个不同的响度值域上。为了让语音模型更好的学习音频特征&#xff0c;就有必要对音频的响度进行归一…

【AIGC】ChatGPT是如何思考的:探索CoT思维链技术的奥秘

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;什么是CoT思维链CoT思维链的背景与技术发展需求 &#x1f4af;CoT思维链的工作原理&#x1f4af;CoT思维链的应用领域&#x1f4af;CoT思维链的优势&#x1f4af;CoT思维…

ppt压缩文件怎么压缩?压缩PPT文件的多种压缩方法

ppt压缩文件怎么压缩&#xff1f;当文件体积过大时&#xff0c;分享和传输就会变得困难。许多电子邮件服务对附件的大小有限制&#xff0c;而在网络环境不佳时&#xff0c;上传和下载大文件可能耗时较长。此外&#xff0c;在不同设备上播放时&#xff0c;较大的PPT文件还可能导…

基于Java+SpringBoot+Uniapp的博客系统设计与实现

项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…

<OS 有关> Windows 11 对不习惯菜单所做修改 自用

新安装 Windows 11 23H2 不习惯菜单&#xff0c;做的修改&#xff1a; 1. 禁用 Show More Options 鼠标右键 想使用旧版的 鼠标右键菜单&#xff0c; 不需要点 show more options , 如下图的方式&#xff1a; 创建一个 注册表文件&#xff1a; disable_content.reg Windows …

Maven 高级之分模块设计与继承、聚合

在软件开发中&#xff0c;随着项目规模的扩大&#xff0c;代码量和复杂度不断增加&#xff0c;传统的一体化开发模式逐渐暴露出诸多问题。为了解决这些问题&#xff0c;模块化开发应运而生&#xff0c;而 Maven 正是模块化开发的利器&#xff0c;它提供的继承和聚合机制为构建和…

STL源码剖析:STL算法

STL 算法总览 质变算法 mutating algorithms—会改变操作对象之值 所有的 STL算法都作用在由迭代器(first,last)所标示出来的区间上。所谓“质变算法”,是指运算过程中会更改区间内(迭代器所指)的元素内容。诸如拷贝(copy)、互换(swap)、替换(replace)、填写(fill)、删除(remov…

【H2O2|全栈】更多关于HTML(2)HTML5新增内容

目录 HTML5新特性 前言 准备工作 语义化标签 概念 新内容 案例 多媒体标签 音频标签audio 视频标签 video 新增部分input表单属性 预告和回顾 后话 HTML5新特性 前言 本系列博客是对入门专栏的HTML知识的补充&#xff0c;并伴随一些补充案例。 这一期主要介绍H…

一文区分SSTI 和 CSTI

前言 有时&#xff0c;SSTI&#xff08;服务器端模板注入&#xff09;和 CSTI&#xff08;客户端模板注入&#xff09;可能会由于它们相似的负载语法而混淆。这种混乱可能会导致渗透测试人员浪费时间尝试实现反向 shell&#xff0c;即使payload仅限于客户端。 定义 &#x1d…

电汽车充电革命:充电桩的过去现在与未来

电动汽车充电革命&#xff1a;中国充电桩行业的过去、现在与未来 一、发展历程概述 中国充电桩行业的发展历程可划分为以下几个阶段&#xff1a; 1. 初始期&#xff08;2006-2008年&#xff09;&#xff1a;在此阶段&#xff0c;国家队主导市场&#xff0c;主要参与者包括国…

linux的学习第二天

1.vmware的功能&#xff1a; 快照 创建快照&#xff1a; 拍摄此虚拟机的快照&#xff1a;记录保存虚拟机的当前状态&#xff0c;如果系统出现故障&#xff0c;可以通过快照还原&#xff08;错删系统时可以找到快照的系统状态&#xff0c;然后恢复系统&#xff09; 恢复快照…

基于LSTM-Transformer混合模型实现股票价格多变量时序预测(PyTorch版)

前言 系列专栏:【深度学习&#xff1a;算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域&#xff0c;讨论了各种复杂的深度神经网络思想&#xff0c;如卷积神经网络、循环神经网络、生成对…

如何替换OCP节点(一):使用oat | OceanBase应用实践

前言&#xff1a; OceanBase Cloud Platform&#xff08;简称OCP&#xff09;&#xff0c;是 OceanBase数据库的专属企业级数据库管理平台。 在实际生产环境中&#xff0c;OCP的安装通常是第一步&#xff0c;先搭建OCP平台&#xff0c;进而依赖OCP来创建、管理和监控我们的生…

Spark全网最全总结

Spark 产生之前&#xff0c;已经有 MapReduce 这类非常成熟的计算系统存在了&#xff0c;并提供 了高层次的 API(map/reduce)&#xff0c;把计算运行在集群中并提供容错能力&#xff0c;从而实现 分布式计算。 虽然 MapReduce 提供了对数据访问和计算的抽象&#xff0c…

八卦GPT-5的一切

这篇超长文章——既是评论&#xff0c;也是探索——关于GPT-5 对最受期待的下一代 AI 模型的深入分析 但它不仅仅是关于GPT-5。 • 它涉及我们对下一代AI模型的期望。 • 它关于即将出现的令人兴奋的新功能&#xff08;如推理和代理&#xff09;。它不仅讨论GPT-5技术本身&…

Web安全 - 跨站点请求伪造CSRF(Cross Site Request Forgery)

文章目录 OWASP 2023 TOP 10CSRF 导图CSRF的基本概念CSRF的工作原理常见CSRF攻击模式CSRF防御策略补充建议应用场景实战防御策略选择1. CSRF Token&#xff08;首选&#xff09;2. SameSite Cookie属性3. 验证Referer和Origin4. 多因素认证 实现方案CSRF Token实现SameSite Coo…

SQL分类中的DQL

DQL&#xff08;Data Query Language&#xff09;:数据查询语言&#xff0c;用来查询数据库中表的记录。 一、DQL语法 编写顺序 执行顺序 SELECT 字段列表 5 FROM 表名列表 1 WHERE 条件列表 2 GROUP BY 分组字段列表 3 HAVING 分组后条件列表 4 ORDER BY 排…

Golang | Leetcode Golang题解之第470题用Rand7()实现Rand10()

题目&#xff1a; 题解&#xff1a; func rand10() int {for {a : rand7()b : rand7()idx : (a-1)*7 bif idx < 40 {return 1 (idx-1)%10}a idx - 40b rand7()// get uniform dist from 1 - 63idx (a-1)*7 bif idx < 60 {return 1 (idx-1)%10}a idx - 60b rand…

Mac 电脑安装redis

1、首先检查电脑是否安装 brew 命令&#xff1a; #打开Mac自带的终端&#xff0c;输入下面命令 brew --version如下图&#xff0c;可以看到我的 brew 正常的&#xff0c;且对应版本是4.0.17-63-g32f2258 如果你的电脑执行上面命名报错&#xff1a;zsh: command not found: br…