Golang中更简洁的切片操作语法实现

Golang中更简洁的切片操作语法实现 关于Go语言的设计问题,我有一个想法。我感觉在Go语言中处理切片操作显得不必要的笨拙。考虑以下用例:

你有一个整数切片 mySlice,想要计算一个包含原切片每个元素平方值的新切片。目前必须这样实现:

mySlice:=[]int{3,1,42}
result:=make([]int,len(mySlice))
for i,val:=range mySlice{
    result[i]=val*val
}

然而其他语言允许类似以下语法:

mySlice:=[]int{3,1,42}
result:=[val*val for _,val:=range mySlice]

我认为这种写法相当简洁。当然这不是巨大的差异,但我经常需要编写类似第一段的代码,这确实会显著增加代码量。

大家对此有什么看法?


更多关于Golang中更简洁的切片操作语法实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

是的,有时必须做出权衡。

请注意,虽然我同意“为了清晰起见,这可能不是期望的做法”,但这只是我个人的观点。我无法代表社区的其他成员,更不用说Go语言的维护者了。

更多关于Golang中更简洁的切片操作语法实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go语言设计的一个主要驱动力可能是为了尽可能保持编译器的快速,或许是为了避免出现这种情况wink

编译器无论如何都会进行一些隐式的类型推断

基于 @christophberger 关于可读性的评论,请注意,即使是那些确实拥有更丰富类型推断的其他语言,为了可读性,仍然强烈鼓励使用类型注解。此外,这些语言通常支持函数重载,因此可能出现的歧义并不是一个(直接的)问题。Go 的设计倾向于清晰明了。

欢迎来到论坛。

我理解你的观点;然而,Go语言更注重可读性而非快速书写。当有半打不同的方式来编写循环时,阅读代码会变得更加复杂。

你可以找到许多其他例子,其中Go语言为了可读性而牺牲了快捷方式。例如,没有三元if-then表达式。(你可能在其他语言中见过这种写法:a := b ? c : d。)这些设计决策都是有意的。做某件事应该只有一种方式。

这门语言被刻意保持得“枯燥”。这正是Go语言适合大型团队的原因。

我完全同意,对于这类问题,泛型函数似乎是正确的解决方案。

peteole: 我在想,为作为参数传递给另一个函数的匿名函数推断参数类型和返回类型会有多困难。实际上,所有类型应该都已经定义好了,无需进一步说明,对吧?

是的,在定义匿名函数的地方,你最终必须指定具体类型(在你的代码中,参数和返回类型都是 int64,并且代码显然运行良好)。

目前,我想不出一个类型推断会变得困难的场景——你心里有例子吗?

是的,在你定义匿名函数的地方,最终必须指定具体的类型。

实际上,我在想是否有可能省略匿名函数的参数类型,因为它们已经由“外部函数”(这里是 Map 函数)的参数类型完全定义了。这会使代码更加简洁:

func main() {
	numbers:=[]int64{2,1,4}
	result:=Map(numbers,func(i){return i*i})
	Print(result)
}

或者像这样,如果我们仍然指定返回类型:

func main() {
	numbers:=[]int64{2,1,4}
	result:=Map(numbers,func(i)int64{return i*i})
	Print(result)
}

上面的例子无法编译,但我实际上不明白为什么编译器不允许这样做:它并没有增加任何额外的复杂性,编译器无论如何都会进行一些隐式的类型推断,而且代码仍然是 100% 类型安全的。

感谢您的回答!

这门语言被有意设计得平淡无奇。

我多少已经意识到了这样做的好处,即使在 Go 中做那些“容易出错”的工作,也出奇地可靠,因为其中没有“魔法”发生。

然而,这种样板代码问题对我来说确实很常见,而且我实际上并不认为我的解决方案非常易读。您认为在标准库中包含一个 map 函数会更可行吗?(它需要使用泛型,所以我们需要等到下一个版本,对吧?) 它实际上相当简单,以下代码在在线编译器中可以运行:

package main

import (
	"fmt"
)

