Skip to main content

Methods and Interfaces

For developers with .NET (C#) experience, this guide covers method receivers, implicit interface satisfaction, and designing small interfaces.

1. Methods and Receivers​

Go has no classes. You define methods on any named type as functions with a receiver.

type Counter struct {
n int
}

// Pointer receiver: can modify the original value
func (c *Counter) Inc() { c.n++ }

// Value receiver: operates on a copy
func (c Counter) Value() int { return c.n }

Methods can be written outside the type definition (in any file of the same package). It feels similar to C# extension methods, but you cannot add methods to types from other packages.

Value vs. Pointer Receivers​

AspectPointer receiver (c *T)Value receiver (c T)
Mutating stateYesNo (only the copy changes)
Copy costSmallHigh for large structs
Contains sync.Mutex etc.Required (must not be copied)Not allowed
Suited forStateful types, servicesSmall immutable values (e.g. time.Time)

Guidelines:

  • When in doubt, use a pointer receiver
  • Keep receivers consistent for a type (do not mix value and pointer receivers)
  • Small, non-mutating value types (coordinates, money) can use value receivers
var c Counter
c.Inc() // for an addressable variable, interpreted as (&c).Inc()
fmt.Println(c.Value())

2. Implicit Interface Satisfaction​

Go interfaces use structural typing. There is no implements declaration; a type satisfies an interface automatically if it has the required methods.

type Notifier interface {
Notify(msg string) error
}

type EmailNotifier struct{ Addr string }

func (e *EmailNotifier) Notify(msg string) error {
fmt.Printf("send to %s: %s\n", e.Addr, msg)
return nil
}

// EmailNotifier satisfies Notifier without any declaration
var n Notifier = &EmailNotifier{Addr: "[email protected]"}

Benefits​

  • Loose coupling between definition and use: the implementer need not know about the interface. Consumers can define their own interfaces for types from the standard library or third-party packages
  • The consumer defines the interface, per Go convention, declaring only the methods it needs
  • Mocks and stubs for testing are easy to write

Caveats​

A type may satisfy an interface unintentionally, or fail to satisfy it when you thought it did. To get a compile-time guarantee, use this idiom:

// Compile error if *EmailNotifier does not satisfy Notifier
var _ Notifier = (*EmailNotifier)(nil)

Method Set Pitfall​

The method set of a value T contains only value-receiver methods, while *T contains both. If you implement with a pointer receiver, assigning a value to the interface is an error.

var n Notifier = EmailNotifier{} // compile error: Notify is a method of *EmailNotifier
var n Notifier = &EmailNotifier{} // OK

3. Mapping to .NET​

.NET (C#)Go
class Foo : IBar (explicit implementation)Structural typing (satisfied implicitly)
Extension methodsMethods with receivers (same package only)
Inheritance (class A : B)No inheritance. Delegate via struct embedding
Abstract classes / virtual / overrideNone. Use interfaces and function values
object / dynamicany / type assertion / type switch
is / asType assertion v, ok := x.(T)
Type pattern matching in switchType switch
Default interface implementationsNone
IDisposableio.Closer (Close() error)
IEnumerable<T>iter.Seq[T] (Go 1.23+) / slices

4. Designing Small Interfaces​

The Go standard library has many famous interfaces with just one method.

type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }

Guidelines:

  • Keep interfaces small (1–3 methods). The larger they are, the harder to implement and swap
  • Do not put a large IUserRepository-style interface on the implementation side. Define only what the consumer needs, on the consumer side
  • Accept interfaces, return structs: use interfaces for parameters and concrete types for return values
  • Do not create an interface up front when there is only one implementation
// The consumer (service package) declares only the methods it needs
type userStore interface {
FindByID(ctx context.Context, id int) (*User, error)
}

type UserService struct {
store userStore
}

// Return a concrete type; callers can fit it to an interface as needed
func NewUserService(store userStore) *UserService {
return &UserService{store: store}
}

Interface Composition​

Small interfaces can be combined into larger ones.

type ReadWriter interface {
Reader
Writer
}

Struct Embedding​

Instead of inheritance, you can embed a struct to promote its methods and write delegation concisely.

type Logger struct{ prefix string }

func (l *Logger) Log(msg string) { fmt.Println(l.prefix + msg) }

type Service struct {
*Logger // embedded: you can call s.Log(...)
name string
}

s := Service{Logger: &Logger{prefix: "[svc] "}, name: "api"}
s.Log("started")

This is delegation, not an is-a relationship. You cannot treat a Service as a *Logger.

5. Type Assertions and Type Switches​

Use a type assertion to extract a concrete type from an interface value.

var x any = "hello"

s, ok := x.(string) // ok form: does not panic on failure
if ok {
fmt.Println(len(s))
}

n := x.(int) // without ok: panics on type mismatch

Use a type switch to branch over multiple types.

func describe(x any) string {
switch v := x.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int: %d", v)
case string:
return "string: " + v
case fmt.Stringer:
return "stringer: " + v.String()
case error:
return "error: " + v.Error()
default:
return fmt.Sprintf("other: %T", v)
}
}

