Go业务开发常用关注点

本文对实际开发场景中面对高频的场景,总结出来的一些处理方案,希望能对业务开发的同学提供帮助!

1. 结构体转换

实际开发中会面对一个相似的数据结构,由于引用不同的包,需要开发转换到对应的结构上,本质上这些数据结构是一致的,但是所在包不同所以不能直接赋值。

常规的方案大致分为下面几种:

1. 直接转换 struct

这种适合结构完全一致的情况,参数名和类型都必须保持一致;适用场景相对较少,面对不同包的协议转换如果包含一个枚举就无效了

type aType int64
type A struct {a aType
}type bType int64
type B struct {a bType
}func Test(t *testing.T) {a := A{a: 1}b := B(a)fmt.Println(a, b)// 如果把aType和bType直接当做in64就可以正常转换
}

2. 手撸代码

开发手动转换结构,适合字段比较少的结构,同时命名不会很相似,如果相似度较高存在写错的可能,面对复杂有嵌套数据结构效率低下。

3. 正反序列化转换

这种方案相对于第一种具备更强的兼容性,可以通过 tag 来实现不同类型的转换,但是面对不同协议生成的代码还是具有局限性,同时效率比较低下,序列化是比较消耗 cpu 的操作;
需要注意的是,官方的原生 json 库处理大数存在精度丢失的问题,我们这里采用 jsonx 默认支持大数
jsonx: code.byted.org/gopkg/jsonx

type aType int64
type A struct {A aType `json:"a"`
}type bType int64
type B struct {A bType `json:"a"`
}func Test(t *testing.T) {aStr := jsonx.ToString(A{1})b := &B{}_ = jsonx.UnmarshalFromString(aStr, &b)fmt.Println(aStr, b)
}

最佳实现

这里的最佳实现其实要区分场景来考虑:

  • 面对高并发或是简单结构的场景,需要减少资源消耗,可以采用【手撸代码】的方式实现
  • 面对并发比较低的场景,通过【正反序列化】是比较好的方案,使用起来更简单

2. 数据库中存储json结构体

表中有extra字段,存储的是扩展信息,比如执行时间,通常的结构声明是这样的:

type BaseInfo struct {ID            int64        `json:"id" gorm:"column:id"`Extra         string       `json:"extra" gorm:"column:extra"`
}

意味着查询出来结构后还需要进行 unmarshal 操作,且写入数据的时候也要进行 marshal,开发者在修改数据的时候需要额外考虑其他接口所使用的数据结构,用起来不方便。

最佳实践

gorm 是支持很多拓展特性的,通过实现Scan、Value的方法就可以省去在业务代码中序列化的操作,降低开发者的心智负担,优化后大致如下:

type BaseInfo struct {ID            int64        `json:"id" gorm:"column:id"`Extra *ExtraInfo `json:"check_in_detail" gorm:"column:check_in_detail"`
}type ExtraInfo struct {Info1 `json:"info1"`
}func (BaseInfo) TableName() string {return "base_info"
}// Value return json value, implement driver.Valuer interface
// 如果接受者是指针,那么就只能是指针来调用
// 如果接受者是值类型,则支持指针、值类型来调用
func (j ExtraInfo) Value() (driver.Value, error) {return json.Marshal(j)
}// Scan scan value into Jsonb, implements sql.Scanner interface
// 接受者要使用指针类型,这才才能实际赋值
func (j *ExtraInfo) Scan(value interface{}) error {bytes, ok := value.([]byte)if !ok {return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))}result := ExtraInfo{}var err errorif len(bytes) > 0 {err = json.Unmarshal(bytes, &result)}*j = resultreturn err
}

3. Slice 过滤元素

业务开发经常需要操作 slice、map,例如过滤切片中的一些元素,或者是二者的相互转换,常规一般通过 range 后进行 append、set 等操作,这些看起来逻辑都不太优雅;这种场景我们都可以用 stream 或是泛型特性来实现~

func Test(t *testing.T) {data := make([]int64, 10)for _, v := range data {// biz codefmt.Println(v)}
}

最佳实践

  1. 过滤元素
import "code.byted.org/lang/gg/stream"   // 注意一定要go1.18版本func main(){d := []int64{0, 1, 2, 3}arr := stream.FromSlice(d).Filter(func(i int64) bool {return i != 0},).ToSlice()fmt.Println(arr)  // [1 2 3]
}

