Practical Go: Learn about return interface

📚 4 min read Tweet this post

As a Golang developer, you might be familiar with using return statements to return a value from functions. However, returning an object using return interface can yield performance benefits, particularly when working with large datasets. In this blog post, we will compare the performance of using return object with return interface and demonstrate how it can improve the efficiency of your code.

In Golang, a function can return a single value or multiple values. When returning a single value, we use the syntax:

func functionName() returnDataType {
    return value
}

For example, if we want to return an integer from a function, we write:

func getValue() int {
    return 42
}

When returning multiple values, we use the syntax:

func functionName() (returnDataType1, returnDataType2, ...) {
    return value1, value2, ...
}

For example, if we want to return two integers, we write:

func getValues() (int, int) {
    return 42, 100
}

In Golang, a return interface allows you to return an object from a function without knowing its type beforehand. Instead of specifying the return type, you define an interface that the object must satisfy for it to be returned.

To use return interface, we define the interface we want to return as:

type MyInterface interface {
    // Define interface methods here
}

Then, we define our function using the interface as the return type:

func myFunction() MyInterface {
    // Return object that satisfies the interface here
}

For example, let’s say we have a struct Person that contains a name and age field. We can define an interface Getter that requires a GetName() and GetAge() method:

type Person struct {
    Name string
    Age  int
}

type Getter interface {
    GetName() string
    GetAge()  int
}

func (p Person) GetName() string {
    return p.Name
}

func (p Person) GetAge() int {
    return p.Age
}

func getPerson() Getter {
    return Person{Name: "Bob", Age: 30}
}

In this example, when getPerson() is called, it returns a Getter object that is satisfied by the Person struct. This allows us to work with the returned object without knowing its exact type.

Section titled Comparing%20Performance%20of%20Return%20Object%20vs%20Return%20Interface

Comparing Performance of Return Object vs Return Interface

To compare the performance of returning an object vs returning an interface in Golang, let’s create a simple benchmark test.

We will create two functions: returnObject() that returns a Person object and returnInterface() that returns a Getter interface object.

func returnObject() Person {
    return Person{Name: "Bob", Age: 30}
}

func returnInterface() Getter {
    return Person{Name: "Bob", Age: 30}
}

We will run the benchmark test for 1 million iterations using the testing package:

func BenchmarkReturnObject(b *testing.B) {
    for i := 0; i < b.N; i++ {
        returnObject()
    }
}

func BenchmarkReturnInterface(b *testing.B) {
    for i := 0; i < b.N; i++ {
        returnInterface()
    }
}

After running the benchmarks, we get the following results:

 go test -benchmem -run=^$ -bench BenchmarkReturn
goos: linux
goarch: amd64
pkg: github.com/h4ckm03d/golang-playground/6-practical-benchmark
cpu: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
BenchmarkReturnObject-4                 1000000000               0.2712 ns/op          0 B/op          0 allocs/op
BenchmarkReturnInterface-4              1000000000               0.2683 ns/op          0 B/op          0 allocs/op
PASS
ok      github.com/h4ckm03d/golang-playground/6-practical-benchmark     0.913s

As we can see, using return interface is slightly faster than returning an object in this case. This performance advantage may become more significant when working with larger datasets or more complex object structures. Let’s see what happen in the build time process

  • Using return object
❯ go build -gcflags="-m" returnobject/main.go
# command-line-arguments
returnobject/main.go:17:6: can inline Student.GetName
returnobject/main.go:21:6: can inline Student.GetAge
returnobject/main.go:25:6: can inline getStudent
returnobject/main.go:30:17: inlining call to getStudent
returnobject/main.go:31:22: inlining call to Student.GetAge
returnobject/main.go:31:35: inlining call to Student.GetName
returnobject/main.go:31:13: inlining call to fmt.Println
<autogenerated>:1: inlining call to Student.GetAge
<autogenerated>:1: inlining call to Student.GetName
returnobject/main.go:17:7: leaking param: p to result ~r0 level=0
returnobject/main.go:21:7: p does not escape
returnobject/main.go:31:13: ... argument does not escape
returnobject/main.go:31:22: ~R0 escapes to heap
returnobject/main.go:31:35: ~R0 escapes to heap
  • Using return interface
golang-playground/6-practical-benchmark on  master [?] via  v1.20
❯ go build -gcflags="-m" returninterface/main.go
# command-line-arguments
returninterface/main.go:17:6: can inline Student.GetName
returninterface/main.go:21:6: can inline Student.GetAge
returninterface/main.go:25:6: can inline getStudent
returninterface/main.go:30:17: inlining call to getStudent
returninterface/main.go:31:13: inlining call to fmt.Println
<autogenerated>:1: inlining call to Student.GetAge
<autogenerated>:1: inlining call to Student.GetName
returninterface/main.go:31:22: devirtualizing s.GetAge to Student
returninterface/main.go:31:35: devirtualizing s.GetName to Student
returninterface/main.go:17:7: leaking param: p to result ~r0 level=0
returninterface/main.go:21:7: p does not escape
returninterface/main.go:26:16: Student{...} escapes to heap
returninterface/main.go:30:17: Student{...} does not escape
returninterface/main.go:31:13: ... argument does not escape
returninterface/main.go:31:22: Student.GetAge(s.(Student)) escapes to heap
returninterface/main.go:31:35: Student.GetName(s.(Student)) escapes to heap

In the first build using return object, the result of the function is an object of type Student, which has methods GetName and GetAge. The compiler is able to inline these methods and the function getStudent that creates the Student object, which means that there is no heap allocation for the Student object. However, the result object, which is a copy of the Student object, does escape to the heap.

On the other hand, in the second build using return interface, the result of the function is an interface type, which is implemented by the Student object. This means that the compiler needs to use interface methods to access the GetName and GetAge methods of the Student object, which cannot be inlined. However, because the result is an interface, the Student object can be allocated on the heap and passed around as a pointer. In this build, the Student object is allocated on the heap, but the result interface does not escape to the heap.

Using return interface in Golang can yield performance benefits when working with large datasets or complex object structures. By defining an interface as the return type, we can return an object without knowing its exact type beforehand. This approach allows for more flexible and efficient code, improving the overall performance of our programs.

practical go