I have one more tricky interview question to discuss in this series. This one is related to function receivers and methods in golang, often called nil receivers.
Question: What is the output of the following?
package main
import "fmt"
type gopher struct {
name string
}
func (r *gopher) print() {
fmt.Println("gopher-printer works!")
}
func main() {
var gpr *gopher
gpr.print()
}
A quick note here, this question has one of the highest failing rates when asked during the interviews. Most of the interviewees never tried or never experienced such behaviour.
Receiver
A receiver argument is what distinguishes a method from a regular function in GoLang. In essence, a method is simply a function that includes a receiver argument. A receiver can also be of a non-struct type. For example, in the code below, we have a method add()
with a receiver of type Integer
, which is an alias for int
.
type Integer int
func (i Integer) add(j Integer) Integer {
return i + j
}
Okay, that's a pretty straightforward representation of what a receiver is. Now, let's try answering this question. If you come from OOP languages like Java, C# or C++ you are familiar with the term null pointer exception
. It means that you are trying to access a part of something that doesn't exist. Here's the classic example from Java:
public class NullPointer f
public static void main(String[] args) {
String a = null;
System.out.println(a.length());
}
}
Trying to compile this will throw the null pointer exception
:
Exception in thread "main" java.lang.NullPointerException
at NullPointer-main (NullPointer.java:7)
The developer familiar with this concept will shortly answer:
The program will panic with nil pointer dereference if we try to run this.
panic: runtime error: invalid memory address or nil pointer dereference
Let's actually run the program and make sure.
[Running] go run "main.go"
gopher-printer works!
[Done] exited with code=0 in 0.318 seconds
Here, most interviewees get confused by this behaviour and assume that the Go compiler initialises the struct when we create the variable with the type var gpr *gopher
. To test this let's add a print line inside the print()
function like this:
func (r *gopher) print() {
fmt.Printf("receiver: %v \n", r)
fmt.Println("gopher-printer works!")
}
and build the program again.
[Running] go run "main.go"
receiver: <nil>
gopher-printer works!
[Done] exited with code=0 in 0.318 seconds
Oops, the struct is not initialised, its value is nil
. Again, if you come from OOP languages this may seem like a static method call, but it's far different from that concept. You see, receivers in Go act like a basic argument, and in this case, when we call a method on an uninitialised struct, inside the methods nil
has passed as a receiver as if you do something like this instead:
package main
import "fmt"
type gopher struct {
name string
}
func print(r *gopher) {
fmt.Println("gopher-printer works!")
}
func main() {
var gpr *gopher
print(gpr)
}
The nil receiver is just a parameter.
After we clear this out, you give a confident answer to the interviewer:
This will print a message "gopher-printer works!" into the console and will not cause a compile time panic
It's that easy!
Here some interviewers like to ask the following:
What will cause the program to panic?
The answer is very simple. Since the nil receiver is just a parameter, this panic will only occur if the method tries to access the struct fields, such as name
in our case.
type gopher struct {
name string
}
func (r *gopher) print() {
fmt.Println(r.name) // <-- accessing the field
fmt.Println("gopher-printer works!")
}
So the full answer to this tricky question is:
When you call this method from a nil pointer you will not get any compiler errors but you may get runtime panic if the method tries to access the struct fields.
Bonus section
Another interesting point to note is that a nil receiver can be initialized inside the method. However, this initialization has no effect outside of that method. Check the following code, I initialise the receiver inside the method:
package main
import "fmt"
type gopher struct {
name string
}
func (r *gopher) print() {
// init the receiver with a new struct and populate the field
if r == nil {
r = new(gopher)
r.name = "crusty0gphr"
}
fmt.Printf("hi there %s \n", r.name)
}
func main() {
var gpr *gopher
gpr.print()
}
The result of this program will be:
[Running] go run "main.go"
hi there crusty0gphr
[Done] exited with code=0 in 0.318 seconds
But inside the main
function, the variable gpr
will remain nil
and accessing its field will cause a compile time panic!
Now, It's that easy, finally!
Top comments (0)