JavaRush /Courses /Go SELF /Functions in Go: func and beyond

Functions in Go: func and beyond

Go SELF
Level 9, Lesson 1
Available

1. Function syntax in Go

If you are writing a program and catch yourself thinking, “I already did this somewhere above,” your brain just asked for a function. A function in Go is a named piece of code that you can call by name, passing it input values (parameters), and get a result (or perform an action without a result). The key value of functions is that they turn a “wall of code” into a set of short steps: each step has a name and a clear contract.

Imagine that main is a recipe, and functions are individual sub-recipes: “chop vegetables,” “preheat the oven,” “mix the sauce.” You do not want to read a recipe where everything is written as one page-long paragraph, right? The computer, by the way, does not want to either, but it silently endures it—until the first error.

Declaration skeleton: func, name, parameters, result, body

When you first see a function declaration, it looks long, but in fact it is a very strict and repeatable pattern. In Go, a function is declared with the func keyword, then comes the name, then parentheses with parameters, after them (sometimes) the return type, and then the body in curly braces.

The simplest example: a function without parameters and without a result can look like hello() in demo code—just func hello() { ... }.

And a function with a parameter and a result looks similar: func name(param Type) ResultType { ... }.

To make it easier to “parse” function declarations at a glance, it helps to keep a table like this in mind:

Part Example Why you need it
Keyword
func
Tells the compiler: “a function is coming next”
Name
add, calc, printHeader
You call the function by this name
Parameters
(a int, b int)
Input data for computation/action
Result
int
(or nothing)
What the function returns to the calling code
Body
{ ... }
The commands executed when called

In this lecture we do not dive into multiple return values and errors as part of the signature (that is a separate big topic). For now, we work with functions that either return nothing or return one value.

Function with no result: “do an action and come back”

A very common type of function is “procedural”: it does something (for example, prints text) but returns nothing. In Go it looks simple: after the parameters you do not write any return type.

It is important to understand: “does not return a value” does not mean “does not end.” Such a function still finishes; it is just that the “result” is the fact that it ran.

package main

import "fmt"

func printHeader() {
	fmt.Println("=== MiniCalc v1 ===") // === MiniCalc v1 ===
}

func main() {
	printHeader()
}

Here printHeader is a function with no parameters and no result. It does one action: prints the header. And that is already a small win: main became a bit “cleaner,” and you can reuse printHeader() anywhere.

One more useful nuance: you can use return even in a function with no result—just to end it earlier. But you should do this only when it genuinely makes the code simpler.

package main

import "fmt"

func printIfPositive(x int) {
	if x <= 0 {
		return // print nothing and exit
	}
	fmt.Println("positive:", x) // positive: 3
}

func main() {
	printIfPositive(3)
}

Parameters: input data that lives only inside

When you pass values into a function, it is like putting them into the function’s “mailbox.” Inside the function, parameters become local variables, and their scope is limited to the function body. Outside, they do not exist.

This is convenient for two reasons at once. First, you do not clutter main with a bunch of temporary variables. Second, you cannot accidentally “break” something outside by modifying parameters (as long as we are not discussing pointers—that will come later, with new nuances).

Example with parameters:

package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func main() {
	fmt.Println(add(2, 3)) // 5
}

Notice two things. First, the parameter type is written after the name: a int. Second, parameters are not “magic”: a and b are ordinary variables, just created when the function is called.

One more small readability trick: if parameters of the same type go in a row, Go allows you to write the type only once.

package main

import "fmt"

func rectArea(width, height int) int {
	return width * height
}

func main() {
	fmt.Println(rectArea(3, 4)) // 12
}

You will see this form all the time: it is shorter and still reads just fine.

One return value and return

Functions with a result differ from “procedural” ones in that their declaration specifies the result type after the parameters, and inside the body there must be a return with a value of that type.

Returning is not just “hand back a value.” It is also the command “stop executing this function right now.” That is why in functions with branching you must ensure that any path leads to a correct return.

Mini example with a simple condition:

package main

import "fmt"

func max2(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(max2(10, 3)) // 10
}

There are two paths here. If a > b, we return a. Otherwise we return b. And what is nice: the code reads linearly, without else—you have already seen this style with if, and it works especially well in functions.

Be careful with types: return must match the declaration

One of the most common reasons beginners ask “why won’t it compile?” is a type mismatch in return. The Go compiler is fairly strict: if you declared func something(...) int, then you must return exactly an int.

Example: if we want to return a string, we need to declare the result as a string.

package main

import "fmt"

func greet(name string) string {
	return "Hello, " + name
}

func main() {
	fmt.Println(greet("Gopher")) // Hello, Gopher
}

Everything is consistent here: the result is string, and return returns a string. If we wrote func greet(name string) int, but returned a string inside, the compiler would (rightfully) complain.

3. Contract and readability

While you are learning to write functions, there is one idea that saves a lot of nerves: every function should have a clear contract. Even if you are not writing documentation comments yet, you should still be able to answer: “what does this function expect as input?” and “what does it promise to return?”