4. 通过减少堆内存分配,优化CPU占用率

堆内存分配是影响cpu占用率的重要因素。
大家可能平时可能会有一种想法:一次rpc请求都已经是ms级的了,而一次内存分配再慢也是ns级的,纠结内存分配次数真的有意义吗?答案是肯定的,因为在发起rpc请求后,cpu就去处理别的任务了,其ms级的处理延时主要影响的是请求延时(off-cpu);而内存分配这一动作虽是ns级的,却是实打实的cpu运算时间(on-cpu)。当我们的优化目标是cpu占用率时,内存分配就是一个绕不开的话题。

const size = 64var avoidEliminationSlice []int // 防止编译器优化的全局变量
// 堆分配测试
func BenchmarkMallocSlice(b *testing.B) {for i := 0; i < b.N; i++ {avoidEliminationSlice = make([]int, 0, size)}
}var avoidEliminationArray [size]int
// 栈分配测试
func BenchmarkMallocArray(b *testing.B) {for i := 0; i < b.N; i++ {avoidEliminationArray = [size]int{}}
}

上面给出两个单测,需要申请的内存都是size个int的大小,我们可以通过命令go test -bench=. -benchmem来测试这两种申请方式的性能,结果如下。

BenchmarkMallocSlice-8   14629460  94.45 ns/op  512 B/op  1 allocs/op
BenchmarkMallocArray-8  240144676  4.902 ns/op  0 B/op    0 allocs/op

可以看到,二者的alloc次数是1和0,说明了前者发生了堆分配,后者则是栈上分配。二者的cpu运算时间基本差了一个数量级。在实践中,这种差异会因为单核上千的qps而被放大,从而产生显著的cpu占用率的差别。

既然堆分配那么慢,那我们有办法将实践中的大部分堆分配都替换成栈分配吗?答案是否定的。

栈分配对象最大的特点实际上是需要编译期就能确定大小,因为这个特点,很多时候分配堆对象是不可避免的:因为业务开发上很多时候需要的对象大小就是需要到运行时才能确定,例如我们常用的各种容器。另外,在go中是否发生堆分配也和逃逸分析机制有关:即变量的生命周期是否超出了其所在的函数栈帧。最后,内联优化和接口值的赋值行为有时候也会决定一个对象是否在堆上分配。

因此,在go语言中,内存是否会被堆分配其实并没有那么明晰,go实际上也希望使用者可以尽可能不关注这一细节。尽管如此,尝试去推测和理解Go的堆分配行为依然对提升程序性能,降低runtime开销有所助益。

5. 序列化 只选取有需要的字段

大pack结构的结构体

type OriginalResp struct{A int64B int64C int64D int64...
}

如果我们在代码中仅需要A,B字段,我们可以用一个简化的结构体来减少反序列化需要处理的字段数

简化后的结构体


type SimpResp struct{A int64B int64
}

简化结构体定义,显著加速了反序列化过程,但这并没有减少任何堆内存分配次数

6. 反序列化,不要用通用的string string, 要用明确含义的类型

大部分需要访问远程数据库的服务,会将大量的cpu时间用在反序列化上。优化反序列化过程的cpu占用,在很多时候是决定性的。我们经常会选择在远程数据库中存入map[string]string类型的数据。对于需要动态更改的数据,这样的选择无可厚非:它减少了代码改动和上线的次数。但相比于使用每个字段都有具体类型的结构体,这个选择在客观上会显著增加cpu的开销。

假设我们现在有一个int64和一个float64类型的数据需要用MsgPack存储进redis。我们定义以下两种结构作为数据的schema:一种拥有正确的数据类型,一种全部转成string后塞入map[string]string中。

//go:generate msgp
type TypedDynamicFields struct {Hello int64   `gorm:"hello" json:"hello,omitempty" msg:"hello,omitempty"`World float64 `gorm:"world" json:"world,omitempty" msg:"world,omitempty"`
}//go:generate msgp
type UntypedDynamicFields map[string]string
// 产生带类型结构体序列化后的内容
func generateTypedBytes() []byte {vals := model.TypedDynamicFields{Hello: 1,World: 1.0,}bytes, _ := vals.MarshalMsg(nil)return bytes
}
// 产生map[string]string序列化后内容
func generateUntypedBytes() []byte {vals := model.UntypedDynamicFields{"hello": "1","world": "1.0",}bytes, _ := vals.MarshalMsg(nil)return bytes
}
// 测试带类型结构体的反序列化
func BenchmarkUnmarshalTypedBytes(b *testing.B) {bytes := generateTypedBytes()b.ResetTimer()for i := 0; i < b.N; i++ {var res model.TypedDynamicFieldsremainedBytes, _ = res.UnmarshalMsg(bytes)}
}
// 测试map[string]string的反序列化
func BenchmarkUnmarshalUntypedBytes(b *testing.B) {bytes := generateUntypedBytes()b.ResetTimer()for i := 0; i < b.N; i++ {var res model.UntypedDynamicFieldsremainedBytes, _ = res.UnmarshalMsg(bytes)}
}

