What is a function?

A function is a separate and reusable block of code which can be runned again and again. Functions may accept input values and they may return output values.

Why do we need functions?

  • Increasing the readability, testability and maintainability
  • Making some part of a code separately executable
  • Composing things from smaller things
  • Adding behavior to types
  • Organizing the code
  • To be DRY

Declares a function named “Len” with “s” as an input param with a type of “String” and a result type of “int”.

✪ First: Declare the Len func

func Len(s string) int { 
return utf8.RuneCountInString(s)
}

✪ Second: Call it by its name

Len("Hello world 👋")

Input Parameters and Result Types

Input params are used to pass values to funcs. Result types are used to return data from funcs. The returned values from a func named as the “result values”.

Takes an int type “input param” named as “s” and returns a “result type” as int type without a name.

A signature is a func’s type — consisting of the types of its input params and its result types.

func jump()
// signature: func()
func Len(s string) int
// signature: func(string) int
func multiply(n ...float64) []float64
// signature: func(...float64) []float64

Funcs in Go are first-class values which can be passed around.

flen := Len
flen("Hello!")
An example code with an exercise to func signatures

When a func is called, its body will run with the provided input params. The func will return one or more result values if at least one result type was declared.

You can directly return from RuneCountInString func, because it returns an int as well.

func Len(s string) int {
return utf8.RuneCountInString(s)
}
lettersLen := Len("Hey!")

This func uses an expression next to a return.

func returnWithExpression(a, b int) int {
return a * b * 2 * anotherFunc(a, b)
}

Func Block

Every bracket group creates a new block and any declared identifiers are only visible inside that block.

const message = "Hello world 👋"
func HelloWorld() {
name := "Dennis"
message := "Hello, earthling!"
}
HelloWorld()
/*
 message constant is visible here.
name variable inside the func is not visible here.
shadowed message variable inside the func is not visible here.
*/

Now, let’s see the different styles of the input params and the result types declarations.

Declares an input param named “s” with a type of “String” and an integer result type.

A func’s input params and result types act as variables.

Niladic funcs

A niladic func doesn’t take any input params.

func tick() {
fmt.Println( time.Now().Format( time.Kitchen ) )
}
tick()
// Output: 13:50pm etc.
If a func has no result value, you can omit the result type and the return keyword.

Singular funcs

func square(n int) int {
return n * n
}
square(4)
// Output: 16
When a func returns just one result type, don’t use parentheses.

Multiple input params and result types

func scale(width, height, scale int) (int, int) {
return width * scale, height * scale
}
w, h := scale(5, 10, 2)
// Output: w is 10, h is 20
Multiple result types should be wrapped in parentheses.

Automatic type-assignment

Types are automatically declared for the previous params.

These declarations are the same:

func scale(width, height, scale int) (int, int)
func scale(width int, height int, scale int) (int, int)

Error value

Some funcs conventionally return errors — multiple result values makes this convenient to use.

func write(w io.Writer, str string) (int, error) {
return w.Write([]byte(s))
}
write(os.Stdout, "hello")
// Output: hello

Directly returning from the Write func is the same as returning multiple result types. Because, it also returns an int and an error value.

func write(w io.Writer, str string) (int, error) {
n, err := w.Write([]byte(s))
return n, err
}

If everything is fine you can just return nil as a result:

func div(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
  return a / b, nil
}
r, err := div(-1, 0)
// err: divide by zero

Discarding the result values

You can use the blank-identifier to discard the result values.

/*
Suppose that we have a func like this:
*/
func TempDir(dir, prefix string) (name string, err error)

Discarding the error value (2nd result value):

name, _ := TempDir("", "test")

Discarding all the result values:

TempDir("", "test")

Omitting the param names

You can also use the blank-identifier in the unused input params as a name — to satisfy an interface, for an example (or this).

func Write(_ []byte) (n int, err error) {
return 0, nil
}

Named result params let you use the result values as variables and they enable you to use a naked return.

Pos result value acts like as a variable and the biggest func returns it with a naked return (without any expressions next to it).

// biggest returns the biggest number's index in nums
func biggest(nums []int) (pos int) {
  if len(nums) == 0 {
return -1
}
  m := nums[0]
  for i, n := range nums {
if n > m {
m = n
pos = i
}
}
  // returns the pos
return
}
pos := biggest([]int{4,5,1})
// Output: 1
Above example is highly unoptimized — O(n).

