Function parameters in golang

Today I want to share a simple concept that I found useful when writing functions in go. Before jumping in, I want to talk about context to better illustrate the use case.

A couple of years ago, I developed a simple CLI tool to interact with packet cloud using their go SDK . As you may guess, the CLI tool had to implement all the major operations/functions provided by the cloud, meaning create, start, stop servers, manage the network environment etc … The first step was to create a function for each operation, and each function would define the options and parameters that would be submitted to the corresponding packet API. I wanted to have these wrapper functions so that the CLI logic would not have to care about how to create the packet client, or how to call the API, but only focus on the user interface, meaning define the CLI parameters, help contents etc… Below is an example of one of the wrapper functions.

func CreateDevice(projectID, hostname, plan, facility, operatingSystem, billingCycle, userData, ipxeScriptURL string, tags []string, spotInstance, alwaysPXE bool, spotPriceMax float64, terminationTime *time.Time, silent bool) error {
	var err error
	// Create the device
	return err
}

Looking at this code now, I’m like, what was I thinking??? I think that a better approach would be to define a struct whose purpose would be to pass the parameters to the function. In fact, the guys at packet actually included in the SDK these structs for most of the API calls that take many options:
Example: here is an illustration with a device create request parameters in a struct

// DeviceCreateRequest type used to create a Packet device
type DeviceCreateRequest struct {
	Hostname              string    
	Plan                  string    
	Facility              []string  
	OS                    string    
	BillingCycle          string    
	ProjectID             string    
	UserData              string    
	Storage               string    
	Tags                  []string  
	IPXEScriptURL         string    
	PublicIPv4SubnetSize  int       
	AlwaysPXE             bool      
	HardwareReservationID string    
	SpotInstance          bool      
	SpotPriceMax          float64   
	TerminationTime       *Timestamp
	CustomData            string    
	UserSSHKeys           []string  
	ProjectSSHKeys        []string  
	Features              map[string]string        
	IPAddresses           []IPAddressCreateRequest 
}

So a refactored CreateDevice wrapper function would look like this:

func CreateDevice(p *DeviceCreateRequest) error {
	var err error
	// Create the device
	// We can access the parameters through *DeviceCreateRequest
	return err
}

In this example, the structs are already included in the SDK, but even if they are not present, I think it still makes sense to create them as needed for the following reasons.

Readability

Compare the two function declarations to see what I mean. All the function parameters in the first function are difficult to read and also difficult to document. By using a struct, the function declaration can be more consise and easy to read. On top of that, each parameter in the struct can be easily documentated. In the packngo SDK, you can see how some relevant options are documented in the struct:

type DeviceCreateRequest struct {
	// Skipping all the other parameters to make illustration
	// shorther

	// UserSSHKeys is a list of user UUIDs - essentialy
	// a list of collaborators. The users must be a collaborator
	// in the same project where the device is created.
	// The user's SSH keys then go to the device.
	UserSSHKeys           []string
	// Project SSHKeys is a list of SSHKeys resource UUIDs.
	// If this param is supplied, only the listed SSHKeys will go
	// to the device.
	// Any other Project SSHKeys and any User SSHKeys will not
	// be present in the device.
	ProjectSSHKeys        []string  
}

Handling of default values

The function caller only has to set the parameters(options) that they are interested in. The other ones can be checked and provided with default values inside the function:

func CreateDevice(p *DeviceCreateRequest) error {
	var err error
	// Before creating the device, we can set default values
	// when necessary
	if p.OS == "" {
		p.OS = "ubuntu"
	}
	// create the device...
	return err
}

Then the caller only has to set mandatory options, and let the CreateDevice function set default values when needed:


func main () {
	// Call CreateDevice
	err := CreateDevice(&CreateDeviceRequest{
		Hostname: "create-device-001",
		// The CreateDevice function can take care of setting
		// default values for all the other parameters.
	})
	if err != nil {
		panic(err)
	}
}

By comparison, the caller would have to set all the possible options for a device create request in the first function, even though Hostname is the only desired option in this case.

Higher maintainability

With a struct as function parameter, you can easily modify the parameters without changing the function declaration. At least in this case, it is easy to add new optional paramters to create a device, without breaking any code that rely on the CreateDevice function.

Conclusion

I’m not sure if this is a definitive best practice, but I found it very usefull to define function paramters in a struct, especially when you have to deal with multiple paramters:

// DoSomethingParameters represents the parameters of
// the `DoSomething` function
type DoSomethingParameters struct {
	ParamOne string
	ParamTwo int
}

func DoSomething(p *DoSomethingParameters) error {
	var err error
	// do something
	return err
}

Well that’s it.