二者的运行结果如下。大家可以试着分析一下,在这个测试中,map[string]string反序列化过程中产生的5次堆分配分别是用于存储什么。

BenchmarkUnmarshalTypedBytes-8    59285511    19.94 ns/op    0 B/op  0 allocs/op
BenchmarkUnmarshalUntypedBytes-8  5916292     203.4 ns/op  352 B/op  5 allocs/op

通过这个单测本身,我们就已经能观察到这两种存储方式在反序列化时的巨大性能差异。然而在现实中,在map中根据key检索也显著慢于在struct中根据字段名取字段值。这也就意味着,后续对反序列化产物的使用过程会产生更大的性能差异。

但实际编码中我们并不总是能将两者互相转换的,结构体终究是没有map灵活。结构体没有map灵活的根本原因在于:结构体中所能包含的键值对在编译完成后就已经固定住了,而我们时常希望新增字段时不需要上线变更,这只有动态容器能做到。因此,一个比较务实的做法是:尽可能将初期设计的动态容器在不会发生变更后用结构体的方式固定下来。

7. 查找元素,数据量小时,slice比map更快

会有一些场景,我们需要判断一个值的集合中是否包含某个特定的值。一般来说,我们会选择用map来做这种存在性检验,这很符合我们学到的知识: 哈希表判断一个key是否存在是常数复杂度的。

当我们有这种需求时,现有的内容可能只有一个slice。我们会想,如果选择直接遍历slice查看其是否包含某个特定的key,算法复杂度为O(n),因此速度会比创建一个map然后在map中查找更慢。但事实真的是这样的吗?当我们在比较这两种做法的时候,有几个因素是不可忽略的:

  • slice是现有的,map是需要新malloc的
  • 单次查找一个key,map真的一定比slice快吗
  • 在map比slice单次查找更快的时候,查找次数能均摊掉malloc带来的成本吗

不难发现,以上这些因素中,最关键的点在于:当元素数量达到什么程度,map的单次查找速度能才能快于slice。因此这里也提供了一个简单的单测尝试探讨这一问题。

const capacity = 16
// 生成一个slice
func generateSlice() []int {res := make([]int, capacity)for i := 0; i < capacity; i++ {res[i] = i}return res
}
// 生成一个map
func generateMap() map[int]struct{} {res := make(map[int]struct{}, capacity)for i := 0; i < capacity; i++ {res[i] = struct{}{}}return res
}
// 判断slice中是否有某个key
func sliceContains(s []int, target int) bool {for _, val := range s {if val == target {return true}}return false
}
// 判断map中是否有某个key
func mapContains(m map[int]struct{}, target int) bool {_, ok := m[target]return ok
}
var exist bool // 防止编译器优化
// 测试slice中单次查找性能
func BenchmarkContainsSlice(b *testing.B) {s := generateSlice()target := fastrand.Intn(capacity)b.ResetTimer()for i := 0; i < b.N; i++ {exist = sliceContains(s, target)}
}
// 测试map单次查找性能
func BenchmarkContainsMap(b *testing.B) {m := generateMap()target := fastrand.Intn(capacity)b.ResetTimer()for i := 0; i < b.N; i++ {exist = mapContains(m, target)}
}
// 容量=8时
BenchmarkContainsSlice-8    505969701    2.257 ns/op    0 B/op    0 allocs/op
BenchmarkContainsMap-8      298618323    3.960 ns/op    0 B/op    0 allocs/op
// 容量=16时
BenchmarkContainsSlice-8    966832947    3.161 ns/op    0 B/op    0 allocs/op
BenchmarkContainsMap-8      231526172    5.792 ns/op    0 B/op    0 allocs/op
// 容量=32时
BenchmarkContainsSlice-8    348595730    16.51 ns/op    0 B/op    0 allocs/op
BenchmarkContainsMap-8      230518400    5.334 ns/op    0 B/op    0 allocs/op
// 容量=64时
BenchmarkContainsSlice-8    53850733     19.06 ns/op    0 B/op    0 allocs/op
BenchmarkContainsMap-8      168312292    6.387 ns/op    0 B/op    0 allocs/op

