Golang中切片作为函数接收器的使用解析

Golang中切片作为函数接收器的使用解析 我在某个文件中定义了以下结构体:

type dbHypervisors struct {
	HID      uint8  `json:"hid" yaml:"hid"`
	Hname    string `json:"hname" yaml:"hname"`
	Haddress string `json:"haddress" yaml:"address"`
}

之后,我这样使用它:

func getHypervisorData(conn *pgx.Conn) []dbHypervisors {
	var hyps []dbHypervisors

	rows, err := conn.Query(context.Background(), "SELECT hID, hName, hAddress from config.hypervisors")
	if err != nil {
		fmt.Println("Error: ", err)
	}
	defer rows.Close()
      <SOME PROCESSING>
      return hyps
}

现在,一旦处理完成,我希望将结构体序列化为JSON(这部分代码是有效的,所以问题不在这里)。我想通过一个函数类型接收器来实现,例如:

func (h dbHypervisors) serialize(filename string) {
	//fmt.Printf("%T \n", jsonObject)
}

然后在我的主函数中,我会这样做:

  hypervisors:= getHypervisorData(conn)
  hypervisors.serialize(targetFile)

它报错说 serialize() 无法解析。我该如何正确使用函数接收器?我猜它期望的是 dbHypervisor 类型,而我希望在那里使用一个切片。

我希望 serialize 能截断文件并一次性写入所有内容。如果我必须通过 for … range 循环,那就意味着我必须在 serialize() 中改变逻辑来追加切片的每个元素,而我想避免这样做。

有什么想法吗?


更多关于Golang中切片作为函数接收器的使用解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

好的,既然在Go中无法实现函数重载,你建议我“重载”类型。这终于让我这个榆木脑袋明白了,Go的核心就是数据类型。

我会尝试一下,再次感谢!

更多关于Golang中切片作为函数接收器的使用解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


……我不明白你为什么要区分单虚拟化管理程序和多虚拟化管理程序?

interface{} 参数不会在意它是否是切片,对吧?

(无论如何,这在这里没有实际意义,因为所有数据库代码仅在存在多个虚拟化管理程序时使用;数据库正是让我的工具能够“感知虚拟化管理程序”的原因。)

好的,现在让我们真正地结束这个问题:

[11:03:12|jfgratton@bergen:source]: jq < hypervisors.json 
[
  {
    "hid": 1,
    "hname": "bergen",
    "haddress": "10.3.1.1"
  },
  {
    "hid": 2,
    "hname": "oslo",
    "haddress": "10.2.1.1"
  }
]

成功了,这正是预期的结果。

您可以定义一个 dbHypervisorsSlice 类型并为其实现一个 serialize 函数:

func getHypervisorData(conn *pgx.Conn) dbHypervisorsSlice {
    // ...
}

type dbHypervisorSlice []dbHypervisors

func (hyps dbHypervisorsSlice) serialize(filename string) {
    // 编写一个包含循环的代码,以写入所有内部的 dbHypervisors 值。
    // 这样,您就不必修改 dbHypervisors 自身的 serialize 函数。
}

是的,我可以按照您的建议去做,但问题是……我希望它尽可能高效,因为我需要在这个模块中导出三个小表,并且每个表都可以导出为 JSON、YAML 或 SQL 格式;这完全由最终用户决定,所以这里的诀窍是尽可能少写代码。

关于您的代码示例,使用 interface{} 是我最初的选择,但我读过一些关于使用 interface{} 的令人困惑的文章,它们甚至自相矛盾!我认为我对它的理解阻碍了我像您刚才那样使用它。

像 C++ 那样的函数重载本应是显而易见的选择,但您只能使用所选语言提供的功能,所以我现在就是这样做的。

……

我刚刚在 uDemy 上给自己买了 2-3 门课程,我受够了四处搜寻信息,这比实际编写代码浪费的时间还要多。我早就应该这么做了,我知道……

既然Go语言不支持函数重载,你建议我“重载”类型。

你无法重载一个函数,但你可以将 serializedbHypervisors 中“分离”出来,使其成为一个独立的函数:

func serialize(v interface{}, filename string) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    data, err := json.Marshal(v)
    if err != nil {
        return err
    }
    _, err = f.Write(data)
    return err
}

// ...

func main() {
    // ...
    oneHypervisor := dbHypervisors{ /* ... */ }
    if err := serialize(oneHypervisor, "single-hypervisor.json"); err != nil {
        log.Fatal(err)
    }
    twoHypervisors := []dbHypervisors{ /* ... */ }
    if err := serialize(twoHypervisors, "multiple-hypervisors.json"); err != nil {
        log.Fatal(err)
    }

}

我理解,你是基于不完整的信息回复的,并据此给出了最佳答案。为了简洁起见,我已将展示的代码量减少到最低限度。你的C++类定义完全正确,如果是我也会采用这种方式,但同样地,我尽量简化了我的解释。

话虽如此,对我来说最重要的事情(学到的教训)是你提到的:type dbHypervisors != type []dbHypervisors,这在我看来有点反直觉,但我会处理的。

我将采用“断开连接的序列化”解决方案。这本来是我的首选方案,但我曾读到(现在在我的浏览器历史记录中找不到了)说这不是一个解决方案,所以我甚至没有尝试。我曾认为空接口是一个完美的解决方案(让我想起了C++中的*object对象),所以读到它不可行时,我有点失望。

