Inheritence and method parameter

Hi,
I’m new to Go and trying to use it in a assignment about knowledge graphs.

The way I’m implementing this is as follows:

  • I have a class Node that have a name and named relationships. It should not be used directly.
  • I want a class Concept and a class Instance that both “inherit” Node.
    …and that’s where I have trouble

Because a Concept isn’t a Node (in Go), my method AddRelationship on Node can’t work on a Concept.

My first idea was to use an interface, but I’m a bit lost and what I tried doesn’t seems to work.

So, my “class” Node that works:

package main

import "fmt"

// a node in the sense of a knowledge graph. Shouldn't be used directly.
type Node struct {
	name          string
	relationships map[string][]*Node
}

// Constructor of node
func NewNode(name string) *Node {
	return &Node{
		name:          name,
        // relationships have a name
		relationships: make(map[string][]*Node),
	}
}

func (n *Node) GetName() string {
	return n.name
}

// Add a relationship to the node
func (n *Node) AddRelationship(name string, node *Node) {
	n.relationships[name] = append(n.relationships[name], node)
}

// Find relationships by name
func (n *Node) Find(name string) []*Node {
	return n.relationships[name]
}

// Print the node and its relationships
func (n *Node) Print() {
	fmt.Println("[", n.name, "]")
	for relationName, nodes := range n.relationships {
		for _, node := range nodes {
			fmt.Println(" \u2514\u2500(", relationName, ")->[", node.GetName(), "]")
		}
	}
}

Simple example:

package main

func main() {
	node := NewNode("Paul")
	node.AddRelationship("is-a", NewNode("Humain"))
	node.Print()
}
$ go run ./main.go ./node.go                                                                                                                    
[ Paul ]
 └─( is-a )->[ Humain ]

What I tried with my Concept class:

Defining an interface

type INode interface {
	AddRelationship(name string, node *INode)
	Find(name string) []INode
	GetName() string
	Print()
}

and replacing every parameter and output in the previous code with INode (instead of *Node).
But even if I implemented all the method, Concept isn’t recognized as INode.

package main

// A concept is a node in the sense of a knowledge graph.
type Concept struct {
	Node
}

func NewConcept(name string) *Concept {
	return &Concept{
		Node: *NewNode(name),
	}
}

func (c *Concept) GetName() string {
	return c.Node.GetName()
}

// I read that in parameters, *INode and INode behave the same as it's a interface?
func (c *Concept) AddRelationship(name string, node INode) {
	c.Node.AddRelationship(name, node)
}

func (c *Concept) Find(name string) []INode {
	return c.Node.Find(name)
}

func (c *Concept) Print() {
	c.Node.Print()
}

So what am I missing in Go’s inheritence system?

First, Concept does not satisfy the INode interface because
AddRelationship(name string, node INode)

is different from

AddRelationship(name string, node *INode)

The INode interface requires the second argument to AddRelationship() to be a pointer to type satisfying the INode interface. While the Concept’s AddRelationship() method’s 2nd arguments require anything that can satisfy the INode interface (which could either be a value or a pointer to a value).

Further explanation that may help why the two parameters are different: T and *T can have different method sets.

And also why pointers to interface (like *INode) should rarely be used: When should I use a pointer to an interface?

Second, looking at:

func (n *Node) AddRelationship(name string, node *Node) {
	n.relationships[name] = append(n.relationships[name], node)
}

Expected 2nd argument is a pointer to a specific type (*Node).

… and then in:

func (c *Concept) AddRelationship(name string, node INode) {
	c.Node.AddRelationship(name, node)
}

2nd parameter is anything (any type or a pointer to any type) that satisfy the INode interface. However, it is passed to c.Node.AddRelationship(name, node) which we know needs to be specifically *Node. So for example, I have another type Foo that satisfy the INode interface, Concept.AddRelationship() can accept it but Node.AddRelationship will not.
You will need to use type assertion to check if the received 2nd parameter is *Node.

Thanks a lot for your help, I have something working! (for now at least)

I think that was my biggest mistake. I now use INode as parameter everywhere (see code below). But there is something I don’t get:

  • How do I specify that in my map[string][]INode variable contains a pointer to a variable, not the variable itself?
  • In my main.go (see below), the 2nd parameter to AddRelationship is a pointer to Concept. But I never specified AddRelationship should take a pointer to a value or a value, or did I?

node.go

package main

import "fmt"

type INode interface {
	AddRelationship(name string, node INode)
	Find(name string) []INode
	GetName() string
	Print()
}

// a node in the sense of a knowledge graph. Shouldn't be used directly.
type Node struct {
	name          string
	relationships map[string][]INode
}

// Constructor of node
func NewNode(name string) *Node {
	return &Node{
		name:          name,
		relationships: make(map[string][]INode),
	}
}

func (n *Node) GetName() string {
	return n.name
}

// Add a relationship to the node
func (n *Node) AddRelationship(name string, node INode) {
	n.relationships[name] = append(n.relationships[name], node)
}

// Find relationships by name
func (n *Node) Find(name string) []INode {
	return n.relationships[name]
}

// Print the node and its relationships
func (n *Node) Print() {
	fmt.Println("[", n.name, "]")
	for relationName, nodes := range n.relationships {
		for _, node := range nodes {
			fmt.Println(" \u2514\u2500(", relationName, ")->[", node.GetName(), "]")
		}
	}
}

concept.go

package main

// A concept is a node in the sense of a knowledge graph.
type Concept struct {
	Node
}

func NewConcept(name string) *Concept {
	return &Concept{
		Node: *NewNode(name),
	}
}

func (c *Concept) GetName() string {
	return c.Node.GetName()
}

func (c *Concept) AddRelationship(name string, node INode) {
	c.Node.AddRelationship(name, node)
}

func (c *Concept) Find(name string) []INode {
	return c.Node.Find(name)
}

func (c *Concept) Print() {
	c.Node.Print()
}

main.go

package main

func main() {
	human := NewConcept("Human")
	woman := NewConcept("Woman")
	woman.AddRelationship("is-a", human)
	woman.Print()
	juliette := NewNode("Juliette")
	juliette.AddRelationship("is-a", woman)
	juliette.Print()
}

stdout

[ Woman ]
 └─( is-a )->[ Human ]
[ Juliette ]
 └─( is-a )->[ Woman ]

How do I specify that in my map[string][]INode variable contains a pointer to a variable, not the variable itself?

You can’t, as far as I know.

In my main.go (see below), the 2nd parameter to AddRelationship is a pointer to Concept. But I never specified AddRelationship should take a pointer to a value or a value, or did I?

You did not. But since the methods required to satisfy the INode interface are defined on a pointer receiver, it means that only a pointer to Concept (*Concept) will satisfy the INode interface.

If it is a bit confusing, see this example: Go Playground - The Go Programming Language
The Given struct methods are defined on *Given . So only pointers to Given will have those methods.
When I pass an empty x Given struct to the checkSample() function that accepts a Sample interface, it fails. Changing it to a pointer (x := &Given{}) makes it work.

1 Like

Thank you so much for the time you spent!

I think I got it