Golang——7、包与接口详解

news2025/6/13 4:57:19

包与接口详解

  • 1、Golang包详解
    • 1.1、Golang中包的定义和介绍
    • 1.2、Golang包管理工具go mod
    • 1.3、Golang中自定义包
    • 1.4、Golang中使用第三包
    • 1.5、init函数
  • 2、接口详解
    • 2.1、接口的定义
    • 2.2、空接口
    • 2.3、类型断言
    • 2.4、结构体值接收者和指针接收者实现接口的区别
    • 2.5、一个结构体实现多个接口
    • 2.6、接口嵌套
    • 2.7、空接口和类型断言使用细节

1、Golang包详解

1.1、Golang中包的定义和介绍

包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、strconv、strings、sort、errors、time、encoding/json、os、io等。
Golang 中的包可以分为三种:1、系统内置包。2、自定义包。3、第三方包。
系统内置包: Golang语言给我们提供的内置包,引入后可以直接使用,如 fmt、 strconv、 strings、sort、errors、time、encoding/json、os、io等。
自定义包:开发者自己写的包。
第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面给大家介绍的。
"github.com/shopspring/decimal"包解决float精度丢失问题。

1.2、Golang包管理工具go mod

在Golang1.11版本之前如果我们要自定义包的话必须把项目放在GOPATH 目录。Go1.11版本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项目放到 GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,Go1.13 以后可以彻底不要GOPATH了。

首先在项目路径下使用go mod init 文件名,来初始化项目,生成一个go.mod文件。
在这里插入图片描述
注意,这个文件名必须是当前所在目录下的文件名,比如当前我在demo60目录下,因此文件名就是demo60。

在这里插入图片描述
另外,使用命令go mod可以查看其他选项,在这些选项中我们最常用的就是init和tidy。
其中go mod tidy表示增加丢失的module,去掉未用的module,当我们执行这个指令后,就会自动将我们需要包下载下来,将不需要的包去除。


1.3、Golang中自定义包

包(package)是多个Go源码的集合,一个包可以简单理解为一个存放多个.go文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
在这里插入图片描述
如图:我们在demo60目录下创建一个clac目录,并创建一个clac。

package main

import (
	"demo60/clac"
	"fmt"
)

func main() {
	res1 := clac.Add(5, 10)
	res2 := clac.Sub(5, 10)
	fmt.Println(res1, res2)
}

接着我们在main.go中引入我们自定义的clac包,并调用里面的Add和Sub方法。
在这里插入图片描述

同时我们需要注意,在clac这个包中,可以定义变量和函数,但是必须是大写的,大写的表示公有属性,可以被其他包访问,小写表示私有属性,不能被其他包使用。


当然我们也可以引入多个包,例如我们在demo60目录下创建一个tools文件夹,里面再定义几个.go文件,但他们都属于同一个包tools。
在这里插入图片描述

package tools

import "fmt"

func PrintInfo() {
	fmt.Println("hello world")
}
package tools

func Mul(x, y int) int {
	return x * y
}

然后我们在main.go中调用这两个函数。

package main

import (
	"demo60/clac"
	"demo60/tools"
	"fmt"
)

func main() {
	res1 := clac.Add(5, 10)
	res2 := clac.Sub(5, 10)
	fmt.Println(res1, res2)
	fmt.Println(clac.AA)

	fmt.Println(tools.Mul(2, 3))
	tools.PrintInfo()
}

在这里插入图片描述
上面就演示了我们自定义了两个包clac和tools,并通过import引入使用。


另外,我们也可以匿名导入一个包,不过在现在vscode的go语法已经非常智能了,所以基本上就用不到了。
例如下面这种情况,我们导入了一个fmt包,但是并没有使用,我们可以在前面加上_

package main

import _ "fmt"

func main() {

}

我们也可以给导入的包起别名,比如我们导入的一个包名字过长,我们就可以给他起个别名。

package main

import f "fmt"

func main() {
	f.Println("hello world")
}

在这里插入图片描述


1.4、Golang中使用第三包

之前我们说过,Golang中的浮点数类型是有精度丢失的,所以我们可以使用第三方包decimal,下面演示如何使用第三方包decimal。
首先在项目目录下执行go mod init初始化项目,然后实现main.go文件。

package main

import (
	"fmt"

	"github.com/shopspring/decimal"
)

func main() {
	num1 := 8.2
	num2 := 3.8
	d1 := decimal.NewFromFloat(num1).Add(decimal.NewFromFloat(num2))
	fmt.Println(d1)
}

当我们这样写好代码保存后是无法运行的,因为我们并没有第三方包decimal。
所以这里就需要使用之前的go mod tidy。它会自动根据我们的文件查找是否有缺少的包,或者不需要使用的包。

