【问题标题】:Pass slice as function argument, and modify the original slice将切片作为函数参数传递,并修改原始切片
【发布时间】:2018-08-31 21:27:39
【问题描述】:

我知道在 Go 中一切都是按值传递的,这意味着如果我给一个函数一个切片,并且该函数使用内置的 append 函数附加到切片,那么原始切片将没有附加的值函数的范围。

例如:

nums := []int{1, 2, 3}

func addToNumbs(nums []int) []int {
    nums = append(nums, 4)
    fmt.Println(nums) // []int{1, 2, 3, 4}
}

fmt.Println(nums) // []int{1, 2, 3}

这给我带来了一个问题,因为我试图在一个累积切片上进行递归,基本上是一个 reduce 类型的函数,除了 reducer 调用自身。

这是一个例子:

func Validate(obj Validatable) ([]ValidationMessage, error) {
    messages := make([]ValidationMessage, 0)

    if err := validate(obj, messages); err != nil {
        return messages, err
    }

    return messages, nil
}

func validate(obj Validatable, accumulator []ValidationMessage) error {
    // If something is true, recurse
    if something {
        if err := validate(obj, accumulator); err != nil {
            return err
        }
    }

    // Append to the accumulator passed in
    accumulator = append(accumulator, message)

    return nil
}

上面的代码给出了与第一个示例相同的错误,因为 accumulator 没有获取所有附加值,因为它们只存在于函数的范围内。

为了解决这个问题,我将一个指针结构传递给函数,该结构包含累加器。该解决方案效果很好。

我的问题是,有没有更好的方法来做到这一点,我的方法是 Go 的惯用方法吗?

更新的解决方案(感谢 icza):

我只是在递归函数中返回切片。这么大的脸,早该想到的。

func Validate(obj Validatable) ([]ValidationMessage, error) {
    messages := make([]ValidationMessage, 0)
    return validate(obj, messages)
}

func validate(obj Validatable, messages []ValidationMessage) ([]ValidationMessage, error) {
    err := v.Struct(obj)

    if _, ok := err.(*validator.InvalidValidationError); ok {
        return []ValidationMessage{}, errors.New(err.Error())
    }

    if _, ok := err.(validator.ValidationErrors); ok {
        messageMap := obj.Validate()

        for _, err := range err.(validator.ValidationErrors) {
            f := err.StructField()
            t := err.Tag()

            if v, ok := err.Value().(Validatable); ok {
                return validate(v, messages)
            } else if _, ok := messageMap[f]; ok {
                if _, ok := messageMap[f][t]; ok {
                    messages = append(messages, ValidationMessage(messageMap[f][t]))
                }
            }
        }
    }

    return messages, nil
}

【问题讨论】:

  • 返回新切片,就像内置的 append() 一样(并在调用者处分配返回值),或者传递一个指向切片的指针,而不是切片头。
  • 我无法返回新切片,因为我必须对切片进行递归处理,但我认为传递一个指向轻微的指针可能有效?让我试试..
  • 递归调用也可以返回值,我看不出有什么问题。如果您使用传递切片指针,请注意处理它们更“冗长”,请参阅Slicing a slice pointer passed as argument 作为示例。
  • 你知道吗,你是完全正确的。不知道我在想什么。从递归函数返回切片对我来说效果很好。非常感谢。
  • @OmarIlias 传递切片标头足够快,因为它很小。如果你传递一个指向它的指针,传递可能会更快,但为了使用它,你必须“不断地”(在每次使用时)取消引用它,代码会更丑陋,所以你会失去优势。想一想:内置的 append() 可以被编写为获取一个指针,并且它不需要返回新切片(并且您不必分配回结果),但它在另一个上失败了需要切片的路径,而不是指向切片的指针。

标签: go


【解决方案1】:

如果要将切片作为参数传递给函数,并让该函数修改原始切片,则必须传递指向切片的指针:

func myAppend(list *[]string, value string) {
    *list = append(*list, value)
}

我不知道 Go 编译器在这方面是天真还是聪明;性能留给评论部分作为练习。

【讨论】:

【解决方案2】:

如果切片的当前大小不足以追加新值,则切片会根据需要动态增长,从而更改底层数组。如果未返回此新切片,则您的追加更改将不可见。

例子:

package main

import (
    "fmt"
)

func noReturn(a []int, data ...int) {
    a = append(a, data...)
}

func returnS(a []int, data ...int) []int {
    return append(a, data...)
}

func main() {
    a := make([]int, 1)
    noReturn(a, 1, 2, 3)
    fmt.Println(a) // append changes will not visible since slice size grew on demand changing underlying array

    a = returnS(a, 1, 2, 3)
    fmt.Println(a) // append changes will be visible here since your are returning the new updated slice
}

结果:

[0]
[0 1 2 3]

注意:

  1. 如果您更新切片中的项目而不向切片添加新项目,则不必返回切片

【讨论】:

  • 谢谢,这是一个很好的解释,但它并没有提供解决问题的方法 :) 不过,我已经通过在递归函数中返回切片找到了解决方案,然后我不'不必尝试操作指向切片的指针或传递包含切片的指针结构。
  • "如果你的切片初始化的大小足以追加新值,你就不必返回切片。"这是错误的。如果你追加到一个切片,即使底层数组有足够的空间来存储追加的值,追加的值也不会出现在调用函数的切片中,因为切片头还包含长度和容量字段,这些字段不会被更新通过附加。您可以通过超出切片长度的子切片来访问它们,但它们不会自动显示。示例:play.golang.org/p/z1mIuIDdMkJ
【解决方案3】:

您传递的切片是对数组的引用,这意味着大小是固定的。如果你只是修改了存储的值,没关系,值将在被调用函数之外更新。

但是如果你给切片添加了新元素,它会重新切片以容纳新元素,也就是说,会创建一个新切片,而旧切片不会被覆盖。

总结一下,如果需要对切片进行扩展或剪切,将指针传递给切片即可。否则,使用切片本身就足够了。

更新


我需要解释一些重要的事实。对于作为值传递给函数的切片添加新元素,有两种情况:

一个

底层数组达到了它的容量,创建了一个新的切片来替换原始切片,显然原始切片不会被修改。

B

底层数组未达到其容量,已被修改。 但是切片的字段len没有被覆盖,因为切片是按值传递的。因此,原始切片不会意识到其 len 已被修改,从而导致切片未被修改。

【讨论】:

    【解决方案4】:

    在向切片追加数据时,如果切片的底层数组空间不足,则会分配一个新数组。然后将旧数组中的元素复制到这个新内存中,并在后面添加新数据

    【讨论】:

    • 这解释了为什么附加操作并不总是修改原始传递的切片,即使切片是“引用类型”,因为底层数组的标题是指针。 +1 用于解释,尽管在实施/示例方面,此评论可能会做得更好
    猜你喜欢
    • 2021-10-26
    • 2016-10-27
    • 1970-01-01
    • 2016-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-12
    • 2018-04-22
    相关资源
    最近更新 更多