Golang中难以理解的OO模式问题求助
Golang中难以理解的OO模式问题求助 我拥有长期的Java开发经验,现在正转向Go语言。我曾尝试过Rust一段时间,但由于我的C和C++经验非常匮乏,我无法投入足够的时间和精力。Go是一个更合适的选择,特别是现在我可以使用fyne创建跨平台UI。我也一直在使用Flutter和Dart,并且非常喜欢它。言归正传,以下是我的问题。请参见下面的 我需要帮助的地方!。
我已将代码包含在下方。
我在结构体代码中有一个接口(称为NodeI)。它有三个方法。
- GetName() string
- GetNodeType() NodeType
- String() string
我有一个名为ParentNode的节点(我希望继承它的行为)。它有两个方法。
- GetName() string
- GetNodeType() NodeType
我有四个结构体,它们都有一个未命名的父节点。例如ListNode,并且它们都有一个String()函数(String() string)
type ListNode struct {
parentNode // 在所有四个结构体中
value []*NodeI // ListNode特有
}
我使用指针来防止“复制”行为,并且所有结构体都必须是可更新的(除了父节点中的name和NodeType。这些是不可变的)
我正在构建一个树形结构,其中ListNode包含叶子节点以及更多的ListNode…等等。
我需要帮助的地方!
在函数中创建具体对象并将其作为接口返回 参考main.go中的makeStruct()函数。我更希望返回 *structure.NodeI。类似于一个工厂方法。然而,我无法将 *structure.ListNode 转换为 *structure.NodeI 并返回它。
我尝试了 pp := (*p).(*structure.NodeI),其中p是 *structure.ListNode,并且我尝试了“许多”不同的格式来使转换生效,但都没有成功。
将具体对象作为其接口传递给方法 参考接受 *structure.NodeI 作为参数的printName函数。 我可以传递一个 *structure.NodeI 给它,但不能传递任何实现了该接口的具体对象。
例如,当p是 *structure.ListNode时,printName§ 会失败。而printName(b)可以工作。这里的b是(T) *structure.NodeI。它是由ListNode的GetNodeAt(i)方法返回的,并且是作为structure.NumberNode创建的。
请帮助我。如果这些模式能够应用,代码将更容易阅读和理解。
非常感谢
Stuart
main.go
func main() {
p := makeStruct()
fmt.Printf(" 1: %T\n", p) // *structure.ListNode
fmt.Printf(" 2: %s", p) // ListNode的String()
b := p.GetValueAt(1)
//
// printName(p) // 这无法编译。p是 *structure.NumberNode
//
printName(b, " 3")
fmt.Printf(" 4: %T\n", b) // *structure.NodeI
fmt.Printf(" 5: %T\n", *b) // *structure.NumberNode
fmt.Printf(" 6: %s\n", *b) // NumberNode的String()
bb := (*b).(*structure.NumberNode)
fmt.Printf(" 7: %T\n", bb) // *structure.NumberNode
bb.SetValue(999)
fmt.Printf(" 8: %s\n", bb.GetName()) // NUM
fmt.Printf(" 9: %f\n", bb.GetValue()) // 999.000000
fmt.Printf("10: %s\n", bb) // NumberNode的String()
fmt.Printf("11: %s", p) // ListNode的String()
}
func printName(ni *structure.NodeI, id string) {
fmt.Printf("%s: Name: %s\n", id, (*ni).GetName())
}
func makeStruct() *structure.ListNode {
p := structure.NewListNode()
s := structure.NewStringNode("STR", "B")
n := structure.NewNumberNode("NUM", 123.5)
b := structure.NewBoolNode("BOO", true)
p.AddValue(s)
p.AddValue(n)
p.AddValue(b)
fmt.Printf(" 0: %T\n", p) // *structure.ListNode
// pp := (*p).(*structure.NodeI)
// fmt.Printf("makeStruct: %T", pp) // *structure.ListNode
return p
}
节点代码!
package structure
import (
"fmt"
"strings"
)
type NodeType int
const (
NT_NUMBER NodeType = iota
NT_STRING NodeType = iota
NT_BOOL NodeType = iota
)
type NodeI interface {
GetName() string
GetNodeType() NodeType
String() string
}
//
// 父节点接口 (NodeI) 和属性
//
type parentNode struct {
name string
nt NodeType
}
func newParentNode(name string, nt NodeType) parentNode {
return parentNode{name: name, nt: nt}
}
func (n *parentNode) GetName() string {
return n.name
}
func (n *parentNode) GetNodeType() NodeType {
return n.nt
}
type ListNode struct {
parentNode
value []*NodeI
}
//
// 列表节点是一个ParentNode,其值类型为 []NodeI
//
func NewListNode() *ListNode {
return &ListNode{parentNode: newParentNode("", NT_NUMBER), value: make([]*NodeI, 0)}
}
func (n *ListNode) GetValue() *[]*NodeI {
return &n.value
}
func (n *ListNode) GetValueAt(i int) *NodeI {
return n.value[i]
}
func (n *ListNode) AddValue(node NodeI) {
n.value = append(n.value, &node)
}
func (n *ListNode) String() string {
var sb strings.Builder
sb.WriteString("ListNode.String() :\n")
for i, v := range n.value {
sb.WriteString(fmt.Sprintf(" %d :", i))
sb.WriteString((*v).String())
sb.WriteString("\n")
}
return sb.String()
}
//
// 字符串节点是一个ParentNode,其值类型为 string
//
type StringNode struct {
parentNode
value string
}
func NewStringNode(name string, value string) *StringNode {
return &StringNode{parentNode: newParentNode(name, NT_STRING), value: value}
}
func (n *StringNode) GetValue() string {
return n.value
}
func (n *StringNode) SetValue(newValue string) {
n.value = newValue
}
func (n *StringNode) String() string {
return fmt.Sprintf("StringNode.String(): \"%s\": \"%s\"", n.GetName(), n.value)
}
//
// 数字节点是一个ParentNode,其值类型为 float64
//
type NumberNode struct {
parentNode
value float64
}
func NewNumberNode(name string, value float64) *NumberNode {
return &NumberNode{parentNode: newParentNode(name, NT_NUMBER), value: value}
}
func (n *NumberNode) GetValue() float64 {
return n.value
}
func (n *NumberNode) SetValue(newValue float64) {
n.value = newValue
}
func (n *NumberNode) String() string {
return fmt.Sprintf("NumberNode.String(): \"%s\": %f", n.GetName(), n.value)
}
//
// 布尔节点是一个ParentNode,其值类型为 bool
//
type BoolNode struct {
parentNode
value bool
}
func NewBoolNode(name string, value bool) *BoolNode {
return &BoolNode{parentNode: newParentNode(name, NT_BOOL), value: value}
}
func (n *BoolNode) GetValue() bool {
return n.value
}
func (n *BoolNode) SetValue(newValue bool) {
n.value = newValue
}
func (n *BoolNode) String() string {
return fmt.Sprintf("BoolNode.String(): \"%s\": %t", n.GetName(), n.value)
}
更多关于Golang中难以理解的OO模式问题求助的实战教程也可以访问 https://www.itying.com/category-94-b0.html
stuartdd:
在函数中创建具体对象并将其作为接口返回
推荐的做法是返回具体的结构体,而不是接口。(这样更灵活,且不会丢失信息)。
stuartdd:
将具体对象作为其接口传递给方法
即使方法接受的是接口,你也只需传递具体对象即可。
更多关于Golang中难以理解的OO模式问题求助的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你好 Holloway
感谢你的回复。我不太习惯这个论坛的语法,所以请原谅我使用纯文本回复。
我认为我的困惑在于向 structure.NodeI 传递指针,正如你所说,它不是一个真实的对象。
我的理解是,我所有的节点都通过组合 parentNode 和节点本身,具备了 NodeI 接口的所有函数。
例如,我的 String 节点是:
type StringNode struct {
parentNode
value string
}
...
func (n *StringNode) String() string {
return fmt.Sprintf("StringNode.String(): \"%s\": \"%s\"", n.GetName(), n.value)
}
而 parentNode 拥有:
func (n *parentNode) GetName() string {
return n.name
}
func (n *parentNode) GetNodeType() NodeType {
return n.nt
}
它们组合起来覆盖了 NodeI 接口的所有方法签名。
问题:
- 在每个结构体中包含
parentNode会扩展该结构体的整体接口。这是正确的做法吗? - 在像
func printName(n *structure.NodeI)这样的方法中,我实际传递的是什么?这是一个错误吗? - 如果我把它改成
func printName(n structure.NodeI),这是否意味着一个复制操作?那么我该如何传入一个指向拥有全部三个关联函数的对象的指针呢?
我想我已经得到了所有问题的答案,但我并不觉得这是一堂“轻松”的课。
我能理解这种复杂性的原因,只是觉得在某些地方,所有的类型转换和指针操作有点不够优雅。这不是批评,只是一个观察 :-)。我太习惯 Java 抽象层提供的便利了。
所以,感谢你的帮助。这让我受益匪浅。
好的。我了解了“扩展接口模式”,它允许我“组合”一个结构体及其方法,使其符合某个接口。
这确实很棒,并且某种程度上是自动实现的,因为它不需要额外的注解或关键字就能完成。
我需要通过实验来更好地理解它。例如,当方法存在优先级问题时会发生什么。比如,如果嵌入的结构体实现了 String() string 方法,而我自己的结构体也有一个 String() string 方法,那么会使用哪一个呢?
建议总是使用结构体名称而不是接口来声明参数和返回值吗?
如果是这样,我想对此表示不同意。所有对象都是具体的,你永远不会丢失数据,一个 NumberNode 即使作为 NodeI 返回,也始终是一个 NumberNode。一旦你将返回的对象转换(cast)为正确的结构体,这个对象就是完整的。
在构建 API 时,将参数和返回值声明为抽象的(接口)是一种非常有用的模式。
拥有一个简单单一的 GetNode() NodeI 和 SetNode(n NodeI),比在我的情况下拥有三个 Get 和 Set 方法更清晰,不易混淆。
GetStringNode() StringNode
SetStringNode(n StringNode)
GetNumberNode() NumberNode
SetNumberNode(n NumberNode)
GetBoolNode() BoolNode
SeBoolNode(n BoolNode)
尽管我发现 Go 中的“类型断言”或“转换”(不管它叫什么)比我用过的其他语言稍微不那么直观。我会习惯的!
一个巨大的优势是,它允许 API 的用户创建他们自己的对象并将其传递给您的 API,而无需更改 API 中的代码。这对于构建有用的工具和包来说是必要的。
这与工厂模式相结合,可以创建出更通用、更具表现力的 API。
感谢这次讨论。我学到了很多关于 Go 内部机制的知识,这真的非常有用。您已经回答了我所有的问题。
此致
不。正如我之前所说,Go 语言没有面向对象范式,
parentNode永远不会被自动“魔法般地”继承(没有继承和多态)。你刚才描述的是继承(节点继承parentNode)。组合应该是这样的:
type Common struct {
Department string
CostCenter string
}
type Person struct {
Common *Common
Name string
}
type Inventory struct {
Common *Common
ID string
Name string
}
func main() {
p1 := &Person {
Common: &Common {
Department: "Service",
CostCenter: "service-center",
}
Name: "John Smith",
}
fmt.Printf("%s is from %s department under cost center '%s'\n", p1.Name, p1.Common.Department, p1.Common.CostCenter)
}
请注意 Common 结构体数据类型是如何被组合到每个不同的特定结构体(Person 和 Inventory)内部的,以及 main 函数是如何访问 Common 数据的。
你操作像 StringNode 这样的 struct。只要 struct 符合某个 interface,你就可以使用其指针来传递 struct 对象。
这里有一个简单的完整示例以供理解:
完全错误。由于
interface是一组“规则”,其类型上的内存指针将不起作用(因为它不是一个像struct、int等那样的具体对象)。
据我理解:不。
interface只能基于其定义的方法进行操作,因此没有东西可以复制到函数中。
换句话说,如果你传入一个结构体(无论是指针还是实际值),除非 interface 有一个像 SetName(...) 这样的方法,或者重新转换该结构体,否则你无法直接更改结构体内部的数据。
因为你已经适应了 Java 范式,改变需要时间。来自 C/C++(我来自那里)的人可以很容易地看到 Go 的好处。你关于使用指针来避免复制的想法是正确的。问题在于对
interface的困惑,这是 Go 特有和具体的东西,是“迈向泛型的第一步”。
他在这里说得对。你不需要返回一个接口,因为规则已经定义好了。你返回的是结构体,也就是数据。
你的问题源于Go语言接口和指针处理的几个关键差异。以下是解决方案:
问题1:接口类型转换
在makeStruct()函数中,你无法将*ListNode转换为*NodeI是因为Go中接口不是指针类型。正确的做法是直接返回接口类型:
func makeStruct() structure.NodeI {
p := structure.NewListNode()
s := structure.NewStringNode("STR", "B")
n := structure.NewNumberNode("NUM", 123.5)
b := structure.NewBoolNode("BOO", true)
p.AddValue(s)
p.AddValue(n)
p.AddValue(b)
return p // ListNode实现了NodeI接口,可以直接返回
}
问题2:ListNode.value切片类型错误
你的ListNode.value定义为[]*NodeI,这会导致接口包装问题。应该使用[]NodeI:
type ListNode struct {
parentNode
value []NodeI // 改为接口切片,而不是指针切片
}
func NewListNode() *ListNode {
return &ListNode{
parentNode: newParentNode("LIST", NT_NUMBER), // 添加名称
value: make([]NodeI, 0),
}
}
func (n *ListNode) GetValueAt(i int) NodeI { // 返回接口,而不是指针
return n.value[i]
}
func (n *ListNode) AddValue(node NodeI) {
n.value = append(n.value, node) // 直接添加接口值
}
问题3:printName函数参数类型
printName函数应该接收接口值,而不是接口指针:
func printName(ni structure.NodeI, id string) {
fmt.Printf("%s: Name: %s\n", id, ni.GetName())
}
修正后的完整main.go:
func main() {
p := makeStruct()
fmt.Printf(" 1: %T\n", p) // *structure.ListNode
fmt.Printf(" 2: %s", p) // ListNode的String()
b := p.GetValueAt(1)
printName(p, " 3") // 现在可以工作了
printName(b, " 4")
fmt.Printf(" 5: %T\n", b) // *structure.NumberNode
fmt.Printf(" 6: %s\n", b) // NumberNode的String()
// 类型断言获取具体类型
if bb, ok := b.(*structure.NumberNode); ok {
fmt.Printf(" 7: %T\n", bb) // *structure.NumberNode
bb.SetValue(999)
fmt.Printf(" 8: %s\n", bb.GetName()) // NUM
fmt.Printf(" 9: %f\n", bb.GetValue()) // 999.000000
fmt.Printf("10: %s\n", bb) // NumberNode的String()
}
fmt.Printf("11: %s", p) // ListNode的String()
}
func printName(ni structure.NodeI, id string) {
fmt.Printf("%s: Name: %s\n", id, ni.GetName())
}
func makeStruct() structure.NodeI {
p := structure.NewListNode()
s := structure.NewStringNode("STR", "B")
n := structure.NewNumberNode("NUM", 123.5)
b := structure.NewBoolNode("BOO", true)
p.AddValue(s)
p.AddValue(n)
p.AddValue(b)
return p
}
关键理解点:
- Go接口是隐式实现的:不需要显式声明
implements - 接口值包含(type, value):当你将具体类型赋值给接口时,Go会自动包装
- 避免接口指针:大多数情况下应该使用
NodeI而不是*NodeI - 类型断言语法:使用
value.(Type)进行类型断言,而不是指针转换
这些修改将使你的代码符合Go语言的惯用法,同时保持你期望的面向对象模式。


