Function inlining is a common compiler optimization for improving performance of the code.

Small, frequently called functions can be directly included (inlined) in the body of the calling function. This eliminate function calling overhead.

Go compiler doesn't provide a way to force inlining of functions.

Go compiler makes inlining decisions based on a number of heuristics.

Here's an incomplete list of attributes that disable inlining:

Checking if a function was inlined

We can find out which functions were inlined with -gcflags -m compiler flags. For example:

$ go build -gcflags -m .
# github.com/kjk/notionapi
./dbg.go:15:6: can inline log
./dbg.go:38:5: inlining call to log
./client.go:61:25: inlining call to bytes.NewBuffer
./client.go:62:5: inlining call to lo
...

Rewriting functions to improve inlining

Go only inlines very small functions. Some functions that are too big to be inlined can be split into 2 functions:

Here's an example from Go standard library. A function sync.Do():

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 1 {
		return
	}
	// Slow-path.
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

was rewritten as:

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

This optimization only makes a difference if a function is called a lot and most of the time the slow path is not called.