可以看到,在容量较小时,slice查找单key的速度实际上要快于map。因此,在实践中,我们还是应该结合具体的业务场景特点来做抉择。

8. 传值与传指针

我们知道,cpu在工作时,实际上就是在不停的拷贝bits,传值还是传指针,对cpu而言其实是没有区别的,都意味着复制,差别只在于拷贝内容的多少。但我们应该也听过,在Go中小对象应优先考虑传值。排除掉语义需求上必须传值或是必须传指针的场景,在一个传值与传指针都可以的场合,我们究竟该怎么选择呢?真正的抉择依据是什么呢?

  1. 传指针导致堆分配
const StructSize = 1024 // 用于控制结构体大小type Value struct {content [StructSize]byte
}func returnValue() Value { // 返回值return Value{content: [StructSize]byte{},}
}func returnPtr() *Value { // 返回指针return new(Value)
}var returnedValue Value // 防止编译器优化
// 测试返回值
func BenchmarkReturnValue(b *testing.B) {for i := 0; i < b.N; i++ {returnedValue = returnValue()}
}var returnedPtr *Value // 防止编译器优化
// 测试返回指针
func BenchmarkReturnPtr(b *testing.B) {for i := 0; i < b.N; i++ {returnedPtr = returnPtr()}
}
BenchmarkReturnValue-8  128926057  9.106 ns/op     0 B/op  0 allocs/op
BenchmarkReturnPtr-8    7841412    151.5 ns/op  1024 B/op  1 allocs/op

在这个场景中,无论如何调整结构体大小,基本永远都是返回值更快,原因就是在返回指针时,因为指针被赋值给了全局变量,所以这个对象逃逸到了堆上。在这个场景下,拷贝的开销远远跟不上堆分配内存的开销。

  1. 不会导致堆分配的传参场景
const copyTimes = 1024 // 拷贝次数,放大传参影响
const StructSize = 16 // 控制结构体大小var existingValue Value // 防止编译器优化
// 测试返回一个现有值
func BenchmarkReturnExistingValue(b *testing.B) {value := Value{}returnExistingValue := func() Value {return value}b.ResetTimer()for i := 0; i < b.N; i++ {for j := 0; j < copyTimes; j++ {existingValue = returnExistingValue()}}
}var existingPtr *Value // 防止编译器优化
// 测试返回一个现有指针
func BenchmarkReturnExistingPtr(b *testing.B) {ptr := new(Value)returnExistingPtr := func() *Value {return ptr}b.ResetTimer()for i := 0; i < b.N; i++ {for j := 0; j < copyTimes; j++ {existingPtr = returnExistingPtr()}}
}
// 当StructSize = 16 时
BenchmarkReturnExistingValue-8    3795523    320.7 ns/op    0 B/op    0 allocs/op
BenchmarkReturnExistingPtr-8      2694915    429.1 ns/op    0 B/op    0 allocs/op
// 当StructSize = 32 时
BenchmarkReturnExistingValue-8    3024436    391.9 ns/op    0 B/op    0 allocs/op
BenchmarkReturnExistingPtr-8      2834446    420.5 ns/op    0 B/op    0 allocs/op
// 当StructSize = 64 时
BenchmarkReturnExistingValue-8    1366735    872.1 ns/op    0 B/op    0 allocs/op
BenchmarkReturnExistingPtr-8      2745406    450.1 ns/op    0 B/op    0 allocs/op

可以看到,两个单测在不同的参数下都没有发生任何堆对象分配。通过调整StructSize参数,我们观察到拷贝指针的开销是相对比较稳定的,而拷贝值的开销则随着StructSize的增大而增大,最终显著超过了拷贝指针。

当需要拷贝的值较大时,传值会比传指针慢很容易理解,毕竟指针实际上只是一个整数的大小。但小对象为什么会传值会更快呢?Go的gc的优化目标是减小stw时间,其采用的三色标记算法需要在堆对象指针发生写行为时,由编译器在生成代码时插入相应写屏障,这会导致一次指针赋值行为不仅仅是一个指针值的拷贝。这实际上是一种为了减少暂停而牺牲吞吐量的做法,感兴趣的同学可以写一段代码后编译成Go汇编,就能看到相关的函数调用,这里就不再赘述。

现实中,在传值和传指针皆可的场合,存在这样一个天然矛盾:传指针通常意味着将对象分配到堆上,会有一次较大的初始开销,但后续每次传递的开销较小;将对象放在栈上,不会有较大的初始分配开销,但每次在函数栈帧间传递的开销都会更大。在现实场景中,传值和传指针哪个是更好的做法并没有一个简单的答案,这更多的取决于传递次数,对象大小等等因素,需要结合场景具体分析调优。

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

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

相关文章

浅谈xss

XSS 简介 XSS,全称Cross Site Scripting,即跨站脚本攻击,是最普遍的Web应用安全漏洞。这类漏洞能够使得攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,则可导致嵌入的恶意脚本代码的执行,从而达到恶意攻击用户的目的。需要强调的是,XSS不仅…

setTimeout,setInterval,setImmdeiate的区别

setTimeout, setInterval, 和 setImmediate 是 JavaScript 中用于执行异步操作的定时器函数&#xff0c;它们之间有一些重要的区别&#xff1a; 1. setTimeout: - setTimeout 用于在一定的延迟时间后执行一个函数。 - 它接受两个参数&#xff1a;要执行的函数和延迟的毫…

webpack、vue.config.js

一、webpack学习 简述 webpack是一个静态资源打包工具&#xff0c;它会以一个或多个文件作为打包的入口&#xff0c;将我们整个项目的文件编译组合成一个或多个文件输出出去。输出的文件就是编译好的文件&#xff0c;可以运行在浏览器中。一般的我们将webpack输出的文件叫做b…

使用ElementUI结合Mock完成主页的搭建

目录 一、Mock ( 1 ) 讲述 ( 2 ) 作用 二、引用 三、主页搭建 学习后带来的收获 一、Mock ( 1 ) 讲述 Mock.js是一个用于前端开发中模拟数据的库。它可以帮助开发人员在前端开发过程中模拟接口返回的数据&#xff0c;从而实现前后端分离开发。Mock.js提供了一套简单易…

Python和Scrapy构建可扩展的框架

构建一个可扩展的网络爬虫框架是利用Python和Scrapy实现高效数据采集的重要技能。在本文中&#xff0c;我将为您介绍如何使用Python和Scrapy搭建一个强大灵活的网络爬虫框架。我们将按照以下步骤展开&#xff1a; 1. 安装Scrapy&#xff1a; 首先&#xff0c;确保您已经安装了…

如何计算3种卷积之后的尺寸(普通卷积,转置卷积,空洞卷积)

文章目录 前言一、普通卷积二、转置卷积三、空洞卷积 前言 三种卷积之后的feature map的尺寸如何计算。包括普通卷积&#xff0c;转置卷积&#xff0c;空洞卷积。可以在下面这个链接看到三种卷积的动态图。 卷积动态图 一、普通卷积 普通卷积比较简单了&#xff0c;其计算方式…

《动手学深度学习 Pytorch版》 6.4 多输入多输出通道

import torch from d2l import torch as d2l6.4.1 多输入通道 简言之&#xff0c;多通道即为单通道之推广&#xff0c;各参数对上即可。 def corr2d_multi_in(X, K):# 先遍历“X”和“K”的第0个维度&#xff08;通道维度&#xff09;&#xff0c;再把它们加在一起return sum…

【python爬虫】爬虫所需要的爬虫代理ip是什么?

目录 前言 一、什么是爬虫代理 IP 二、代理 IP 的分类 1.透明代理 2.匿名代理 3.高匿代理 三、如何获取代理 IP 1.免费代理网站 2.付费代理服务 四、如何使用代理 IP 1.使用 requests 库 2.使用 scrapy 库 五、代理 IP 的注意事项 1.代理 IP 可能存在不稳定性 2…

企业关键数据采集如何做

数据对于企业的重要性不言而喻&#xff0c;目前又处于大数据时代&#xff0c;企业对于数据的解读将是辅助决策最重要的一环。依据所掌握的数据信息&#xff0c;帮助企业做决策的优化。然而&#xff0c;在企业的关键数据采集并不是一项简单轻松的任务&#xff0c;他需要企业投入…