在这里插入图片描述


1.5、init函数

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。
在这里插入图片描述

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go 编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其init()函数,如下图示:

在这里插入图片描述
有点类似DFS的感觉。


2、接口详解

Golang中的接口是一种抽象数据类型,Golang中接口定义了对象的行为规范,只定义规范不实现。接口中定义的规范由具体的对象来实现。
通俗的讲接口就一个标准,它是对一个对象的行为和规范进行约定,约定实现接口的对象必须得按照接口的规范。

2.1、接口的定义

在Golang中接口(interface)是一种类型,函数method的集合,Golang中的接口不能包含任何变量。一种抽象的类型。接口(interface)是一组在Golang中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想Golang中的接口也是一种数据类型,不需要显示实现。只需要一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口。
在这里插入图片描述
其中:
• 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。 接口名最好要能突出该接口的类型含义。
• 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
• 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

实现一个Usber接口规范,接着让手机和相机实现这个接口。

package main

import "fmt"

type Usber interface {
	start()
	stop()
}

type Phone struct {
	Name string
}

func (p Phone) start() {
	fmt.Printf("%v启动\n", p.Name)
}

func (p Phone) stop() {
	fmt.Printf("%v关闭\n", p.Name)
}

type Camera struct {
	Name string
}

func (c Camera) start() {
	fmt.Printf("%v启动\n", c.Name)
}

func (c Camera) stop() {
	fmt.Printf("%v关闭\n", c.Name)
}

func main() {
	p := Phone{
		"小米手机",
	}
	c := Camera{
		"哈苏相机",
	}
	var u1, u2 Usber // Golang中接口就是一个数据类型
	u1 = p           // 表示Phone实现接口Usber
	u2 = c           // 表示Camera实现接口Usber
	u1.start()
	u1.stop()
	u2.start()
	u2.stop()
}

在这里插入图片描述

下面我们可以再实现一个Computer结构体,然后实现它的一个方法work,该方法必须传入一个Usber接口。

package main

import "fmt"

type Usber interface {
	start()
	stop()
}

type Phone struct {
	Name string
}

func (p Phone) start() {
	fmt.Printf("%v启动\n", p.Name)
}

func (p Phone) stop() {
	fmt.Printf("%v关闭\n", p.Name)
}

type Camera struct {
	Name string
}

func (c Camera) start() {
	fmt.Printf("%v启动\n", c.Name)
}

func (c Camera) stop() {
	fmt.Printf("%v关闭\n", c.Name)
}

type Computer struct {
}

func (c Computer) work(u Usber) {
	u.start()
	u.stop()
}

func main() {
	p := Phone{
		"小米手机",
	}
	c := Camera{
		"哈苏相机",
	}
	computer := Computer{}
	computer.work(p)
	computer.work(c)
}

2.2、空接口

Golang中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。

1、定义空接口
空接口表示没有任何约束,任何类型都可以实现空接口。

package main

import "fmt"

type A interface{}

func main() {
	var a A
	var str = "你好golang"
	a = str
	fmt.Printf("值: %v, 类型: %T\n", a, a)

	var num = 10
	a = num
	fmt.Printf("值: %v, 类型: %T\n", a, a)

	
	var flag = true
	a = flag
	fmt.Printf("值: %v, 类型: %T\n", a, a)
}

在这里插入图片描述

2、空接口作为函数参数

package main

import "fmt"

func show(a interface{}) {
	fmt.Printf("值: %v, 类型: %T\n", a, a)
}

func main() {
	show(20)
	show("你好golang")
	show([]int{1, 2, 3, 4, 5})
}

在这里插入图片描述

3、map的值实现空接口,使用空接口实现可以保存任意值的字典。

package main

import "fmt"

func main() {
	var m = make(map[string]interface{})
	m["username"] = "张三"
	m["age"] = 18
	m["hobby"] = []string{"跑步", "爬山", "写代码"}
	fmt.Printf("值: %v, 类型: %T\n", m, m)
}

在这里插入图片描述

4、切片实现空接口。

var slice = []interface{}{"张三", 18, true, 33.22}
fmt.Printf("值: %v, 类型: %T\n", slice, slice)

在这里插入图片描述


2.3、类型断言

一个接口的值 (简称接口值) 是由一个具体类型和具体类型的值两部分组成的。 这两部分分别称为接口的动态类型和动态值。
如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:
• x :表示类型为interface{}的变量
• T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为 T 类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为 alse则表示断言失败。

1、基本使用。

package main

import "fmt"

func main() {
	var x interface{}
	x = "hello golang"
	v, ok := x.(string)
	if ok {
		fmt.Printf("类型为string, 值: %v\n", v)
	} else {
		fmt.Println("类型断言失败")
	}
}

