Tuesday, January 20, 2015

Go: Import cycle not allowed

Level: 1 where 1 is noob and 5 is totally awesome
System: Go

Among the programming languages I use, Go is one of my favorites. Intentionally Go lacks features such as generics, inheritance and some others features, which people might see as standards in a programming language. Some might see this as weaknesses in the language, and some might see it as strengths. In the end, the purpose is the keep the language simple, and to build software with less complex code.

A thing which can be challenging in Go, is object parent-child relations, where a child knows it's parent. Like this code below tries, and fails with an 'Import cycle not allowed' error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package child

import "Parent"

type Child struct {
  parent *Parent
}

func (child *Child) PrintParentMessage() {
  child.parent.PrintMessage()
}

func NewChild(parent *Parent) *Child {
  return &Child{parent: parent }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package parent

import (
  "fmt"
  "child"
)

type Parent struct {
  message string
}

func (parent *Parent) PrintMessage() {
  fmt.Println(parent.message)
}

func (parent *Parent) CreateNewChild() *child.Child {
  return child.NewChild(parent)
}

func NewParent() *Parent {
  return &Parent{message: "Hello World"}
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
  "parent"
)

func main() {
  p := parent.NewParent()
  c := p.CreateNewChild()
  c.PrintParentMessage()
}

Cross-refering packages are not allowed in Go. The best thing would be, if Parent could keep track of the children. Then there would be no issues, and some might argue the code would be more clean. But the world is not perfect, and sometimes circumstances enforces design decisions like this.

To make things work, we can use the nice duck typing feature(interfaces) which Go supports. In general, it is good practice to use interfaces. It keeps the code de-couplet and flexible.

So if we make the interface IParent and use it, then everything works out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package child

type IParent interface {
  PrintMessage()
}

type Child struct {
  parent IParent
}

func (child *Child) PrintParentMessage() {
  child.parent.PrintMessage()
}

func NewChild(parent IParent) *Child {
  return &Child{parent: parent }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package parent

import (
  "fmt"
  "child"
)

type Parent struct {
  message string
}

func (parent *Parent) PrintMessage() {
  fmt.Println(parent.message)
}

func (parent *Parent) CreateNewChild() *child.Child {
  return child.NewChild(child.IParent(parent))
}

func NewParent() *Parent {
  return &Parent{message: "Hello World"}
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
  "parent"
)

func main() {
  p := parent.NewParent()
  c := p.CreateNewChild()
  c.PrintParentMessage()
}

Now 'Hello World' is written to output, as intended. Of course, the interface can be placed in a 3rd package. As always, it depends...

No comments:

Post a Comment