DC电源模块的过热保护功能

BOSHIDA DC电源模块的过热保护功能 DC电源模块的过热保护功能是为了防止电源模块因长时间工作或外部环境因素导致的过热而损坏。在使用DC电源模块时&#xff0c;电源模块内部的电子元件会产生一定的热量&#xff0c;如果超过了元件所能承受的温度范围&#xff0c;就可能会发生…

【VsCode】vscode创建文件夹有小图标显示和配置

效果 步骤 刚安装软件后&#xff0c; 开始工作目录下是没有小图标显示的。 如下图操作&#xff0c;安装vscode-icons 插件&#xff0c;重新加载即可 创建文件夹&#xff0c;显示图标如下&#xff1a;

Jenkins学习笔记2

Jenkins下载安装&#xff1a; 从清华源开源镜像站上下载jenkins的安装包&#xff1a; 安装的是这个版本。 关于软件的版本&#xff0c;尽量使用LTS&#xff0c;长期支持。 首先是安装openjdk&#xff1a; yum install fontconfig java-11-openjdk[rootlocalhost soft]# java …

ElasticSearch - DSL查询文档语法,以及深度分页问题、解决方案

目录 一、DSL 查询文档语法 前言 1.1、DSL Query 基本语法 1.2、全文检索查询 1.2.1、match 查询 1.2.2、multi_match 1.3、精确查询 1.3.1、term 查询 1.3.2、range 查询 1.4、地理查询 1.4.1、geo_bounding_box 1.4.2、geo_distance 1.5、复合查询 1.5.1、相关…

ubuntu18.04安装docker

ubuntu18.04安装docker 文章目录 ubuntu18.04安装docker一.安装1.更新软件库索引2.安装一些必要的软件包3.添加Docker的官方GPG密钥4.添加Docker软件库5.再次更新软件库索引6.安装Docker CE7.启动Docker并设置开机启动8.验证Docker安装9.(若要让非root用户可以运行Docker命令)可…

Pipenv:一个Python的包管理工具,用于创建一个python的虚拟环境

Pipenv是一个Python的包管理工具&#xff0c;它提供了一种简单的方式来管理Python项目的依赖关系。 以下是使用Pipenv的一些基本步骤&#xff1a; 安装Pipenv&#xff1a;可以通过运行以下命令来安装pipenv&#xff1a; pip install pipenv初始化项目&#xff1a;在项目的根…

【QT】QT事件Event大全

很高兴在雪易的CSDN遇见你 &#xff0c;给你糖糖 欢迎大家加入雪易社区-CSDN社区云 前言 本文分享QT中的事件Event技术&#xff0c;主要从QT事件流程和常用QT事件方法等方面展开&#xff0c;希望对各位小伙伴有所帮助&#xff01; 感谢各位小伙伴的点赞关注&#xff0c;小易…

C# string stringbuilder区别

在C#中&#xff0c;有三个主要的字符串类型&#xff0c;分别是string、StringBuffer和StringBuilder。这些类型之间有一些重要的区别&#xff1a; string&#xff1a; string 是C#中的字符串类型&#xff0c;是不可变的。这意味着一旦创建了一个字符串对象&#xff0c;就不能修…

如何搭建数据驱动自动化测试框架?

前言 说到数据驱动自动化测试&#xff0c;你会不会有这样的疑问&#xff1a;数据怎么管理&#xff1f;数据怎么才能驱动测试用例执行&#xff1f;到底怎么样才算数据驱动&#xff1f;那么本篇文章就教你如何进行数据驱动测试&#xff0c;相信你一定能对数据驱动自动化测试有一…

Android AMS——ActivityThread解析(四)

一、ActivityThread简介 ActivityThread 是Android 系统中的一个关键类,它是应用程序的入口点,并且负责启动和管理应用程序的主线程以及处理与UI相关的操作。下面是关于 ActivityThread 的一些简介: 应用程序的启动:当用户启动一个应用程序时,Android系统会创建一个新的进…

FlashDuty Changelog 2023-09-21 | 自定义字段和开发者中心

FlashDuty&#xff1a;一站式告警响应平台&#xff0c;前往此地址免费体验&#xff01; 自定义字段 FlashDuty 已支持接入大部分常见的告警系统&#xff0c;我们将推送内容中的大部分信息放到了 Lables 进行展示。尽管如此&#xff0c;我们用户还是会有一些扩展或定制性的需求…