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
| Aspect | Pointer receiver (c *T) | Value receiver (c T) |
|---|---|---|
| Mutating state | Yes | No (only the copy changes) |
| Copy cost | Small | High for large structs |
Contains sync.Mutex etc. | Required (must not be copied) | Not allowed |
| Suited for | Stateful types, services | Small 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
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 methods | Methods with receivers (same package only) |
Inheritance (class A : B) | No inheritance. Delegate via struct embedding |
Abstract classes / virtual / override | None. Use interfaces and function values |
object / dynamic | any / type assertion / type switch |
is / as | Type assertion v, ok := x.(T) |
Type pattern matching in switch | Type switch |
| Default interface implementations | None |
IDisposable | io.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 thanGetName();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
implementsis 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 overusingany - Beware that an interface holding a nil pointer is not nil