10分钟了解Golang泛型

泛型是Golang在1.18版本引入的强大工具,能够帮助我们在合适的场合实现简洁、可读、可维护的代码。原文: Go Generics: Everything You Need To Know

alt
导言

可能有人会觉得Go泛型很难,因此想要借鉴其他语言(比如Java、NodeJS)的泛型实践。事实上Go泛型很容易学,本文希望能帮助读者更好的理解Go泛型。

👉注:本文不会将 Go 泛型与其他语言的泛型实现进行比较,但会帮助你理解 Go 泛型元素背后的上下文、结构及其原理。

前置条件

要编写本文中的示例代码,需要:

  • 在计算机上安装 Go 1.18+
  • 对Golang结构、类型、函数和方法有最低限度的了解
概述

在 2020 年之前,Go泛型既是风险也是机遇。

当 Go 泛型在 2009 年左右被首次提出时(当时该编程语言已经公开),该特性是 Go 语言的主要弱点之一(Go 团队调查发现)。

此后,Go 团队在 Go 草案设计中接受了许多泛型实现,并在 Go 1.18 版本中首次引入了泛型。

Go 博客 2020 调查结果
Go 博客 2020 调查结果

Go 2020 调查显示,自 Go 语言诞生以来,Go 社区一直要求引入泛型功能。

Go 开发人员(以及 Go 团队成员)看到这一缺陷阻碍了 Go 语言的发展,同时,如果得到修复,Go将具有更大的灵活性和性能。

什么是程序设计中的泛型?

根据维基百科[1]的解释,泛型编程是一种计算机编程风格,在这种编程风格中,算法的具体类型可以在以后指定。

简单解释一下:泛型是一种可以与多种类型结合使用的类型,泛型函数是一种可以与多种类型结合使用的函数。

☝️ 简单提一下:尽管"泛型"在过去和现在都可以通过 interface{}、反射包或代码生成器在 Go 中实现,但还是要提一下在使用这三种方法之前需要仔细考虑。

为了帮助我们以实用的方式理解和学习 Go 泛型,我们将在本文稍后部分提供示例代码。

但要知道,既然 Go 泛型已经可用,就可以消除模板代码,不必担心向后兼容问题,同时还能编写可重用、类型安全和可维护的代码。

那么......为什么需要 Go 泛型?

简而言之,最多可提高 20% 性能。

根据 Go 博客的描述,Go 泛型为 Go 语言增加了三个主要组件:

  • 函数和类型的类型参数。
  • 将接口类型定义为类型集,包括没有方法的类型。
  • 类型推导,允许在调用函数时省略类型参数。

在 Go 1.18 之前没有这种功能吗?

从技术上讲,早在 Go 泛型发布之前,Go 就有一些处理"泛型"的方法:

  • 使用"泛型"代码生成器生成 Go 软件包,如 https://github.com/cheekybits/genny [2]
  • 使用带有 switch语句和类型转换的接口
  • 使用带有参数验证的反射软件包

然而,与正式的Go泛型相比,这些方法还远远不够,有如下缺点:

  • 使用类型 switch和转换时性能较低
  • 类型安全损耗:接口和反射不是类型安全的,这意味着代码可能会传递任何类型,而这些类型在编译过程中会被忽略,从而在运行时引起 panic
  • Go 项目构建更复杂,编译时间更长
  • 可能需要对调用代码和函数代码进行类型断言
  • 缺乏对自定义派生类型的支持
  • 代码可读性差(使用反射时更明显)

👉注:上述观点并不意味着在 Go 编程中使用接口或反射包不好;它们还有其他用途,应该在合适的场景下应用。