When to use named result params?

  • Named result params are mostly used as a hint for the result values.
  • Do not use them just to skip declaring variables inside a func.
  • Use them if they make your code more readable.

There is also a controversial optimization trick when you use the named result values, however the compiler probably will be fixed soon to negate this.

Be careful about the shadowing problem

func incr(snum string) (rnum string, err error) {
var i int
  // start of a new scope
if i, err := strconv.Atoi(snum); err == nil {
i = i + 1
}
// end of the new scope
  rnum = strconv.Itoa(i)

return
}

incr("abc") 
// Output: 0 and nil

The variables, i and err are only visible inside the if block. In the end, the err should not have been “nil”, because, “abc” couldn’t be converted to an integer and there was an error, but we missed it.

See the solution to the shadowing problem — here

Pass-by-value

Pass func sets the input params’ values to their zero-values:

func pass(s string, n int) {
s, n = "", 0
}

We pass two variables to pass func:

str, num := "knuth", 2
pass(str, num)

After the func ends, our variables are still the same.

str is "knuth"
num is 2

This is because, when we pass params to a func, they’re copied automatically as new variables. This is called pass-by-value.

Pass-by-value and the pointers

Below func accepts a pointer to a string variable. It changes the value the ps pointer points to. Then it will try to set the passed pointer’s value to nil. So, the pointer will not point to the passed string variable’s address anymore.

func pass(ps *string) {
*ps = "donald"
ps = nil
}

We define a new variable s and then we take its address by using the ampersand operator and store its address inside a new pointer variable: ps.

s := "knuth"
ps := &s

Let’s pass ps to the pass func.

pass(ps)

After the func ends, we see that the value inside the s variable has changed. But, the ps pointer still points to a valid address of the s variable.

// Output: 
// s : "donald"
// ps: 0x1040c130

The ps pointer is passed-by-value to the pass func, only the address it points to get copied to a new pointer variable inside the pass func. So, setting it to nil inside the func had no effect on the passed pointer’s value.

`&s` and `ps` are different variables, but they all point to the same `s` variable.

Let’s run the code to understand it better.

This completes the parameter declaration styles of a func.

Now, let’s see how to name the funcs and the input params and the result types properly.

Naming funcs

Some of the goals of using a func is to increase the readability and the maintainability of the code. You may need to bend these suggestions depending on the problem.

Be a Minimalist

When choosing names be a minimalist. Prefer short, descriptive and meaningful names.

// Not this:
func CheckProtocolIsFileTransferProtocol(protocolData io.Reader) bool
// This:
func Detect(in io.Reader) Name {
return FTP
}
// Not this:
func CreateFromIncomingJSONBytes(incomingBytesSource []byte)
// This:
func NewFromJSON(src []byte)

Name in MixedCaps

// This:
func runServer()
func RunServer()
// Not this:
func run_server()
func RUN_SERVER()
func RunSERVER()

Acronyms should be all uppercase:

// Not this:
func ServeHttp()
// This:
func ServeHTTP()

Choose descriptive param names

// Not this:
func encrypt(i1, a3, b2 byte) byte
// This:
func encrypt(privKey, pubKey, salt byte) byte
// Not this:
func Write(writableStream io.Writer, bytesToBeWritten []byte)
// This:
func Write(w io.Writer, s []byte)
// Types make it clear, no need for the names

Use verbs

// Not this:
func mongo(h string) error
// This:
func connectMongo(host string) error
// If it's in Mongo package, just:
func connect(host string) error

Use is / are

// Not this:
func pop(new bool) item
// This:
func pop(isNew bool) item

Omit types in the name

// Not this:
func show(errorString string)
// This:
func show(err string)

Getters and Setters

There are no getters and setters in Go. However, you can imitate them by using funcs.

// Not this:
func GetName() string
// This:
func Name() string
// Not this:
func Name() string
// This:
func SetName(name string)

Features Go funcs don’t support:

So, you can stop Duckling or Googling for these things. There are some workarounds for some of them which I’ll write about them in the upcoming posts.

  • Func overloading — It can be imitated with type assertions.
  • Pattern matcher funcs.
  • Default parameter values in the declaration.
  • Specifying input params by name in the declaration in any order.

💓 Share this with your friends. Thank you!



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here