// 现在 playground 使用方括号表示类型参数。除此之外,
// 类型参数列表的语法与常规参数列表的语法相同,只是所有类型参数都必须有名称,并且
// 类型参数列表不能为空。预声明的标识符 "any" 可以用在
// 类型参数约束的位置(并且只能在那里);
// 它表示没有约束。

func Print[T any](s []T) {
	for _, v := range s {
		fmt.Print(v)
	}
}

func Map[T any, U any](arr []T,f func(T) U) []U{
	res:=make([]U,len(arr))
	for i,val:=range arr{
		res[i]=f(val)
	}
	return res
}

func main() {
	numbers:=[]int64{2,1,4}
	result:=Map(numbers,func(i int64)int64{return i*i})
	Print(result)
}

然而,看着这段代码,我在想,对于作为参数传递给另一个函数的匿名函数,推断其参数类型和返回类型会有多困难。实际上,所有类型应该都已经定义好了,不需要任何进一步的说明,对吧?

感谢您的详细解答! 我完全理解,这样的特性会给编译器增加很多复杂性,而这可能并不是大家所期望的。

根据您建议的类型推断,以下声明将是有效的:

无论如何,以我(对Go有限的了解)来看,这并非毫无希望: 我有点把匿名函数看作是数字字面量:可以定义它们,使其在被赋值给变量之前没有类型。在赋值的那一刻,类型必须是明确的。例如,以下代码应该是有效的:

sqrt func(int64) int64:=func(i){return i*i}

因为在赋值的那一刻,类型是明确的。

然而,这个

sqrt:=func(i){return i*i}

或者这个

func sqrt(i){return i*i}

由于您上面描述的原因,将是无效的。

所以,如果在赋值给Map函数的参数时,所有类型都是明确的,那么它应该可以工作。 但是,对于泛型函数来说,情况是否如此,这个问题对我来说似乎不那么容易回答: 泛型类型的“真相来源”对我来说一点也不明显。如果一个多次使用泛型类型的泛型函数被调用,到底是哪个出现决定了泛型类型的值呢? 我实现这个的方式如下:

对于泛型类型的每次出现,判断它是“定义性”的还是“被驱动的”。例如,一个已经有固定类型的参数是定义性的,而一个返回值是被驱动的。 如果定义性的出现具有不同的类型,或者如果没有定义性的出现,则抛出错误;否则,用定义性出现的类型替换泛型类型。

因此,在map函数的情况下,如果匿名函数的返回类型没有指定,就会抛出错误,因为这里会出现“没有定义性出现”的情况。

同样,判断一个匿名函数是不返回任何内容还是仅仅没有指定其返回类型应该很简单:要么函数定义,要么匿名函数被写入的“接收变量”必须指定返回类型,所以这一点是完全清楚的。

无论如何,我明白,尽管据我理解,这样的类型推断可以在有限的努力下实现,但为了代码的清晰性,可能就是不希望这样做。

Go语言目前确实没有内置的列表推导式语法,但可以通过一些方式实现更简洁的切片操作。以下是几种常见方法:

1. 使用泛型函数(Go 1.18+)

func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

// 使用示例
mySlice := []int{3, 1, 42}
result := Map(mySlice, func(x int) int {
    return x * x
})

2. 使用第三方库

许多第三方库提供了类似的功能,例如:

import "github.com/samber/lo"

mySlice := []int{3, 1, 42}
result := lo.Map(mySlice, func(x int, _ int) int {
    return x * x
})

3. 使用通道和goroutine(并行处理)

func ParallelMap[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    var wg sync.WaitGroup
    
    for i, v := range slice {
        wg.Add(1)
        go func(idx int, val T) {
            defer wg.Done()
            result[idx] = f(val)
        }(i, v)
    }
    
    wg.Wait()
    return result
}

4. 方法链式调用(自定义类型)

type Slice[T any] []T

func (s Slice[T]) Map(f func(T) T) Slice[T] {
    result := make(Slice[T], len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

// 使用示例
mySlice := Slice[int]{3, 1, 42}
result := mySlice.Map(func(x int) int {
    return x * x
})

虽然Go语言没有原生的列表推导式语法,但这些方法可以在保持代码可读性的同时减少样板代码。Go团队在设计时更注重显式性和简单性,因此短期内不太可能添加列表推导式语法。

回到顶部