For example, rectArea(width, height int) int has an obvious contract: it takes width and height and returns the area. But a function like calc(a, b int) int without clarification is suspicious: “what exactly does it calculate?” The function name is part of the contract. That is why we try to name functions with verbs or “actions”: add, printHeader, max2, rectArea.

Another important point: do not be afraid of small functions. Beginners often think “more functions = harder.” In practice it is usually the opposite: “one 200-line function = hard,” while “10 functions of 10 lines = manageable.”

Functions also have a “quiet” trap: arguments are passed by position. That means sum(a, b) is not “put the numbers in somehow,” but “the first value goes into the first parameter, the second goes into the second.”

In functions where parameters have the same type, it is especially important to watch the meaning and naming of parameters. For example, rectArea(width, height int) is much clearer than rectArea(a, b int), even though the types are the same.

Mini demo:

package main

import "fmt"

func rectArea(width, height int) int {
	return width * height
}

func main() {
	fmt.Println(rectArea(2, 10)) // 20
	// rectArea(10, 2) will also be 20, but you have already lost the "width/height" meaning :)
}

Yes, the math does not suffer in this example, but in a real task (for example, “min/max,” “start/end,” “x/y”) the argument order can completely change the meaning.

4. Example: MiniCalc v1

Now we will start building a mini application that will keep evolving in later topics. It will be a simple console calculator: it reads two numbers and prints the sum and the product. It sounds modest, but it is perfect for practicing functions: you can extract computations, and keep main as the “conductor.”

First, let’s look at the “naive” version (everything inside main). It works, but as soon as there is more logic, it will start to balloon.

package main

import (
	"fmt"
)

func main() {
	var a, b int
	fmt.Scan(&a, &b)

	fmt.Println("sum =", a+b)
	fmt.Println("mul =", a*b)
}

Now let’s do the same thing, but with functions for calculations. Important: we are not complicating input/output yet; we are just extracting arithmetic to see the mechanics.

package main

import "fmt"

func sum(a, b int) int {
	return a + b
}

func mul(a, b int) int {
	return a * b
}

func main() {
	var a, b int
	fmt.Scan(&a, &b)

	fmt.Println("sum =", sum(a, b))
	fmt.Println("mul =", mul(a, b))
}

In terms of the result, the program does the same thing. But in terms of structure, something important happened: main now reads like a script—“read → call functions → print,” and the computation details are no longer inside it.

main as a script, functions as steps

When you start writing functions, it is sometimes useful to draw (in your head or on paper) the call chain. It helps you avoid getting lost: “who calls whom” and “where things are computed.”

For our MiniCalc v1, the chain looks like this:

flowchart TD
    A[main: read a and b] --> B["sum(a, b)"]
    A --> C["mul(a, b)"]
    B --> D[main: print sum]
    C --> E[main: print mul]

The meaning of the diagram is simple: main is responsible for “organization,” while sum and mul are responsible for “computations.” This is a good habit that will help a lot later, when computations get more complex and bugs appear.

5. Common mistakes

Error #1: forgot to specify the parameter type (or specified it only for the first one).
Beginners often write something like func sum(a, b int) int and are surprised that it works, and then they write func sum(a, b) int and get an error. In Go, the type is required, but you can “share” it across multiple names only when the type is the same, as in a, b int. If the types are different, each parameter must explicitly have its own type.

Error #2: mixed up the parts of the signature.
Sometimes people try to write it “like a human”: func int sum(a int, b int). But Go likes order: func → name → parameters → result. It is like a line at a clinic: you can argue about it, but it is easier to memorize it once and move on.

Error #3: a function with a result does not return a value on all branches.
You write an if, do a return in one branch, forget it in the other, and the compiler complains. This is not the compiler being mean; it is caring about your sanity: better to get an error immediately than “weird behavior” in production. The fix is simple: verify that any path ends with a return of the required type.

Error #4: overly “abstract” function and parameter names.
f(x, y) works, but reads like “something somewhere.” In a week you will forget what f is, and in a month you will start suspecting it is not even your code (even though it is). Reasonable names like rectArea(width, height) or max2(a, b) do half the work that comments would do.

Error #5: turning main into a dumping ground for everything—even after functions appear.
It happens like this: you created functions, but main is still huge because you extracted only “tiny” things, while the big logical steps stayed inside. A good guideline: if main no longer fits on the screen without scrolling, it is time to ask yourself again: “what repeating step here can I name with one word and extract into a function?”

1
Task
Go SELF, level 9, lesson 1
Locked
Warm Greeting
Warm Greeting
1
Task
Go SELF, level 9, lesson 1
Locked
Room layout
Room layout
1
Task
Go SELF, level 9, lesson 1
Locked
Best score
Best score
1
Task
Go SELF, level 9, lesson 1
Locked
Primality Guard
Primality Guard
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION