Skip to content

Instantly share code, notes, and snippets.

@travisjeffery
Last active April 23, 2023 11:13
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save travisjeffery/8265ca411735f638db80e2e34bdbd3ae to your computer and use it in GitHub Desktop.
Save travisjeffery/8265ca411735f638db80e2e34bdbd3ae to your computer and use it in GitHub Desktop.
How to do functional options in Golang

Here's the simplest example showing how to do functional options in Golang.

They're a great way to enable users to set options and ease adding new options later.

package main

import (
	"flag"
	"fmt"
)

// This is your function used by users to set options.
func Host(host string) func(*Server) {
	return func(s *Server) {
		s.Host = host
	}
}

// This is another function used by users to set options.
func Port(port int) func(*Server) {
	return func(s *Server) {
		s.Port = port
	}
}

// This is the type whose options you're enabling users to set.
type Server struct {
	Host string
	Port int
}

// This is your creator function that accepts a list of option functions.
func NewServer(opts ...func(*Server)) *Server {
	s := &Server{}

	// call option functions on instance to set options on it
	for _, opt := range opts {
		opt(s)
	}

	return s
}

func main() {
	var host = flag.String("host", "127.0.0.1", "host")
	var port = flag.Int("port", 8000, "port")
	flag.Parse()

	// This is how your user sets the options.
	s := NewServer(
		Host(*host),
		Port(*port),
	)

	fmt.Printf("server host: %s, port: %d", s.Host, s.Port)
}
@logrusorgru
Copy link

I'm not sure that functional options are good choose https://gist.github.com/logrusorgru/0d0f4ac5d7b95f79b7884b5664658bc9

@dustinevan
Copy link

dustinevan commented Nov 22, 2017

I'm still not sold on these. In this situation just say s := &Server{ Host: host, Port: port } I feel like they defeat the original intended flexibility of the struct itself.

@etsangsplk
Copy link

etsangsplk commented May 17, 2018

@dustinevan
But we can without changing the code and does these ... kinda like construction overloading.. which is not native in golang
NewServer()
NewServer(Host(*host))
.....

func main() {
	var host = flag.String("host", "127.0.0.1", "host")
	var port = flag.Int("port", 8000, "port")
	flag.Parse()

	// This is how your user sets the options.
	a := NewServer()
	fmt.Printf("server host: %s, port: %d \n", a.Host, a.Port)
	b := NewServer(Host(*host))
	fmt.Printf("server host: %s, port: %d \n", b.Host, b.Port)
	s := NewServer(
		Host(*host),
		Port(*port),
	)

	fmt.Printf("server host: %s, port: %d \n", s.Host, s.Port)
	
	f := NewServer(
		Host(*host),
		Port(*port),
		Host(*host),
		Port(*port),
	)

	fmt.Printf("server host: %s, port: %d %s, port: %d \n", f.Host, f.Port,f.Host, f.Port)
}

@melekes
Copy link

melekes commented Jan 30, 2020

What if I need to validate some parameters? For example, I want to check Port is > 0 && < 10000. How should I do error handling in such case?

@garrettsparks
Copy link

@melekes to tackle validations, you'll want to change your function option signature to return an error and use that for validation purposes

for example

package main

import (
	"flag"
	"fmt"
)

type ErrPortInvalid struct {
	portValue int
}

func(e ErrPortInvalid) Error() string {
	return fmt.Sprintf("supplied port %d must be between 0 and 10000", e.value)
}

// The option is now a function that takes a pointer to a Server and returns an error if it is invalid
type option func(*Server) error

// This is your function used by users to set options.
// For this option, perform no validations.
func Host(host string) option {
	return func(s *Server) error {
		s.Host = host
                return nil
	}
}

// This is another function used by users to set options.
// For this option, validate that it is between 0 and 10000 exclusively.
func Port(port int) option {
	return func(s *Server) error {
		if port <= 0 || 10000 <= port {
			return ErrPortInvalid{port}
		}
                s.Port = port
		return nil
	}
}

// This is the type whose options you're enabling users to set.
type Server struct {
	Host string
	Port int
}

// This is your creator function that accepts a list of option functions.
func NewServer(opts ...option) (*Server, []error) {
	s := &Server{}
	errs := make([]error, 0)

	// call option functions on instance to set options on it
	for _, opt := range opts {
		err := opt(s)
		// if the option func returns an error, add it to the list of errors
		if err != nil {
			errs = append(errs, err)
		}
	}

	return s, errs
}

func main() {
	var host = flag.String("host", "127.0.0.1", "host")
	var port = flag.Int("port", 8000, "port")
	flag.Parse()

	// This is how your user sets the options.
	s, _ := NewServer(
		Host(*host),
		Port(*port),
	)

	fmt.Printf("server host: %s, port: %d\n", s.Host, s.Port)
	
	// If the user supplies an invalid port
	invalidPort := 10001
	_, errs := NewServer(
		Host(*host),
		Port(invalidPort),
	)
	// errs will be the list of validation errors
	if len(errs) > 0 {
		fmt.Println("invalid server config", errs)
	}
}

@melekes
Copy link

melekes commented Feb 10, 2020

Cool! Thank you

@doncatnip
Copy link

doncatnip commented May 7, 2020

So much boilerplate for something so simple. It's quite ridiculous. Google says they do it to avoid bad practices but instead they open pandoras box of bad practices with a restriction like this because now everyone comes up with their own ad hoc solution. It makes code potentially unreadable and forces developers to get creative. The exact opposite of go's goals.

I'm sure they will add some proper sane syntax for optional parameters at some point.

By the way not only is this 'solution' extremely verbose, it's also far from perfect. I.e. you can specify multiple Ports and Hosts. You can also set those parameters after initialization which is not exactly what you want for a Server.

Sorry for that totally off-topic rant but maybe those are necessary.

@logrusorgru
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment