【节选】Go语言的100个错误使用场景|数据类型

news2024/11/26 0:48:12

Data types

🌟 章节概述:

  • 基本类型涉及的常见错误

  • 掌握 slice 和 map 的基本概念,避免使用时产生 bug

  • 值的比较

低效的切片初始化(#21)

实现一个 conver 方法,将一个切片 Foo 转换成另一个类型的切片 Bar,这里给出三种实现方式:

// 方式一
func convert(foos []Foo) []Bar {
    bars := make([]Bar, 0)
    
    for _, foo := range foos {
        bars = append(bars, fooToBar(foo))
    }
    return bars
}
// 方式二
func convert(foos []Foo) []Bar {
    n := len(foos)
    // 设置容量但是不设置长度,此时append调用会从0索引开始为底层数组赋值
    bars := make([]Bar, 0, n)
    
    for _, foo := range foos {
        bars = append(bars, fooToBar(foo))
    }
    return bars
}
// 方式三
func convert(foo []Foo) []Bar {
    n := len(foo)
    // 设置len之后,会初始化这部分的值为Foo的零值,此时append会追加在len之后,触发扩容
    bars := make([]Bar, n)
    
    for i, foo := range foos {
        bars[i] = fooToBar(foo)
    }
    return bars
}

  • 方式一:由于没有初始化切片的长度,因此切片随着 append 逐渐扩容,不断替换底层数组,增加 GC 压力,在已知切片长度的时候,不推荐使用。

  • 方式二和方式三:单就性能来说方式三会更好一点,因为不用调用 append 操作。但是在大多数情况下方式二的表述更为清晰。因为如果遇到 convert 方法内有复杂逻辑,直接使用索引去为 bars[i] 设置值不太方便。

🌟 如果有一个场景是需要将一个 Foo 切片转换成一个两倍长度的 Bar 切片,则使用索引复制的方式看起来将不太清晰,且不易维护:

// 方式二
func convert(foos []Foo) []Bar {
    n := len(foos)
    // 设置容量但是不设置长度,此时append调用会从0索引开始为底层数组赋值
    bars := make([]Bar, 0, 2*n)
    
    for _, foo := range foos {
        bars = append(bars, fooToBar(foo))
        bars = append(bars, fooToBar(foo))
    }
    return bars
}
// 方式三
func convert(foo []Foo) []Bar {
    n := len(foo)
    // 设置len之后,会初始化这部分的值为Foo的零值,此时append会追加在len之后,触发扩容
    bars := make([]Bar, 2*n)
    
    for i, foo := range foos {
        bars[2*i] = fooToBar(foo)
        bars[2*i+1] = fooToBar(foo)
    }
    return bars
}

切片为 nil 与为空混淆(#22)

两个概念:

  • 一个切片为空,如果它的长度是0

  • 一个切片为nil,如果这个切片等于nil

func main {
    var s []string // 方式一
    long(1, s) 
    
    s = []string(nil) // 方式二
    log(2, s)
    
    s = []string{} // 方式三
    log(3, s)
    
    s = make([]string, 0) // 方式四
    log(4, s)
}

func log(i int, s []string) {
    fmt.Printf("%d: empty=%t\tnil=%t\n", i, len(s) == 0, s == nil)
}
// 输出结果
1: empty=true nil=true // 方式一
2: empty=true nil=true // 方式二
3: empty=true nil=false // 方式三
4: empty=true nil=false // 方式四

所有切片的 len 都是0,因此 nil 切片也是空切片。在探究哪种初始化切片之前,需要提示两点:

  • 空切片和 nil 切片的区别在于是否分配地址,初始化一个 nil 切片不会发生地址分配(底层数组)。

  • 无论切片是空还是 nil,内置的 append 方法都可以直接调用。

因此如果需要初始化一个 nil 切片,推荐上述方式一(var s []string);如果需要初始化一个长度为0的空切片,则使用方式四(make([]string, 0))。

当然如果你需要初始化一个已知长度的切片,不仅仅是空切片,也推荐方式四:

func intsToStrings(ints []int) []string {
    // 使用 make([]string, 0, len(ints)) 以及 append 的方式也是可以的
    s := make([]string, len(ints))
    for i, v := range ints {
        s[i] = strconv.Itoa(v)
    }
}

  • 方式二的意义:

s := append([]string(nil), "32")

类似语法糖的用法,可以用一行代码完成切片初始化和添加元素的编写。

  • 方式三使用场景分析:

s := []string{"1", "2", "3"}

如果初始化切片但是不设置初始元素 s := []string{},则不如使用方式一 var s []string 进行初始化。方式三应该用在需要指定初始化值的切片时。

留意空切片(empty but non-nil)和 nil 切片(empty and nil)在一些库中会发生不同处理:

  • encoding/json 库中,针对 marshal 序列化方法,空切片序列化为 [],而 nil 切片序列化为 null。

  • 标准库 reflect.DeepEqual 方法中,比较 nil 和 空切片返回 false。

没有正确检查切片是否为空(#23)

示例代码1:

func handleOperations(id string) {
    operations := getOperations(id)
    if operations != nil {
        handle(operations)
    }
}

func getOperations(id string) []float32 {
    operations := make([]float32, 0)
    
    if if == "" {
        return operations
    }
    // ... 相关逻辑
    
    return operations
}

假设调用 getOperations 得到 []float32 切片后,通过判断它是否为 nil 来决定是否执行 handle 方法,但事实上,getOperations 方法从来都不会返回 nil,因此这种情况下 handle(operations) 一定会触发。

此时有两种修改方式:

  1. 修改被调用方(不推荐):

func getOperations(id string) []float32 {
    operations := make([]float32, 0)
    
    if if == "" {
        return nil // 返回一个 nil 切片
    }
    return operations
}

此时调用方代码中 operations != nil 确实可以生效,但是作为被调用方的函数来说,本身是无法预计所有被调用的场景的,并且什么时候返回 nil,什么时候返回空,不应该通过习惯去约束。

🌟 而应该在在调用方 handleOperations 侧做更通用的判断。

  1. 修改调用方:

func handleOperations(id string) {
    operations := getOperations(id)
    if len(operations) != 0 {
        handle(operations)
    }
}

因为无论切片是 nil 还是空,都会满足 len(operations) != 0 这个条件。

错误的切片拷贝(#24)

错误示例:

src := []int{0, 1, 2}
var dst []int
copy(dst, src)
fmt.Println(dst) // 输出 [] 而不是 [0, 1, 2]

原因在于内置的 copy 函数,拷贝的切片的元素个数等于:min(len(dst), len(src))

修正方案:

src := []int{0, 1, 2}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst) // 输出 [0, 1, 2]

通过 append 方法实现拷贝切片的功能:

src := []int{0, 1, 2}
dst := append([]int(nil), src...)

通过这种方式,将一个切片追加到一个 nil 切片之中,此时 dst 切片的长度和容量都为3。

切片使用 append 的副作用(#25)

示例代码:

s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := append(s2, 10)
fmt.Println(s1, s2, s3) // [1, 2, 10] [2] [2, 10]

image-20240201203622818

当执行完上述第三行代码,s1 切片的第三个元素也发生了修改。

这种情况也发生在将切片作为参数传递给某个函数:

func main() {
    s := []int{1, 2, 3}
    
    f(s[:2])
    fmt.Println(s) // [1, 2, 10]
}

func f(s []int) {
    _ = append(s, 10)
}

🌟 有两种方法可以避免这个问题。

方法一:

func main() {
    s := []int{1, 2, 3}
    sCopy := make([]int, 2)
    copy(sCopy, s)
    
    f(sCopy)
    fmt.Println(s) // [1, 2, 3]
}

在传入切片之前,将其通过 copy 函数拷贝一份,则无论其是否在 f 中被改动,将不会影响 s。

方法二:

func main() {
    s := []int{1, 2, 3}
    f(s[:2:2])
}

切片截取 s[low:high:max] 前两个参数左闭右开控制切片区间,第三个参数控制新切片的容量(max-low)。

image-20240201204656995

由于此时通过 s[:2:2] 创建的切片容量是2,如果在 f 函数内对其进行 append 操作时,由于 len 已经等于 cap,将触发扩容,导致其底层数组将引用一个新的二倍扩容后的数组。

切片和内存泄漏(#26)

🌟 场景一:切片容量泄漏

func consumeMessages() {
    for {
        msg := receiveMessage() // 假设每次msg都是一个长度为1000000的字节切片
        storeMessageType(getMessageType(msg))
    }
}

// 字符切片截取函数,截取前5个字符
func getMessageType(msg []byte) []byte {
    return msg[:5]
}

这个场景不断输入大小为 1M 的字节切片,截取前五个字节存储。如果一共有1000个切片传入,程序运行之后,内存占用将达到1G。

分析原因:

image-20240202102753557

for 循环内,getMessageType() 函数每次在调用之后,虽然 msg 切片变量已经不在被引用,从而被 GC 回收,但是底层的数组没有收到影响。

即 getMessageType() 函数每次截取前5个字符,但是 msg[:5] 切片的 cap 值依旧是1M,Go 语言并不会自动回收其余部分的内存占用。

解决方案:

// 有效方案
func getMessageType(msg []byte) []byte {
    msgType := make([]byte, 5)
    copy(msgType, msg)
    return msgType
}
// 无效方案
func getMessageType(msg []byte) []byte {
    return msg[:5:5]
}

通过 copy 创建新的切片存放5个字节,使得原 msg 以及底层数组解除引用从而在 for 循环后被 GC 回收。但是通过 msg[:5:5] 方式创建切片,虽然限制了索引5之后的位置的访问,但是 Go 语言目前不支持自动回收这部分无法访问的内存。

🌟 场景二:切片和引用

type Foo struct {
	v []byte
}

func main() {
	foos := make([]Foo, 1_000)
	printAlloc()

	for i := 0; i < len(foos); i++ {
		foos[i] = Foo{
			v: make([]byte, 1024*1024),
		}
	}
	printAlloc()

	two := keepFirstTwoElementsOnly(foos)
	runtime.GC()
	printAlloc()
	runtime.KeepAlive(two) // 保持对变量two的引用
}

func keepFirstTwoElementsOnly(foos []Foo) []Foo {
	return foos[:2]
}

func printAlloc() {
	var m runtime.MemStats // 记录内存分配
	runtime.ReadMemStats(&m)
	fmt.Printf("%d KB\n", m.Alloc/1024)
}
// 结果展示
95 KB // 分配了1000个零值的 Foo 结构
1024098 KB // 为长度为1000的 Foo 切片的 v 属性分配内存1024*1024
1024101 KB // 虽然截取前两个元素,但是后998个Foo以及其内部v的内存依旧占用

⚠️ 注意:如果切片的元素是引用类型或者是一个内部有引用类型的结构,在这个元素被回收之前,则这个元素所指向内容将不会被 GC 自动回收。(引用链依旧存在)

解决方案:

// 方式一
func keepFirstTwoElementsOnly(foos []Foo) []Foo {
    res := make([]Foo, 2)
    copy(res, foos)
    return res
}
// 方式二
func keepFirstTwoElementsOnly(foos []Foo) []Foo {
    for i := 2; i < len(foos); i++ {
        foos[i].v = nil
    }
    return foos[:2]
}

方式一:通过上面反复提及的 copy 创建一个新的切片实现赋值,此时新切片 len 和 cap 都是2。原切片 foos 由于不再被引用,则整体全部被 GC 回收,包括每个 Foo 结构的 v 切片。

方式二:通过手动将索引2至999的 Foo 结构的 v 切片手动设置为 nil,此时后998个 Foo 元素的 v 切片底层数组失去引用,会被 GC 回收。与方式一的区别在于,for 循环之后,foos[:2] 新切片 len 为2但是 cap 依旧为1000。

image-20240202112830971

🌟 使用这两种方案自行权衡效率,方案一需要遍历0至i-1的元素,方案二需要遍历i至n-1的元素。

低效的 Map 初始化(#27)

🌟 map 的实现:

image-20240202114039583

map 本质是一个 hash table,以数组的形式组织一系列的 bucket,每个 bucket 固定存放8个键值对,根据 key 的 hash 结果,决定这个 key-value 存放在哪个索引的 bucket 中。

image-20240202114310003

如果相同 hash 值的键值对超过8个,则会创建一个新的 bucket,被前一个 bucket 链式引用,因此最差情况下,查询效率会退化成 O(p),p 等价于这个 bucket 链条中键值对的个数。

🌟 map 的初始化:

mp := map[string]int {
    "1": 1,
    "2", 2,
    "3", 3
}

当逐渐向这个 map 添加 1_000_000 个键值对,达到某些条件时会触发 map 的扩容,因为 map 的设计上不会允许 hash 值相同的 bucket 链无限延长,这失去了 hash table 的效率。

🌟 扩容时机:

  1. 负载因子:当 bucket 的平均容量超过6.5。

  2. 太多的 bucket 溢出(包含超过8个键值对)。

在这两种情况下,map 会触发扩容,增加 hash array 的长度,并重建整个 map,重新整理和平衡各个 bucket 链。这种情况下,会导致绝大多数键值对重新分配,因此简单的一次 insert 操作,性能可能就跌落为 O(N),N 为当前 map 的所有键值对数量。

🌟 高效的初始化:

mp := make(map[string]int, 1_000_000)

与切片的初始化类似,通过指定希望存放的键值对的个数,map 的内置初始化流程会根据输入的容量,创建一个合适大小的 map。这为后续存入 1_000_000 免去了 map 扩容导致的重建开销。

同样的,指定 1_000_000 大小,不意味着这个 map 只能存放这么多键值对,这只是提示给 Go runtime 去分配至少能容纳 1_000_000 键值对的空间。

// 分配1_000_000容量的 banchmarks,性能相差约60%
InitiateMapWithoutSize  6       227413490 ns/op
InitiateMapWithSize     13		 91174193 ns/op

Map 和内存泄漏(#28)

概念:Go 语言的 Map 只能增长大小,并不能自动收缩,即使内部元素被删除。

场景分析:

n := 1_000_000
m := make(map[int][128]byte)
printAlloc()

for i := 0; i < n; i++ {
    m[i] = randBytes() // 获取长度128的字符切片
}
printAlloc()

for i := 0; i < n; i++ {
    delete(m, i)
}

runtime.GC()
printAlloc()
runtime.KeepAlive(m) // 保持对m的引用,避免被回收
// 打印结果展示
0 MB
461 MB
293 MB

第一次打印:由于初始化的是空的切片,因此没有分配内存。

第二次打印:添加了一百万个字符数组。

第三次打印:虽然从 map 中删除了这一百万个字符数组,但是内存占用依旧很大。

🌟 原因分析:

type hmap struct {
    B uint8 // 2^B 个 buckets
    // ...
}

Go 语言的 map 底层实现是一个 hmap 结构,有一个 B 字段存放 map 的 buckets 的个数,这个场景下,存放 1_000_000 个键值对,B == 18,2^18 == 262144 个 buckets。

当 delete 1_000_000 个键值对之后,B 依旧是18,意味着 buckets 没有减少,只是将 bucket 对应的插槽设置为0值。

因此如果用 map 做缓存,当 map 某一时间段扩容到很大情况时,后续访问量下降,这个 map 还是占用很大的内存空间。

🌟 解决方案:

  • 方案一:使用 map 做缓存,则根据时间,定期新创建一个 map,去存放旧 map 的元素,人工去释放旧 map。(缺点在于在下次 GC 触发之前,会占用两倍内存,并且拷贝 map 中元素也需要花费时间,同时需要考虑并发安全)。

  • 方案二:将键值对的值元素使用指针替换:map[int]*[128]byte,这种情况下,bucket 中 value 占用的内存将限制在一个指针的大小(bucket 的插槽变小了),通过 delete 操作删除所有键值对,最后即使 map 的 bucket 数量无法减少,但是占用内存减少比较明显。(因为实际指向的 [128]byte 数组失去了引用,被回收)。

image-20240202133210171

⚠️ 注意:当使用 map 时,如果 key/value 的长度超过 128 bytes,Go 将会默认使用指针存放 bucket 的键值对。

错误的值比较方式(#29)

使用逻辑运算符不可比较的数据类型:

  • 切片

  • map

使用逻辑运算符可比较的数据类型:

  • 布尔型:比较两个 Booleans 是否相等

  • 数值型:比较数值是否相等

  • Strings:比较字符串是否相等

  • Channels:比较两个 channel 是否通过相同的 make 创建,或者是否都是 nil

  • Interfaces:比较两个接口的动态类型和动态值,或者是否都是 nil

  • Pointers:比较两个指针指向的内存中的 value 是否相等,或者是否都是 nil

  • 结构体和数组:整合上述可比较的数据类型,依次比较

针对不可使用逻辑运算符比较的数据类型,可以使用 Go 的反射去实现运行时的比较(递归),使用前建议阅读文档:

cust1 := customer{id: "x", operations: []float64{1.}}
cust2 := customer{id: "x", operations: []float64{1.}}
fmt.Println(reflect.DeepEqual(cust1, cust2)) // true

此时即使结构体存在不可比较的切片类型,依旧可以打印出 true。

🌟 使用反射比较需要注意的点:

  1. 集合为空和集合为 nil 是不同的概念(这在 #22 中提到了),需要留意。

  2. 反射是在运行时确定值的,因此性能很差,通常来说比 == 差两个数量级(100倍)。因此反射可以使用在单元测试中,而不是程序运行时。

自定义 compare 方法代替 reflect.DeepEqual()

func (a customer) equal(b customer) bool {
    if a.id != b.id {
        return false
    }
    if len(a.operations) != len(b.operations) {
        return false
    }
    for i := 0; i < len(a.operations); i++ {
        if a.operations[i] != b.operations[i] {
            return false
        }
    }
    return true
}

经过 benchmark 测试,使用自定义的 equal 方法比较两个切片是否相等,比使用反射快96倍。

📒 提示:针对数据类型的比较,可以选择开源的第三方的库。

文章转载自:白泽talk

原文链接:https://www.cnblogs.com/YLTFY1998/p/18003138

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

在线视频格式转换,就是这么简单!(免费)

随着数字化时代的发展&#xff0c;我们在日常生活中越来越频繁地与各种视频文件打交道。然而&#xff0c;不同设备和平台对于视频格式的支持可能存在差异&#xff0c;这就导致了我们有时需要进行视频格式的转换&#xff0c;以确保视频在各种环境中流畅播放。而幸运的是&#xf…

简单使用阿里云OSS对象存储

首先我们先去阿里云控制台开通oss对象存储&#xff08;阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台&#xff09; 这篇文件是借鉴至&#xff08;教你三分钟上手阿里云OOS上传操作_阿里云定时上传怎么使用-CSDN博客&#xff09;的&#xff0c;源码也给了&a…

Python代码混淆工具,Python源代码保密、加密、混

引言 Python作为一种高级脚本语言&#xff0c;便捷的语法和丰富的库使它成为众多开发者的首选。然而&#xff0c;有时候我们希望保护我们的Python源代码&#xff0c;避免被他人轻易获取和篡改。为了实现这一目标&#xff0c;我们可以采取代码混淆的技术手段。本文将介绍Python…

深度学习系列55:深度学习加速技术概述

总体有两个方向&#xff1a;模型优化 / 框架优化 1. 模型优化 1.1 量化 最常见的量化方法为线性量化&#xff0c;权重从float32量化为int8&#xff0c;将输入数据映射在[-128,127]的范围内。在 nvdia gpu&#xff0c;x86、arm 和 部分 AI 芯片平台上&#xff0c;均支持 8bit…

Vue中keep-alive的作用、原理及应用场景

在进行Vue开发的过程中&#xff0c;我们经常会遇到需要进行组件缓存的场景&#xff0c;这时候Vue提供的keep-alive组件就派上了用场。keep-alive组件是Vue内置的一个抽象组件&#xff0c;它可以将其包裹的组件进行缓存&#xff0c;提高组件的性能&#xff0c;同时也可以节省服务…

docker-学习-3

docker 学习第三天 docker 学习第三天1. 回顾一下1.1. 对比图1.2. docker和虚拟机的区别1.3. 在容器化部署中&#xff0c;为什么有些场景更适合选择Docker而非虚拟机&#xff1f;1.4. 有哪些场景适合选择虚拟机而不是Docker进行部署&#xff1f;1.5. 虚拟机和Docker在性能和资源…

Python 数据分析(PYDA)第三版(二)

原文&#xff1a;wesmckinney.com/book/ 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 四、NumPy 基础知识&#xff1a;数组和向量化计算 原文&#xff1a;wesmckinney.com/book/numpy-basics 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 此开放访问网络版…

Java tomcat 使用spring-task,实现定时任务功能

前言 今天接触到一个需求&#xff0c;需要添加一个定时任务功能&#xff0c;第一反应是启动类EnableScheduling、定时任务方法使用Scheduled实现&#xff0c;导入项目后才发现&#xff0c;这个项目是ssm整合框架的tomcat项目&#xff0c;没有启动类&#xff0c; 于是改变了思路…

protoc结合go完成protocol buffers协议的序列化与反序列化

下载protoc编译器 下载 https://github.com/protocolbuffers/protobuf/releases ps: 根据平台选择需要的编译器&#xff0c;这里选择windows 解压 加入环境变量 安装go专用protoc生成器 https://blog.csdn.net/qq_36940806/article/details/135017748?spm1001.2014.3001.…

知识融合前沿技术:构建多模态、公平高效的大规模知识表示

目录 前言1 无监督对齐&#xff1a;构建智能实体关联2 多视角嵌入&#xff1a;提高数据利用效率3 嵌入表示增强&#xff1a;挑战节点相似性&#xff0c;对抗训练解决4 大规模实体对齐&#xff1a;克服模糊性和异构性结论 前言 在信息时代&#xff0c;知识融合成为推动人工智能…

AI新工具(20240204)pot-desktop - 为用户提供便捷的文字翻译和识别功能;ChatALL - 能够同时向多个AI机器人发送提示

pot-desktop - 为用户提供便捷的文字翻译和识别功能 pot-desktop pot-desktop是一款备受欢迎的跨平台划词翻译和OCR软件&#xff0c;为用户提供便捷的文字翻译和识别功能。 功能点&#xff1a; 划词翻译&#xff1a;用户只需将鼠标光标悬停在需要翻译的文字上&#xff0c;po…

Docker 可视化工具

1、Portainer 概念介绍 Portainer是一款轻量级的应用&#xff0c;它提供了图形化界面&#xff0c;用于方便地管理Docker环境&#xff0c;包括单机环境和集群环境。 Portainer分为开源社区版&#xff08;CE版&#xff09;和商用版&#xff08;BE版/EE版&#xff09;。 Porta…

Python实现加密

目录 一&#xff1a;哈希加密 二&#xff1a;aes加密 三&#xff1a;rsa加密 Python中&#xff0c;你可以使用多种方法来实现加密。下面我们介绍下常用的加密方法。 一&#xff1a;哈希加密 下面是一个使用Python内置的hashlib库实现SHA256哈希加密的例子&#xff1a; im…

在vscode上传项目到gitee

一、在Gitee上新建一个仓库 Tip&#xff1a;若已经创建过了&#xff0c;直接跳到第二部分看VsCode如何上传代码到Gitee 创建仓库比较简单&#xff0c;下面两张图就是整个过程&#xff0c;这里不在赘述&#xff0c;具体如下&#xff1a; 二、VsCode连接Gitee上创建的仓…

DevExpress ASP.NET Web Forms v23.2最新版本系统环境配置要求

本文档包含有关安装和使用 DevExpress ASP.NET Web Forms控件的系统要求的信息。 点击获取DevExpress v23.2正式版(Q技术交流&#xff1a;909157416&#xff09; .NET Framework DevExpress ASP.NET Web Forms控件和MVC扩展支持以下.NET Framework 版本。 如果您需要 DevExp…

电脑如何录制屏幕视频?超简单的教程来了!

在当今信息化的时代&#xff0c;电脑已经成为了我们日常生活和工作中不可或缺的工具。除了常规的文字处理、数据分析等功能外&#xff0c;电脑还为我们提供了许多实用的小工具&#xff0c;其中屏幕录制便是其中的一项。本文将介绍电脑如何录制屏幕视频&#xff0c;并推荐两种常…

Python基础知识:Python数据行列处理

我们在应用Python开展机器学习或统计分析时&#xff0c;经常需要对数据行列进行处理&#xff0c;下面介绍几种常用的Python数据行列处理操作。 我们结合数据2.2文件&#xff0c;以示例的方式讲解删除变量列、样本行的操作。输入以下代码并逐行运行&#xff1a; datapd.read_c…

计算机网络-差错控制(奇偶校验码 CRC循环冗余码)

文章目录 差错从何而来从传感器层面提高信道比来减少线路本身的随机噪声的一个例子热噪声和冲击噪声 数据链路层的差错控制检错编码-奇偶校验码检错编码-CRC循环冗余码例子注意 差错从何而来 噪声通常指的是任何未预期的、随机的信号干扰&#xff0c;这些干扰可能源自多种物理…

【iOS ARKit】3D 人体姿态估计

与基于屏幕空间的 2D人体姿态估计不同&#xff0c;3D人体姿态估计是尝试还原人体在三维世界中的形状与姿态&#xff0c;包括深度信息。绝大多数的现有3D人体姿态估计方法依赖2D人体姿态估计&#xff0c;通过获取 2D人体姿态后再构建神经网络算法&#xff0c;实现从 2D 到 3D人体…

上线GPT应用的流程

上线一个应用是一个逐步迭代的过程&#xff0c;不断根据用户反馈和市场需求进行改进和优化。上线一个基于GPT的应用通常需要以下步骤&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.明确目标和用途…