Avoid overusing any, since it loses type safety. First consider whether generics (Generics in Go) or a small interface can express it.

6. Common Pitfalls for C# Developers​

The Nil Interface Trap​

An interface value is a (type, value) pair. Assigning a nil pointer makes the interface itself non-nil.

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func run() error {
var e *MyErr = nil
return e // carries type info, so the returned error is not nil
}

if err := run(); err != nil {
fmt.Println("treated as an error!")
}

Fix: explicitly return nil when there is no error, and never return a concrete-typed variable as error.

Other Points​

  • No implements: use the IDE's "go to implementation" and compile-time assertions to see what implements what
  • Where to put interfaces: unlike C#, do not put them in the implementing project; put them in the consuming package
  • Passing a value of a pointer-receiver type: causes a compile error due to method sets
  • No inheritance: embedding is handy but does not provide overriding or polymorphism
  • No method overloading: only one method per name. Use distinct names (ParseInt / ParseFloat)
  • Getter/setter naming: use Name() rather than GetName(); SetName() is kept as is

7. Code Example​

C#​

public interface INotifier
{
Task NotifyAsync(string message);
}

public class EmailNotifier : INotifier
{
public Task NotifyAsync(string message) => Task.CompletedTask;
}

public class OrderService
{
private readonly INotifier _notifier;
public OrderService(INotifier notifier) => _notifier = notifier;

public Task PlaceAsync() => _notifier.NotifyAsync("placed");
}

Go​

package main

import (
"context"
"fmt"
)

// The consumer (OrderService) declares only the methods it needs
type notifier interface {
Notify(ctx context.Context, msg string) error
}

type OrderService struct {
notifier notifier
}

func NewOrderService(n notifier) *OrderService {
return &OrderService{notifier: n}
}

func (s *OrderService) Place(ctx context.Context) error {
return s.notifier.Notify(ctx, "placed")
}

// The implementation does not know about notifier
type EmailNotifier struct{}

func (EmailNotifier) Notify(ctx context.Context, msg string) error {
fmt.Println("email:", msg)
return nil
}

// A test stub takes only a few lines
type fakeNotifier struct{ got []string }

func (f *fakeNotifier) Notify(_ context.Context, msg string) error {
f.got = append(f.got, msg)
return nil
}

func main() {
svc := NewOrderService(EmailNotifier{})
_ = svc.Place(context.Background())
}

8. Summary​

  • Methods are defined on any named type with a receiver. When in doubt, standardize on pointer receivers
  • Interfaces are satisfied implicitly. No implements is needed, and the consumer defines only what it needs
  • Prefer small interfaces and follow Accept interfaces, return structs
  • There is no inheritance; reuse code through embedding (delegation) and interface composition
  • Extract types with v, ok := x.(T) and type switches. Avoid overusing any
  • Beware that an interface holding a nil pointer is not nil

References​