It’s time for the FUNC 🪩🕺 Can you handle it?
Function that is and of course you can handle it! 😄 We’ve been using them this
entire time. We’ve needed some preliminary exposure to other keywords before we
can dig into function. So let’s get func
-y 😹 and boogie on down to our next
lesson.
In it’s purest form a function takes in zero to many inputs and produces one to
many outputs. We can think of the simple addition operator +
as a function,
because reliably every time you put a number to its left and put a number
to its right then outputted is another number 4 + 5 --> 9
. That means +
would be a function that takes in two inputs and produces one output.
Setup
Let’s make our directory function
and the files we want inside of
that directory example_test.go
function.go
mkdir function
touch function/example_test.go function/function.go
Now let’s open up function.go
and for the very first line we’ll add
package function
Next for example_test.go
for the very first line we’ll add
package function_test
We can import basics/function
into cmd/main.go
and run functions
from there with go run cmd/main.go
and also to run our example_test.go
👍
we use go test function/example_test.go
in the commandline.
Private Functions
This is going to be the first time we’ll be explicitly talking about package
visibility, but we’ve been using it this entire time. In Go there is no
keywords dedicated to private
or public
things. You make something public
by using TitleCase and you make something private by using camelCase.
Why would someone want a private… anything? 🤔 First off it helps with readability, if a variable or function is private I know that it isn’t used outside of a package. Where as if something is Public … It could be used anywhere 😵 Second it can help break up code into small easily understood units. Sometimes functions can span 100s of lines❗ It makes code much easier to read seeing small private function calls. Let’s not do a full example but we can read this:
func EvaluateSpeed(difficulty string) (int, error) {
test, err := createTest(difficulty)
if err != nil {
return 0, err
}
g := findWillingGopher(test)
seconds, err := doTypingTest(g)
if err != nil {
return 0, err
}
return seconds, nil
}
We don’t know what each of those calls are doing but it reads like a story.
- Create a test with a difficulty.
- If error (difficulty doesn’t exist, maybe?) return error
- Find a gopher who wants to participate in the typing test
- Have gopher do the test, which produces how long it took or an error
- If error (spilled tea 🍵 on keyboard, maybe?) return error and 0 seconds
- Return how long it took them
Coding Time!
function.go
// privateFunc is an example function, that is not exported. It is not always
// necessary to document unexported or private functions, because they won't
// show up in any documentation. It is only a good idea when the function does
// something that you wouldn't expect and should explain it.
func privateFunc() {
fmt.Println("This function can only be called from within this package.")
}
example_test.go
// This will not work, uncomment it and see what error it gives you.
// func ExampleprivateFunc() {
// // NOTE(jay): privateFunc not exported by package functions
// function.privateFunc()
// }
Public Functions
Now over to the Public side of things. We’ve been using them the entire time,
but lets understand why. When we create our tests we are under a different
package functions_test
this is by design, and WOW what a good design it
is. 👏👏👏 We get to experience what our clients would use and we test our
application’s behavior.
I could go on for days about how well the package <MY_PACKAGE>_test
is
designed, but all we need to know is we are making our functions public so that
we can call them outside of the package and yes, even though they look very
similar (functions
and functions_test
) they are completely separated
packages.
This means you’ve known how to call functions outside of the packages we’ve
been creating this entire time! 😁 So don’t be afraid when calling something
like fmt.Printf
. You’re literally doing the same thing that the language
designers did in their package fmt
and would you look at that 😲, they even
have a
fmt_test
package. 😄
Coding Time!
function.go
// Public is an example function, that is exported. It is always a good
// idea to document your exported functions and variables, so that other
// developers can know how to use your code! Run `go doc --all .` in your
// terminal in this package and see what you get!
func Public() {
fmt.Println("This function is exported and can be called anywhere.")
}
example_test.go
func ExamplePublic() {
function.Public()
// Output:
// This function is exported and can be called anywhere.
}
Adding Parameters To Functions
A parameter is what you call your inputs to a function. Let’s return to our
example with +
. When adding two numbers we can generalize it to:
(numberOne int) + (numberTwo int) --> int
We call numberOne
and numberTwo
parameters.
You may also here some people refer to them as arguments. In most contexts it’s fine to use parameter and argument interchangeably, but know there is a difference between the two and when we talk about making a function the correct word is parameter. When calling the already made function, you pass arguments. Everyone would understand if you said parameter or argument to a function though, so no big deal.
Coding Time!
function.go
// WithParams is an example function, that shows you how to pass in
// multiple arguments to a function and use them.
func WithParams(name string, value int, emoji rune) {
fmt.Printf("%s looks like %s and is a %d/10", name, string(emoji), value)
}
example_test.go
func ExampleWithParams() {
function.WithParams("Mechanical Arm", 9, '🦾')
// Output:
// Mechanical Arm looks like 🦾 and is a 9/10
}
Return an Output
A true function has an output. If you have a function that takes in inputs but doesn’t have outputs, that’s referred to as a procedure (There’s a famous style of programming called – procedural programming), but like before it’s not really important to know that. If you call a function with no outputs a function, it’s fine.
When we want to return something from our function it works like the example of
the plus operator (numberOne int) + (numberTwo int) --> int
and now let’s
turn it Go style!
func Add(numberOne int, numberTwo int) int { return numberOne + numberTwo }
Coding Time!
function.go
// WithReturn is an example function on how to specify what type you want a
// function to return.
func WithReturn() string {
return "It's just this easy to return a type"
}
example_test.go
func ExampleWithReturn() {
fmt.Println(function.WithReturn())
// Output:
// It's just this easy to return a type
}
Return Multiple Types
A very nice feature to have in Go is returning whatever we want and how many we want. There are other languages that don’t allow for this, which if you’re unlucky enough to use those languages, then you have to do wacky 🤪 things to return your multiple types in some ugly, hackish manner.
This won’t work with our previous example so let’s think of modifying it 🤔 Maybe we want to return a number and a boolean if the number is greater than 1000, so that we can do something special with it.
func Add(n1 int, n2 int) (int, bool) {
sum := n1 + n2
if sum > 1000 {
return sum, true
}
return sum, false
}
Coding Time!
function.go
// WithMultipleReturn is an example function that will return two types at
// the same time.
func WithMultipleReturn() ([]int, bool) {
canDoMultipleReturns := true
return []int{1, 2, 3, 4, 5}, canDoMultipleReturns
}
example_test.go
func ExampleWithMultipleReturn() {
fmt.Println(function.WithMultipleReturn())
// Output:
// [1 2 3 4 5] true
}
Returns With Names!
With our last example it wasn’t really obvious why we were returning true
or false
. You had to read the source code to know why the function would
return true
or false
. So how can we fix that? 🤔 Well, two things✌️
documentation and documentation 😂 We can document what the function does
and we can document the return types with a name! 🤯
// Add takes in two numbers adds them together and checks if they are over
// 1000. It always returns the sum and for the second output returns true if
// the sum is over (greater than) one thousand.
func Add(n1 int, n2 int) (sum int, gtThousand bool) {
sum = n1 + n2
if sum > 1000 {
return sum, true
}
return sum, false
}
Coding Time!
function.go
// WithNamedReturn is an example function that shows how you can name all
// of your parameters and all of your return types if you want to. You will
// notice we don't have to specify the type over and over if they are the same
// type. i.e. (email string, url string) == (email, url string)
func WithNamedReturn(name, scheme, host, path, query string) (email, url string) {
// Notice we don't use `:=` for email and url. The function already makes
// them for us when we named them up above.
email = name + "@" + host
url = scheme + host + path + query
// An empty return will look for your 2 named returns and output them.
return
// return email, url also works! And is more readable, so go with this. 👍
}
example_test.go
func ExampleWithNamedReturn() {
fmt.Println(function.WithNamedReturn("Gamba",
"https://", "gophergo.dev", "/fun-with-funcs", "?isFun=yes&isEasy=yes"))
// Output:
// Gamba@gophergo.dev https://gophergo.dev/fun-with-funcs?isFun=yes&isEasy=yes
}
Variadic Arguments (Varargs)
What in the GREAT GOOGLY MOOGLY 👀 is a variadic anything ⁉️ Great
question! 😁 It has some formal definition that we don’t care about. All we
need to know is – it is usually shortened to varargs
and means you can have a
variable number of arguments (parameters) of a certain type
. More plainly,
the function can take zero, one, or many parameters of a certain type
.
It, essentially, gives more control 🎛️ to the caller (the person calling our
function), but makes our lives (the writer of the function) a little more
difficult. I’ll explain more during coding in example_test.go
.
So how do we get this fancy stuff into our functions? Well with an operator of
course; the ...
operator. According to the
language
specification
there is no name for the ...
operator 😥 In other languages they have similar
operators with names, (splat/spread), but our ...
operator doesn’t just pull
out all values (from a slice), it also puts them all in. So we’ll take a page
out of the Lua programming language and call it the
pack/unpack
operator.
Coding Time!
function.go
When we use the pack operator ...
it will take all of the values and turn it
into a slice of a type, for this we make a slice of int []int
and as we
figured out in the previous lesson on
range
we
can loop through a slice nicely with range
.
// Variadic is an example function. It takes in an arbitrary amount of
// `int`s and allows you to use all of them, the way you see fit. This can be
// seen as a more powerful version of `[]int`, and it works for all types.
func Variadic(varargsNums ...int) (sum int) {
for _, n := range varargsNums {
sum += n
}
return sum
}
example_test.go
Here we give the caller the ability to pass in no values, some values, or an
entire slice with the unpack operator ...
We briefly looked at this on our
lesson with
slice
We can see how convenient that it
for them, but for more complex functions we may need to do checks for no values
or other edge cases.
func ExampleVariadic() {
fmt.Println(function.Variadic())
fmt.Println(function.Variadic(1, 2, 3))
nums := []int{4, 5, 6, 7, 8, 9, 10, 11, 12}
fmt.Println(function.Variadic(nums...))
// Output:
// 0
// 6
// 72
}