在这里插入图片描述

2、定义一个方法,可以传入任意类型,然后根据不同的类型实现不同的功能。
下面实现两种方式:if-else和switch-case。

package main

import "fmt"

func MyPrint1(x interface{}) {
	if _, ok := x.(string); ok {
		fmt.Println("string类型")
	} else if _, ok := x.(int); ok {
		fmt.Println("int类型")
	} else if _, ok := x.(bool); ok {
		fmt.Println("bool类型")
	}
}

func MyPrint2(x interface{}) {
	switch x.(type) {
	case string:
		fmt.Println("string类型")
	case int:
		fmt.Println("int类型")
	case bool:
		fmt.Println("bool类型")
	default:
		fmt.Println("传入错误...")
	}
}

func main() {
	MyPrint1("你好golang")
	MyPrint2(12)
}

在这里插入图片描述
注意:x.(type)只能结合switch语句使用。


针对前面的实现的Computer,我们判断传入的类型然后实现对应的方法。有点类似于C++中的多肽。

package main

import "fmt"

type Usber interface {
	start()
	stop()
}

type Phone struct {
	Name string
}

func (p Phone) start() {
	fmt.Printf("%v启动\n", p.Name)
}

func (p Phone) stop() {
	fmt.Printf("%v关闭\n", p.Name)
}

type Camera struct {
	Name string
}

func (c Camera) start() {
	fmt.Printf("%v启动\n", c.Name)
}

func (c Camera) stop() {
	fmt.Printf("%v关闭\n", c.Name)
}

type Computer struct {
}

func (c Computer) work(u Usber) {
	if _, ok := u.(Phone); ok {
		u.start()
	} else if _, ok := u.(Camera); ok {
		u.stop()
	}
}
func main() {
	p := Phone{
		"小米手机",
	}
	c := Camera{
		"哈苏x2d",
	}
	com := Computer{}
	com.work(p)
	com.work(c)
}

在work函数中我们实现类型断言,如果是Phone类型就执行start函数,如果是Camera类型就执行stop函数。


2.4、结构体值接收者和指针接收者实现接口的区别

package main

import "fmt"

type Usber interface {
	start()
	stop()
}

type Phone struct {
	Name string
}

func (p Phone) start() {
	fmt.Printf("%v启动!\n", p.Name)
}

func (p Phone) stop() {
	fmt.Printf("%v关闭!\n", p.Name)
}

func main() {
	var u Usber
	var p1 = Phone{
		"小米手机",
	}
	u = p1
	u.start()
	u.stop()

	var p2 = &Phone{
		"苹果手机",
	}
	u = p2
	u.start()
	u.stop()
}

在这里插入图片描述
根据现象,如果结构体中的方法是值接受者,那么实例化后的结构体值类型和指针类型都可以赋值给接口变量。

将start和stop方法改为指针接受者类型*Phone,那么结构体值类型就无法复制给接口变量了,此时只有指针类型可以赋值给接口变量。

另外如果要修改结构体内的成员变量,必须采用指针接受者的方式,如下:

package main

import "fmt"

type Animaler interface {
	setName(name string)
	getName() string
}

type Dog struct {
	Name string
}

func (d *Dog) setName(name string) {
	d.Name = name
}

func (d Dog) getName() string {
	return d.Name
}

func main() {
	d := &Dog{
		Name: "小柴",
	}
	var a Animaler
	a = d
	fmt.Println(a.getName())
	a.setName("小奇")
	fmt.Println(a.getName())
}

在这里插入图片描述


2.5、一个结构体实现多个接口

package main

import "fmt"

type Animaler1 interface {
	getName() string
}

type Animaler2 interface {
	setName(name string)
}

type Dog struct {
	Name string
}

func (d Dog) getName() string {
	return d.Name
}

func (d *Dog) setName(name string) {
	d.Name = name
}

func main() {
	d := &Dog{
		Name: "小柴",
	}
	var a1 Animaler1
	var a2 Animaler2
	a1 = d
	a2 = d
	fmt.Println(a1.getName())
	a2.setName("小奇")
	fmt.Println(a1.getName())
}

在这里插入图片描述


2.6、接口嵌套

package main

import "fmt"

type Ainterface interface {
	SetName(name string)
}

type Binterface interface {
	GetName() string
}

type Animaler interface {
	Ainterface
	Binterface
}

type Dog struct {
	Name string
}

func (d Dog) GetName() string {
	return d.Name
}

func (d *Dog) SetName(name string) {
	d.Name = name
}

func main() {
	d := &Dog{
		Name: "小柴",
	}
	var a Animaler
	a = d
	fmt.Println(a.GetName())
	a.SetName("小奇")
	fmt.Println(a.GetName())
}

在这里插入图片描述


2.7、空接口和类型断言使用细节

下面的代码有没有问题?

package main

import "fmt"

type Address struct {
	Name  string
	Phone string
}

func main() {
	var m = make(map[string]interface{})
	m["username"] = "张三"
	m["age"] = 20
	m["hobby"] = []string{"跑步", "爬山", "写代码"}
	fmt.Println(m["username"])
	fmt.Println(m["age"])
	fmt.Println(m["hobby"])
	fmt.Println(m["hobby"][0])
}

在这里插入图片描述
可以看到报错了,不支持这么访问。

再来看另一种情况:

m["address"] = Address{
	Name:  "李四",
	Phone: "13912344321",
}
fmt.Println(m["address"])
fmt.Println(m["address"].Name)

在这里插入图片描述
这样也是不支持的。

解决办法,实现类型断言接受后再访问:

package main

import "fmt"

type Address struct {
	Name  string
	Phone string
}

func main() {
	var m = make(map[string]interface{})
	m["username"] = "张三"
	m["age"] = 20
	m["hobby"] = []string{"跑步", "爬山", "写代码"}
	fmt.Println(m["username"])
	fmt.Println(m["age"])
	fmt.Println(m["hobby"])
	hobby, _ := m["hobby"].([]string)
	fmt.Println(hobby[0], hobby[1], hobby[2])
	m["address"] = Address{
		Name:  "李四",
		Phone: "13912344321",
	}
	fmt.Println(m["address"])
	address, _ := m["address"].(Address)
	fmt.Println(address.Name, address.Phone)
}

在这里插入图片描述

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

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

相关文章

给网站添加live2d看板娘

给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…

手机平板能效生态设计指令EU 2023/1670标准解读

手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用

在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…

从“安全密码”到测试体系:Gitee Test 赋能关键领域软件质量保障

关键领域软件测试的"安全密码":Gitee Test如何破解行业痛点 在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的"神经中枢"。从国防军工到能源电力,从金融交易到交通管控,这些关乎国计民生的关键领域…

Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案

在大数据时代,海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构,在处理大规模数据抓取任务时展现出强大的能力。然而,随着业务规模的不断扩大和数据抓取需求的日益复杂,传统…

pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)

目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 (1)输入单引号 (2)万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…

论文阅读笔记——Muffin: Testing Deep Learning Libraries via Neural Architecture Fuzzing

Muffin 论文 现有方法 CRADLE 和 LEMON,依赖模型推理阶段输出进行差分测试,但在训练阶段是不可行的,因为训练阶段直到最后才有固定输出,中间过程是不断变化的。API 库覆盖低,因为各个 API 都是在各种具体场景下使用。…

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制

目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …

关于easyexcel动态下拉选问题处理

前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…

Ubuntu Cursor升级成v1.0

0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开,快捷键也不好用,当看到 Cursor 升级后,还是蛮高兴的 1. 下载 Cursor 下载地址:https://www.cursor.com/cn/downloads 点击下载 Linux (x64) ,…

wpf在image控件上快速显示内存图像

wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…

android RelativeLayout布局

<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…

windows系统MySQL安装文档

概览&#xff1a;本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容&#xff0c;为学习者提供全面的操作指导。关键要点包括&#xff1a; 解压 &#xff1a;下载完成后解压压缩包&#xff0c;得到MySQL 8.…

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…

如何应对敏捷转型中的团队阻力

应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中&#xff0c;明确沟通敏捷转型目的尤为关键&#xff0c;团队成员只有清晰理解转型背后的原因和利益&#xff0c;才能降低对变化的…

华为OD机试-最短木板长度-二分法(A卷,100分)

此题是一个最大化最小值的典型例题&#xff0c; 因为搜索范围是有界的&#xff0c;上界最大木板长度补充的全部木料长度&#xff0c;下界最小木板长度&#xff1b; 即left0,right10^6; 我们可以设置一个候选值x(mid)&#xff0c;将木板的长度全部都补充到x&#xff0c;如果成功…

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准

城市路内停车管理常因行道树遮挡、高位设备盲区等问题&#xff0c;导致车牌识别率低、逃费率高&#xff0c;传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法&#xff0c;正成为破局关键。该设备安装于车位侧方0.5-0.7米高度&#xff0c;直接规避树枝遮…

通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器

拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件&#xff1a; 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…

在 Spring Boot 中使用 JSP

jsp&#xff1f; 好多年没用了。重新整一下 还费了点时间&#xff0c;记录一下。 项目结构&#xff1a; pom: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://ww…

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

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