巧合的是,上述几点 ☝️ 使 Go 泛型适合处理目前在 Go 中的泛型实现,因为:

  • 类型安全 ( 运行时不会丢失类型,也不需要类型验证、切换或转换
  • 高性能
  • Go IDE 的支持
  • 向后兼容 ( 使用 Go 1.18+ 重构后,旧版代码仍可运行
  • 对自定义数据类型的高度支持
入门:使用 Go 泛型

在开始重构之前,我们借助一个迷你 Go 程序来了解 Go 泛型使用的一些术语和逻辑。

作为实操案例,我们将首先在不使用 Go 泛型的情况下解决 Leetcode 问题。然后,随着我们对这一主题的了解加深,我们将使用 Go 泛型对其进行重构。

alt

Leetcode 问题

有几家公司在技术面试时都问过这个问题,我们对措辞稍作改动,但逻辑不变。Leetcode 链接为:https://leetcode.com/problems/contains-duplicate[3]

📌问题:给定一个整型(int 或 in32 或 int64)数组 nums,如果任何值在数组中至少出现两次,则返回 true;如果每个元素都不同,则返回 false

现在,我们在不使用 Go 泛型的情况下解决这个问题。

进入开发目录,创建一个新的 Go 项目目录,名称不限。我将其命名为 leetcode1。然后将目录更改为新创建的项目目录。

按照惯例,我们在终端的项目根目录下运行 go mod init github.com/username/leetcode1,为项目创建一个 Go 模块。

❗️ 记住:不要忘记将username替换为你自己的 Github 用户名

接下来,创建 leetcode.go 文件并将下面的代码复制进去:

package main

import "fmt"

type FilterInt map[int]bool
type FilterInt32 map[int32]bool
type FilterInt64 map[int64]bool

func main() {
 data := []int{134458732}     // sample array
 data32 := []int32{134458732// sample array
 data64 := []int64{134458732// sample array
 fmt.Printf("Duplicate found %t\n", FindDuplicateInt(data))
 fmt.Printf("Duplicate found %t\n", FindDuplicateInt32(data32))
 fmt.Printf("Duplicate found %t\n", FindDuplicateInt64(data64))
}

func FindDuplicateInt(data []int) bool {
 inArray := FilterInt{}
 for _, datum := range data {
  if inArray.has(datum) {
   return true
  }
  inArray.add(datum)
 }
 return false
}

func FindDuplicateInt32(data []int32) bool {
 inArray := FilterInt32{}
 for _, datum := range data {
  if inArray.has(datum) {
   return true
  }
  inArray.add(datum)
 }
 return false
}

func FindDuplicateInt64(data []int64) bool {
 inArray := FilterInt64{}
 for _, datum := range data {
  if inArray.has(datum) {
   return true
  }
  inArray.add(datum)
 }
 return false
}

func (r FilterInt) add(datum int) {
 r[datum] = true
}

func (r FilterInt32) add(datum int32) {
 r[datum] = true
}

func (r FilterInt64) add(datum int64) {
 r[datum] = true
}

func (r FilterInt) has(datum int) bool {
 _, ok := r[datum]
 return ok
}

func (r FilterInt32) has(datum int32) bool {
 _, ok := r[datum]
 return ok
}

func (r FilterInt64) has(datum int64) bool {
 _, ok := r[datum]
 return ok
}

再看一下 Leetcode 的问题,程序应该检查输入的数组(可以是 INT、INT32 或 INT64),并找出是否有重复数据,如果有则返回 true,否则返回 false,上面这段代码就是完成这个任务的。

在第 10、11 和 12 行,分别提供了 intint32int64 类型数据的示例数组。

在第 5、6 和 7 行,分别创建了关键字类型为 intint32int64map 类型 FilterIntFilterInt32FilterInt64

所有类型 map 的值都是布尔值,所有类型都有相同的 hasadd 方法。从本质上讲,add 方法将接受 datum 参数,并在 map 中创建值为 true 的键。根据 map 是否包含作为 datum 传入的键,has 方法将返回 truefalse

现在,第 18 行的函数 FindDuplicateInt、第 29 行的函数 FindDuplicateInt32 和第 40 行的函数 FindDuplicateInt64 实现了相同的逻辑,即验证所提供的数据中是否存在重复数据,如果发现重复数据,则返回 true,否则返回 false

看看这些重复代码。

有没有让你感到恶心🤕?

总之,如果我们在终端运行项目根目录下的 go run leetcode.go,就会编译成功并运行。输出结果应该与此类似:

Duplicate found true
Duplicate found true
Duplicate found true

如果我们要查找 float32float64 或字符串的重复内容,该怎么办?

我们可以为每种类型编写一个实现,为不同类型明确编写多个函数,或者使用接口,或者通过包生成"泛型"代码。这就是"泛型"诞生的过程。

通过泛型,我们可以编写泛型函数来替代多个函数,或使用带有类型转换的接口。

接下来我们用泛型来重构代码,但首先需要熟悉一些术语和概念。

泛型基础知识
1.类型参数
类型参数的可视化表示
类型参数的可视化表示

上图描述的是泛型函数 FindDuplicateT 是类型参数,any 是类型参数的约束条件(接下来将讨论约束条件)。

类型参数就像一个抽象的数据层,通常用紧跟函数或类型名称的方括号中的大写字母(多为字母 T)来表示。下面是一些例子:

...
// map type with type parameter T and constraint comparable
type Filter[T comparable] map[T]bool
...

...
// Function FindDuplicate with type parameter T and constraint any
func FindDuplicate[T any](data T) bool {
// find duplicate code
}
...
2.类型推导

泛型函数必须了解其支持的数据类型,才能正常运行。

🎯要点:泛型类型参数的约束条件是在编译时由调用代码确定的代表单一类型的一组类型。

进一步来说,类型参数的约束代表了一系列可允许的类型,但在编译时,类型参数只代表一种类型,因为 Go 是一种强类型的静态检查语言。

❗️提醒:由于 Go 是一种强类型的静态语言,因此会在应用程序编译期间而非运行时检查类型。Go 泛型解决了这个问题。

类型由调用代码类型推导提供,如果泛型类型参数的约束条件不允许使用该类型,代码将无法编译。

符合参数约束的类型
符合参数约束的类型

由于类型是通过约束知道的,因此在大多数情况下,编译器可以在编译时推断出参数类型。

通过类型推导,可以避免从调用代码中为泛型函数或泛型类型实例化进行人工类型推导。

👉注意:如果编译器无法推断类型(即类型推导失败),可以在实例化时或在调用代码中手动指定类型。

下面是 FindDuplicate 泛型函数的一个很好的示例:

FindDuplicate 泛型函数示例
FindDuplicate 泛型函数示例

我们可以忽略调用代码中的 [int],因为编译器会推断出[int],但我更倾向于加入[int]以提高代码的可读性。

3.约束

在引入泛型之前,Go 接口用于定义方法集。然而,随着泛型约束的引入,接口现在既可以定义类型集,也可以定义方法集。

约束是用于指定允许使用的泛型的接口,在上述 FindDuplicate 函数中使用了 any 约束。

❗️Pro 提示:除非必要,否则避免使用 any 接口约束。

在底层实现上,any关键字只是一个空接口,这意味着可以用 interface{} 替换,编译时不会出现任何错误。

Go 泛型中约束的可视化表示
Go 泛型中约束的可视化表示

上述接口约束允许使用 intint16int32int64 类型。这些类型是约束联合体,用管道符 | 分隔类型。

约束在以下几个方面有好处:

  • 通过类型参数定义了一组允许的类型
  • 明确发现泛型函数的误用
  • 提高代码可读性
  • 有助于编写更具可维护性、可重用性和可测试性的代码

☝️ 简单提一下:使用约束时有一个小问题

请看下面的代码:

package main

import "fmt"

type CustomType int16

func main() {
 var value CustomType
 value = 2
 printValue(value)
}

func printValue[T int16](value T) {
 fmt.Printf("Value %d", value)
}

在上面的代码中,第 5 行定义了一个名为 CustomType 的自定义类型,其基础类型为 int16

在第 8 行,声明了一个以 CustomType 为类型的变量,并在第 9 行为其赋值。

然后,在第 10 行调用带有值的 printValue 泛型函数。

...🤔

...🤔

你认为代码可以编译运行吗?

如果我们在终端执行 go run custom-generics.go,就会出现这样的错误。

./custom-type-generics.go:10:12: CustomType does not implement int16 (possibly missing ~ for int16 in constraint int16)

尽管自定义类型 CustomTypeint16 类型,但 printValue 泛型函数的类型参数约束无法识别。

鉴于函数约束不允许使用该类型,这也是合理的。不过,可以修改 printValue 函数,使其接受我们的自定义类型。

现在,更新 printValue 函数如下:

func printValue[T int16 | CustomType](value T) { 
    fmt.Println(value)
}

使用管道操作符,我们将自定义类型 CustomType 添加到 printValue 泛型函数类型参数的约束中,现在有了一个联合约束。

如果我们再次运行该程序,编译和运行都不会出现任何错误。

但是,等等!为什么需要 int16 类型和"int16"类型的约束联合?

alt

我们将在下一节介绍波浪线 ~ 运算符。

4.波浪线(Tilde)运算符和基础类型

幸运的是,Go 1.18 通过波浪线运算符引入了底层类型,波浪线运算符允许约束支持底层类型。

在上一步代码示例中,CustomType 类型的底层类型是 int16。现在,我们使用 ~ 波浪线更新 printValue 泛型函数类型参数的约束,如下所示:

func printValue[T ~int16](value T) { 
    fmt.Println(value)
}

新代码应该是这样的:

package main

import "fmt"

type CustomType int16

func main() {
 var value CustomType
 value = 2
 printValue(value)
}

func printValue[T ~int16](value T) {
 fmt.Printf("Value %d", value)
}

再次运行程序,应该可以成功编译和运行。我们删除了约束联合,并在约束中的 int16 类型前用 ~ 波浪线运算符替换了 CustomType

编译器现在可以理解,CustomType 类型之所以可以使用,仅仅是因为它的底层类型是 int16

💡 简单来说,~ 告诉约束接受任何 int16 类型以及任何以 int16 作为底层类型的类型。

下面是一个泛型约束接口示例,它也允许函数声明:

type Number interface {
  int | float32 | float64
  IsEven() bool 
}

不过,下一步还有更多东西要学。

5.预定义约束

Go 团队非常慷慨的为我们提供了一个常用约束的预定义包,可在 golang.org/x/exp/constraints[4] 找到。

以下是预定义约束包中包含的约束示例:

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

type Unsigned interface {
 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Integer interface {
 Signed | Unsigned
}

type Float interface {
 ~float32 | ~float64
}

type Ordered interface {
 Integer | Float | ~string
}

因此,我们可以更新之前示例中的 printValue 泛型函数,使其接受所有整数,具体方法如下。

func printValue[T Integer](value T) { 
    fmt.Println(value)
}

❗️ 记住:不要忘记导入预定义约束包 golang.org/x/exp/constraints。

重构 Leetcode 示例

现在我们对泛型有了一些了解,接下来重构 FindDuplicate 程序,通过泛型在整数、浮点数和字符串类型的切片及其底层类型中查找是否有重复数据。

具体修改为:

  • 创建允许使用整数、浮点和字符串及其底层类型的接口约束
  • 使用 go get 将约束包下载到项目中,在终端的 Leetcode 根目录中执行如下指令:
go get -u golang.org/x/exp/constraints
  • 添加到项目中后,在主函数上方创建名为 AllowedData 的约束,如下所示:
type AllowedData interface {
   constraints.Ordered
}

constraints.Ordered 是一种约束,允许任何使用支持比较运算符(如 ≤=≥===)的有序类型。

👉注:可以在泛型函数中使用 constraint.Ordered,而无需创建新的接口约束。不过,为了便于学习,我们还是创建了自己的约束 AllowData

  • 接下来,删除类型 map 中的所有 FilterIntX 类型,创建一个名为 Filter 的新类型,如下所示,该类型以 T 为类型参数,以 AllowedData 为约束条件:
type Filter[T AllowedData] map[T]bool

在泛型类型 Filter 前面,声明了 T 类型参数,并指定 map 键只接受类型参数的约束 AllowedData 作为键类型。

  • 现在,删除所有 FindDuplicateIntX 函数。然后使用 Go 泛型创建一个新的 FindDuplicate 函数,代码如下:
func FindDuplicate[T AllowedData](data []T) bool {
   inArray := Filter[T]{}
   for _, datum := range data {
      if inArray.has(datum) {
         return true
      }
      inArray.add(datum)
   }
   return false
}

FindDuplicate 函数是一个泛型函数,添加了类型参数 T,并在函数名后面的方括号中指定了 AllowedData 约束,然后用类型参数 T 定义了切片类型的函数参数,并用类型参数 T 初始化了 inArray

👉注:在函数中声明泛型参数时使用方括号。

  • 接下来,更新 has 以及 add 方法,如下所示。
func (r Filter[T]) add(datum T) {
   r[datum] = true
}

func (r Filter[T]) has(datum T) bool {
   _, ok := r[datum]
   return ok
}

因为我们在定义类型 Filter 时已经声明了约束,因此方法中只包含类型参数。

最后,更新调用 FindDuplicateIntX 的调用代码,使用新的泛型函数 FindDuplicate,最终代码如下:

package main

import (
 "errors"
 "fmt"
 "golang.org/x/exp/constraints"
)

type Filter[T AllowedData] map[T]bool

type AllowedData interface {
 constraints.Ordered
}

func main() {
 data := []int{134458732}     // sample array
 data32 := []int32{134458732// sample array
 data64 := []int64{134458732// sample array
 fmt.Printf("Duplicate found %t\n", FindDuplicate(data))
 fmt.Printf("Duplicate found %t\n", FindDuplicate(data32))
 fmt.Printf("Duplicate found %t\n", FindDuplicate(data64))

}

func (r Filter[T]) add(datum T) {
 r[datum] = true
}

func (r Filter[T]) has(datum T) bool {
 _, ok := r[datum]
 return ok
}

func FindDuplicate[T AllowedData](data []T) bool {
 inArray := Filter[T]{}
 for _, datum := range data {
  if inArray.has(datum) {
   return true
  }
  inArray.add(datum)
 }
 return false
}

现在执行 go run main.go,程序成功编译并运行,预期输出为:

Duplicate found true
Duplicate found true
Duplicate found true

我们成功重构了代码,却没有犯复制粘贴的错误。

6.可比较(comparable)约束

可比较约束与相等运算符(即 == 和≠)相关联。

这是在 Go 1.18 中引入的一个接口,由结构体、指针、接口、管道等类似类型实现。

👉注:Comparable 不用作任何变量的类型。

func Sort[K comparableT Data](values map[K]T) error {
 for k, t := range values {
  // code
 }
 return nil
}
7.约束类型链和类型推导
  • 类型链

允许一个已定义的类型参数与另一个类型参数复合的做法被称为类型链。当在泛型结构或函数中定义辅助类型时,这种方法就派上用场了。

示例:

类型链示例
类型链示例
  • 约束类型推导

前面我们详细介绍了类型推导,但与类型链无关,可以如下调用上图中的函数:

c := Example(2)

由于 ~T 是类型参数 T 与任意约束条件的复合体,因此在调用 Example 函数时可以推断出类型参数 U

👉注:2 是整数,是 T 的底层类型。

8.多类型参数和约束

Go 泛型支持多类型参数,但有一个问题,我们看下面的另一个例子:

package main

import "fmt"

func main() {
 printValues(123"c")
}

func printValues[AB anyC comparable](a, a1 A, b B, c C) {
 fmt.Println(a, a1, b, c)
}

如果编译并成功运行,预期输出结果将是:

1 2 3 c

在函数方括号[]中,我们添加了多个类型参数。类型参数 AB 共享同一个约束条件。在函数括号中,参数 aa1 共享同一个类型参数 any 约束条件。

现在更新主函数,如下所示。

...
func main() {
 printValues(12.13"c")
}
...

发生了什么?

我们将 2 的值从 2 改为 2.1,如你所知,这会将 2 的数据类型从 int 改为 float。当我们再次运行程序时,编译失败:

/main.go:6:14: default type float64 of 2.1 does not match inferred type int for A

等等!我们到底有没有声明 int 类型?

原因就在这里--在编译过程中,编译器会根据函数括号中的类型参数约束进行推断。可以看到,aa1 共享同一个类型参数 A,约束条件是 any(允许所有类型)。

编译器会根据调用代码的变量类型进行推断,并在编译过程中使用函数括号中的类型参数约束来检查类型。

可以看到,aa1 具有相同的类型参数 A,并带有 any 约束。因此,aa1 必须具有相同的类型,因为它们在用于类型推导的函数括号中共享相同的类型参数。

尽管类型参数 AB 共享同一个约束条件,但 b 在函数括号中是独立的。

何时使用(或不使用)泛型

总之,请记住一点--大多数用例并不需要 Go 泛型。不过,知道什么时候需要也很有帮助,因为这样可以大大提高工作效率。

这里有一些指导原则:

何时使用 Go 泛型
  • 替换多个类型执行相同逻辑的重复代码,或者替换处理切片、映射和管道等多个类型的重复代码
  • 在处理容器型数据结构(如链表、树和堆)时
  • 当代码逻辑需要对多种类型进行排序、比较和/或打印时
何时不使用 Go 泛型
  • 当 Go 泛型会让代码变得更复杂时
  • 当指定函数参数类型时
  • 当有可能滥用 Go 泛型时。避免使用 Go 泛型/类型参数,除非确定有使用多种类型的重复逻辑
  • 当不同类型的实现不同时
  • 使用 io.Reader 等读取器时
局限性

目前,匿名函数和闭包不支持类型参数。

Go 泛型的测试

由于 Go 泛型支持编写多种类型的泛型代码,测试用例将与函数支持的类型数量成正比增长。

结论

本文介绍了 Go 中的泛型、与之相关的新术语,以及如何在类型、函数、方法和结构体中使用泛型。

希望能对大家的学习 Go 有所帮助,但请不要滥用 Go 泛型。

收获
  • 如果使用得当,Go 泛型的功能会非常强大;但要谨慎,因为能力越大,责任越大。
  • Go 泛型将提高代码的灵活性和可重用性,同时保持向后兼容,从而为 Go 语言增添价值。
  • 它简单易用,直接明了,学习周期短,练习有助于更好的理解 Go 泛型及其局限性。
  • 过度使用、借用其他语言的泛型实现以及误解会导致 Go 社区出现反模式和复杂性,风险自担。

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

参考资料
[1]

泛型 - 维基百科: https://en.wikipedia.org/wiki/Generic_programming#:~:text=Generic%20programming%20is%20a%20style,specific%20types%20provided%20as%20parameters.

[2]

genny: https://github.com/cheekybits/genny,

[3]

Leetcode: contains duplicate: https://leetcode.com/problems/contains-duplicate/

[4]

golang.org/x/exp/constraints: https://golang.org/x/exp/constraints

本文由 mdnice 多平台发布

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

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

相关文章

LangChain:大模型框架的深度解析与应用探索

在数字化的时代浪潮中,人工智能技术正以前所未有的速度蓬勃发展,而大模型作为其中的翘楚,以生成式对话技术逐渐成为推动行业乃至整个社会进步的核心力量。再往近一点来说,在公司,不少产品都戴上了人工智能的帽子&#…

什么是读写分离?

读写分离将读操作和写操作分别分配给不同的数据库实例,以提高系统的吞吐量和性能。 一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据…

用大于meilisearch-java-0.7.0.jar的报错的解决

Elasticsearch 做为老牌搜索引擎,功能基本满足,但复杂,重量级,适合大数据量。 MeiliSearch 设计目标针对数据在 500GB 左右的搜索需求,极快,单文件,超轻量。 所以,对于中小型项目来说…

面试经典算法系列之数组/字符串3 -- 移除元素

面试经典算法题35-移除元素 LeetCode.27 公众号:阿Q技术站 问题描述 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空…

手把手微调大模型【附:一镜到底视频教程】

前言 近期有很多小伙伴来问是否有大模型微调教程,其实目前网上有很多教程,但是据了解,由于网上教程质量参差不齐,导致很多小伙伴尤其是初学者,一坑未出又入一坑,有种从入门到放弃的感觉。于是乎&#xff0…

信息检索(36):ConTextual Masked Auto-Encoder for Dense Passage Retrieval

ConTextual Masked Auto-Encoder for Dense Passage Retrieval 标题摘要1 引言2 相关工作3 方法3.1 初步:屏蔽自动编码3.2 CoT-MAE:上下文屏蔽自动编码器3.3 密集通道检索的微调 4 实验4.1 预训练4.2 微调4.3 主要结果 5 分析5.1 与蒸馏检索器的比较5.2 …

【0003day】VOSviewer分析

这个软件也可以用知网,也可以用web of science。 首先,需要创建数据。这个数据如何创建,需要参考对应的教程。(本文以web of science为平台来做分析。) 首先,创建对应的数据库。 一直下一步 让后选择完…

哈希表(unordered_set、unordered_map)

文章目录 一、unordered_set、unordered_map的介绍二、哈希表的建立方法2.1闭散列2.2开散列(哈希桶/拉链法) 三、闭散列代码(除留余数法)四、开散列代码(拉链法/哈希桶) 一、unordered_set、unordered_map的…

【GO】go语言中的HTTP标准库 - http编程

上一节已经学习了HTTP的基础知识,本章将学习关于go语言的HTTP编程,最重要的是掌握 net/http 包的用法,以及如何自己编写一个简单的Web服务端,通过客户端访问Server端等。 编写简单的Web 服务器 http.ListenAndServe 启动 Http S…

maven deploy项目发布到中央仓库签名失败signing failed: No secret key

maven deploy项目发布到中央仓库签名失败signing failed: No secret key 执行操作 在我执行命令打包项目到中央仓库时失败 mvn clean deploy错误信息 [INFO] --- gpg:3.1.0:sign (sign-artifacts) LocalCache --- [INFO] Signing 4 files with 9961AA14xxxxxxxxxxxxxxD064…

JVM 类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.class 对…

【Unity 鼠标输入检测】

Unity 鼠标输入检测 Unity提供了多种方法来检测和处理鼠标输入,允许开发者在游戏中实现对鼠标移动、点击和滚轮滚动的响应。以下是一些基本的鼠标输入检测方法: 1. Input.mousePosition 这个属性返回当前鼠标指针的屏幕坐标。坐标是以像素为单位的&…

信息系统项目管理师0102:可行性研究的内容(7项目立项管理—7.2项目可行性研究—7.2.1可行性研究的内容)

点击查看专栏目录 文章目录 7.2项目可行性研究7.2.1可行性研究的内容1.技术可行性分析2.经济可行性分析3.社会效益可行性分析4.运行环境可行性分析5.其他方面的可行性分析记忆要点总结7.2项目可行性研究 可行性研究是在项目建议书被批准后,从技术、经济、社会和人员等方面的条…

【OceanBase诊断调优】—— 租户资源统计项及其查询方法

本文主要介绍 OceanBase 数据库中租户资源统计项及其查询方法。 适用版本 OceanBase 数据库 V4.1.x、V4.2.x 版本。 CPU 资源统计项 逻辑 CPU 使用率(线程处理请求的时间占比)。 通过虚拟表 __all_virtual_sysstat 在 SYS 系统租户下,查看…

【免费Java系列】大家好 ,今天是学习面向对象高级的第十二天点赞收藏关注,持续更新作品 !

这是java进阶课面向对象第一天的课程可以坐传送去学习http://t.csdnimg.cn/Lq3io day10-多线程 一、多线程常用方法 下面我们演示一下getName()、setName(String name)、currentThread()、sleep(long time)这些方法的使用效果。 public class MyThread extends Thread{publi…

AI办公自动化-用kimi批量重命名Word文档

文件夹里面有很多个word文档,标题里面都含有零代码编程,现在想将其替换为AI办公自动化。 在kimichat中输入提示词: 你是一个Python编程专家,要完成一个编写Python脚本的任务,具体步骤如下: 打开文件夹&am…

Kafka和Spark Streaming的组合使用学习笔记(Spark 3.5.1)

一、安装Kafka 1.执行以下命令完成Kafka的安装: cd ~ //默认压缩包放在根目录 sudo tar -zxf kafka_2.12-2.6.0.tgz -C /usr/local cd /usr/local sudo mv kafka_2.12-2.6.0 kafka-2.6.0 sudo chown -R qiangzi ./kafka-2.6.0 二、启动Kafaka 1.首先需要启动K…

Github上 5 个好玩儿的开源项目

1. 在你的 Windows 养小猫 2. 把你的图片生成 ASCII 3. 中国制霸生成器 4. 像素风格代码字体 5. 梦回 QQ 空间 01 在你的 Windows 养小猫 在MacBook的触摸板上,你可以抚养一只小宠物,并与它互动、喂食,这样非常有趣。 我向你推荐了一个…

【Qt 学习笔记】Qt常用控件 | 容器类控件 | Group Box的使用及说明

博客主页:Duck Bro 博客主页系列专栏:Qt 专栏关注博主,后期持续更新系列文章如果有错误感谢请大家批评指出,及时修改感谢大家点赞👍收藏⭐评论✍ Qt常用控件 | 容器类控件 | Group Box的使用及说明 文章编号&#xff…

外汇crm系统是什么

外汇CRM系统是一种专门为外汇交易市场设计的客户关系管理系统。它结合了外汇交易的特点和客户管理的需求,为外汇交易商提供了全面的解决方案。它的出现,极大地促进了外汇交易行业的发展,为交易商提供了更高效、更智能的客户管理方式。 一、外…