再次感谢你花时间指导我这个刚刚开始接触Go的人。我正在构建一个libvirt客户端,用于管理跨多个虚拟机管理器的虚拟机及其资源。通过解决实际问题来学习一门新语言,这是一个非常好的方法。

… 我不明白你为什么要在单个管理程序和多个管理程序之间做区分?

interface{} 参数不会在意它是不是切片,对吧?

在你回复我的第一个建议后,提供了更多信息,所以我修改了我的答案 😊,但我的两个答案本质上都可以归结为:

type dbHypervisors != type []dbHypervisors

在你最初的问题中,你提到你的 getHypervisorData 函数返回 []dbHypervisors。然后你表示希望能够这样写:

hypervisors:= getHypervisorData(conn)
hypervisors.serialize(targetFile)

我将其理解为:“我有一个类型 dbHypervisors,它上面有一个 serialize 方法。我还希望能够有一个 dbHypervisors 的切片,并为它们调用一个单一的 serialize 方法。” 我的第一个答案只展示了如何直接实现这一点。dbHypervisors 的切片是一个独立的类型(根据你提到的 C++,我会将 []dbHypervisors 描述为类似于 std::vector<dbHypervisors>),就像在 C++ 中你不能这样做:

std::vector<dbHypervisors> myHypervisors;
// ...
myHypervisors.serialize();

在 Go 中你也不能这样做。相反,你必须定义一个新类型并为其添加成员函数;因此,在 Go 中是:type dbHypervisorsSlice []dbHypervisors,我认为在 C++ 中这类似于:

class dbHypervisorsVector : public std::vector<T>
{
    // ...
}

通过拥有一个独立的类型(在 Go 中是 dbHypervisorsSlice,在 C++ 中是 dbHypervisorsVector),你现在可以在其上定义一个 serialize 方法。


在我的第二个建议中,在你提到重载之后,让我想到你可以用一个单一的函数定义来解决问题,就像我建议的那样。在这个例子中,你是对的:不再有理由去区分 dbHypervisorsdbHypervisorsSlice,这就是为什么我放弃了 dbHypervisorsSlice 而使用了 []dbHypervisors(事后看来,我应该把这一点说得更明确)。

在Go语言中,接收器类型必须与方法定义的类型完全匹配。你定义的方法接收器是单个dbHypervisors结构体,但调用时使用的是[]dbHypervisors切片,因此会出现错误。

你有两种解决方案:

方案1:为切片类型定义方法(推荐)

创建自定义切片类型,然后为其定义方法:

// 定义自定义切片类型
type dbHypervisorsSlice []dbHypervisors

// 为切片类型定义serialize方法
func (hs dbHypervisorsSlice) serialize(filename string) error {
    // 创建或截断文件
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    // 序列化整个切片为JSON
    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    return encoder.Encode(hs)
}

// 修改getHypervisorData函数返回自定义切片类型
func getHypervisorData(conn *pgx.Conn) dbHypervisorsSlice {
    var hyps dbHypervisorsSlice
    
    rows, err := conn.Query(context.Background(), "SELECT hID, hName, hAddress from config.hypervisors")
    if err != nil {
        fmt.Println("Error: ", err)
        return hyps
    }
    defer rows.Close()
    
    // 处理rows并填充hyps
    for rows.Next() {
        var h dbHypervisors
        if err := rows.Scan(&h.HID, &h.Hname, &h.Haddress); err != nil {
            fmt.Println("Scan error: ", err)
            continue
        }
        hyps = append(hyps, h)
    }
    
    return hyps
}

// 使用示例
func main() {
    hypervisors := getHypervisorData(conn)
    if err := hypervisors.serialize(targetFile); err != nil {
        fmt.Println("Serialize error:", err)
    }
}

方案2:使用指针接收器处理切片

如果你不想创建自定义类型,可以使用辅助函数:

// 为切片定义函数(不是方法)
func serializeHypervisors(hyps []dbHypervisors, filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    return encoder.Encode(hyps)
}

// 或者为切片指针定义方法
type dbHypervisorsList struct {
    Items []dbHypervisors
}

func (hl *dbHypervisorsList) serialize(filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    return encoder.Encode(hl.Items)
}

方案3:使用接口包装

type Serializable interface {
    Serialize(filename string) error
}

type HypervisorCollection struct {
    Hypervisors []dbHypervisors
}

func (hc *HypervisorCollection) Serialize(filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    return encoder.Encode(hc.Hypervisors)
}

// 使用
func getHypervisorData(conn *pgx.Conn) Serializable {
    collection := &HypervisorCollection{}
    
    rows, err := conn.Query(context.Background(), "SELECT hID, hName, hAddress from config.hypervisors")
    if err != nil {
        fmt.Println("Error: ", err)
        return collection
    }
    defer rows.Close()
    
    for rows.Next() {
        var h dbHypervisors
        if err := rows.Scan(&h.HID, &h.Hname, &h.Haddress); err != nil {
            fmt.Println("Scan error: ", err)
            continue
        }
        collection.Hypervisors = append(collection.Hypervisors, h)
    }
    
    return collection
}

第一种方案是最简洁的,它通过类型别名dbHypervisorsSlice为切片添加了方法,同时保持了与普通切

回到顶部