JavaRush /Courses /Go SELF /Logical operators &&, ||, !

Logical operators &&, ||, !

Go SELF
Level 3, Lesson 3
Available

1. Introduction

When you write your first conditions, it seems like a single check is enough: if age >= 18 { ... }. But very quickly life throws in an unpleasant extra detail: “and you also need a ticket,” “and you also need a password,” “and the user must not be on the ban list,” “and the discount works only on weekdays.” And this is where beginners hit the classic pattern: either a forest of nested if statements grows, or “sausage conditions” appear—conditions so long that even the author reads them like a Mayan cipher a week later.

Logical operators are needed to express conditions compactly, while keeping them predictable. Go, in general, likes predictability: the language and its spec try not to multiply “magic” rules and extra concepts, because readability and clarity are an important part of the philosophy.

2. Operator &&: logical “AND”

Imagine a security guard at the entrance. They are not impressed that you have “almost everything ready”: if both an ID and a ticket are required, then without either one, entry is closed. The && operator is exactly that kind of guard: the result will be true only when both conditions are true.

It is important to remember: && works only with boolean values. So first we do comparisons (age >= 18, hasTicket == "yes"), and only then we connect them with &&.

Truth table for &&

A B
A && B
true
true
true
true
false
false
false
true
false
false
false
false

Mini example: “adult and has a ticket”

package main

import "fmt"

func main() {
	age := 20
	hasTicket := true

	canEnter := age >= 18 && hasTicket
	fmt.Println(canEnter) // true
}

Here age >= 18 is a bool, hasTicket is also a bool, and we can safely connect them via &&.

Mini example: condition directly in if

package main

import "fmt"

func main() {
	age := 16
	hasTicket := true

	if age >= 18 && hasTicket {
		fmt.Println("welcome")
	} else {
		fmt.Println("no entry") // no entry
	}
}

3. Operator ||: logical “OR”

Now imagine a different scenario: you can get into the club either with a ticket or with a VIP invitation. Here the guard is kinder: at least one condition is enough. That is ||: if the left side is true or the right side is true, then the whole expression is true.

Again: || works only with bool. So first we turn facts into boolean values (comparisons), and only then connect them.

Truth table for ||

A B
A || B
true
true
true
true
false
true
false
true
true
false
false
false

Mini example: “admin or editor”

package main

import "fmt"

func main() {
	role := "editor"

	isAdmin := role == "admin"
	isEditor := role == "editor"

	canEdit := isAdmin || isEditor
	fmt.Println(canEdit) // true
}

Notice: we pulled the comparisons out into variables with normal names. These are not “extra lines”—they are an investment so you do not curse yourself two days later.

4. Operator !: logical “NOT”

The ! operator is inversion. It was true—it becomes false. It was false—it becomes true. It is especially useful when you want to emphasize negation: “not empty,” “not banned,” “not expired.”

Beginners often fear ! because it is visually small, but it can cause big problems. The main principle is simple: if you invert a complex condition, use parentheses. Then you are not arguing with precedence—you are working with it.

Mini example: “non-empty string”

package main

import "fmt"

func main() {
	name := "gopher"

	isEmpty := name == ""
	fmt.Println(isEmpty)  // false
	fmt.Println(!isEmpty) // true
}

Mini example: “if NOT an admin”

package main

import "fmt"

func main() {
	role := "user"

	if role != "admin" {
		fmt.Println("no admin access") // no admin access
	}
}

Here I intentionally showed the version without !: it is often more readable to write role != "admin", than !(role == "admin"). But sometimes ! really does make the thought shorter—the key is not to turn code into a logic quest.

5. Precedence: when you need parentheses

When an expression is short, precedence seems like a minor detail. But as soon as you have three or four conditions in a row, you start getting errors like “it looks right, but it works differently.” The reason is often one: you read the condition one way, and Go reads it another.

In Go (as in many languages) there is an order in which operations “bind” without parentheses. For our topic, the minimum that matters is:

  • ! is evaluated first,
  • then &&,
  • then ||.

One more important point: comparison operators (==, <, >=, etc.) are “stronger” than && and ||, meaning comparisons are evaluated first, and then the logic. This is convenient: age >= 18 && age < 60 reads naturally.

Below is a “useful slice” of the precedence table (simplified, only what we actually need today):

Operation group Examples Which is “stronger”
Comparisons
== != < <= > >=
stronger
Logical NOT
!x
stronger
Logical AND
a && b
middle
Logical OR
a || b
weaker

An example where you can make a mistake without parentheses

Let us imagine a condition: “allow entry if (has a ticket and age >= 18) or has VIP.”

Correct form:

canEnter := (hasTicket && age >= 18) || isVIP

If you remove the parentheses, in this specific case nothing will break (because && binds stronger than ||), but I still recommend keeping them: you explicitly show the meaning instead of relying on remembering precedence.

An example where parentheses really save your brain

“Deny entry if the user is not VIP and not an admin.”

deny := !isVIP && !isAdmin

This reads fine. But if you want to do “deny entry if NOT (VIP or admin),” then it is better like this:

deny := !(isVIP || isAdmin)

Without parentheses, it would already be a different meaning, and you can easily get the behavior “the guard let in the wrong people.” And the guard is, of course, a metaphor... but sometimes you are actually writing authorization.

6. Short-circuit: the right side may not run

Now comes an important thing that Go does “by default,” and it is both convenient and dangerous. The && and || operators use short-circuit evaluation.

This means:

  • In A && B, the right side B is not evaluated if A == false.
  • In A || B, the right side B is not evaluated if A == true.

It sounds like tedious theory, but in practice it is your built-in “fuse.”

Fuse example: protection from division by zero

package main

import "fmt"

func main() {
	a := 10
	denom := 0

	// The right side will NOT run if denom == 0.
	ok := denom != 0 && (a/denom > 1)
	fmt.Println(ok) // false
}

If Go always tried to evaluate both sides, the program would crash on a/denom. But thanks to short-circuit evaluation, the right side does not even start, because the left side already makes it “obvious”: denom != 0 is false.

Example: “if admin, do not check further”

package main

import "fmt"

func main() {
	role := "admin"
	hasPaid := false

	canUseFeature := role == "admin" || hasPaid
	fmt.Println(canUseFeature) // true
}

If the user is an admin, we no longer care whether they paid or not: the condition is true from the left side, and the right side may not even be looked at.

Why it is important to remember

Because sometimes people (especially after other languages) put something “important” on the right side—for example, a check that must always be called. But it will not be called if the left side already determined the result. So the rule is simple: the right side of &&/|| should be free of side effects (that is, no actions that “must happen”).

Mini diagram: how short-circuit “cuts off” evaluation

To reinforce the idea of short-circuit evaluation, it helps to imagine it as a simple flowchart: with && we will not go to the right if the left side is already false. This is not a “compiler optimization,” but part of the operators’ behavior.

flowchart TD
    A[Evaluate the left side A] --> B{Operator && ?}
    B -->|yes| C{A == false?}
    C -->|yes| D[Result: false
Do not evaluate B] C -->|no| E[Evaluate the right side B] E --> F[Result: A && B]

And yes: because of this, it is sometimes convenient to put “dangerous” operations on the right side of &&—operations that are valid only if the left check passes (division, indexing, accessing something). The key is to do it intentionally, not “by accident.”

7. How to read complex conditions

A complex condition is like a long sentence in legal language: formally it can be correct, but your brain refuses to parse it without pain. The good news: you can improve readability with just a couple of habits. In Go this is especially valued: the language is designed so that code is read more often than written (and for you—especially so).

Below is a practical approach that almost always works.

First, you extract small “facts” into variables with meaningful names, and then you assemble the final condition from those facts. This does two things: it reduces the chance of a logical mistake and turns if into an almost human sentence.

Reads kind of poorly

if (age >= 18 && hasTicket) || (isVIP && country == "US") {
	fmt.Println("ok")
}

Better: split into facts

isAdult := age >= 18
isFromUS := country == "US"

canEnter := (isAdult && hasTicket) || (isVIP && isFromUS)

And here you start to understand what is going on, even if you do not remember precedence.

By the way, equality/inequality (==, !=) in Go is defined for a huge number of types (not only int and string), so comparisons are a very basic building block of the language.

8. The return statement

There is another clever way to avoid multi-level nested if statements and improve code readability: use the return statement.

The return statement immediately ends the execution of the current function. For the main() function, this means: the program ends right at this point.

This is very convenient

Sometimes code without return starts to “wrap” inward into nested if blocks:

if !isBanned {
	if isVIP || (isAdult && hasTicket) {
		fmt.Println("welcome")
	}
}

But you can make it simpler and more readable:

if isBanned {
	fmt.Println("denied: banned")
	return
}

if isVIP || (isAdult && hasTicket) {
	fmt.Println("welcome")
}

This style is often called early return.

9. Example: the mini app “DoorGuard”

Now we will build a small console program that checks whether to let a person into an event. We intentionally keep it simple—no loops and no functions (except main)—to use only what we have already learned. But the logic will be as “real-life” as possible, which means it will include &&, ||, !.

Version 1: read input and build simple boolean facts

Let us start by reading four values: age, ticket presence (“yes/no”), VIP presence (“yes/no”), and banned (“yes/no”). Yes, it sounds strict—but it is a guard; that is the job.

package main

import "fmt"

func main() {
	var age int
	var ticketStr string
	var vipStr string
	var bannedStr string

	fmt.Scan(&age, &ticketStr, &vipStr, &bannedStr)

	hasTicket := ticketStr == "yes"
	isVIP := vipStr == "yes"
	isBanned := bannedStr == "yes"

	fmt.Println(age, hasTicket, isVIP, isBanned)
	// example output: 20 true false false
}

Here is an important point: we do not try to read bool directly; instead, we create a boolean value by comparing strings. This is a common early-stage trick, because input is usually “text-based.”

Version 2: define the access condition

Let the rule be:

  • if the person is banned—deny entry (even if VIP),
  • otherwise allow entry if they are VIP or (they are an adult and they have a ticket).

That is:

canEnter = !isBanned && (isVIP || (isAdult && hasTicket))
Access formula (with explicit parentheses for readability)

Implement it:

package main

import "fmt"

func main() {
	var age int
	var ticketStr string
	var vipStr string
	var bannedStr string

	fmt.Scan(&age, &ticketStr, &vipStr, &bannedStr)

	hasTicket := ticketStr == "yes"
	isVIP := vipStr == "yes"
	isBanned := bannedStr == "yes"
	isAdult := age >= 18

	canEnter := !isBanned && (isVIP || (isAdult && hasTicket))

	if canEnter {
		fmt.Println("welcome") // welcome
	} else {
		fmt.Println("denied") // denied
	}
}

Pay attention to the parentheses. Formally, some of them can be removed, but we kept them as “road signs”: it is easier to read and there is less chance to make a mistake when you change conditions later.

Version 3: show the “reason”

Sometimes it is useful not just to say “no,” but to explain. We are not building a perfect UX system yet, but at the if/else level you can make a simple branch:

package main

import "fmt"

func main() {
	var age int
	var ticketStr string
	var vipStr string
	var bannedStr string

	fmt.Scan(&age, &ticketStr, &vipStr, &bannedStr)

	hasTicket := ticketStr == "yes"
	isVIP := vipStr == "yes"
	isBanned := bannedStr == "yes"
	isAdult := age >= 18

	if isBanned {
		fmt.Println("denied: banned") // denied: banned
		return
	}

	if isVIP || (isAdult && hasTicket) {
		fmt.Println("welcome") // welcome
	} else {
		fmt.Println("denied: need ticket and age 18+") // denied: need ticket and age 18+
	}
}

Here return appears even though we have not covered writing our own functions yet. But return inside main just means “end the program right now.” This often makes code simpler: we handled the strictest prohibition (banned) and exited immediately, without increasing nesting.

9. Common mistakes when working with &&, ||, !

Error #1: trying to use &&/|| not with bool, but with numbers or strings.
Beginners sometimes bring a habit from other languages: “if the string is not empty, it is kind of true.” In Go, you cannot do that: the condition in if and the operands of &&/|| must be strictly bool. So first write a comparison like len(s) > 0 or s != "", and only then connect it with logic.

Error #2: a complex condition without parentheses reads one way but runs differently due to precedence.
Even if you remember “! is stronger than &&, && is stronger than ||,” in a week you might not remember it, and your teammate definitely might not. Parentheses in logic are not a sign of weakness—they are a way to lock in the meaning. It is especially useful to show “groups” explicitly: (A && B) || C, even if it would formally work without them.

Error #3: negations with ! turn the condition into a puzzle.
When !, !=, and a couple of parentheses show up on one line, your brain starts “flipping” the meaning back and forth. It is often easier to rename the variable: instead of !isInvalid use isValid; instead of !isNotAllowed use isAllowed. The code becomes longer by a couple of letters, but shorter in reading time.

Error #4: forgetting about short-circuit and hiding an important action on the right side.
Short-circuit evaluation is great, but it can also become a trap. If you expect the right side to run “always,” you might accidentally remove half the logic. A || B will not evaluate B if A == true, and that is normal. Just do not put in B something that “must happen” (printing, changing a variable, writing a result).

Error #5: trying to “stuff everything” into one condition instead of extracting parts into variables.
When a condition becomes longer than one line, the chance of making a mistake grows sharply: mixing up && and ||, forgetting !, putting a parenthesis in the wrong place. In such cases it helps to “slice the sausage”: extract simple facts (isAdult, hasTicket, isVIP) and assemble the final formula from them. This is one of the cheapest ways to make code more reliable.

1
Task
Go SELF, level 3, lesson 3
Locked
Drone Zone
Drone Zone
1
Task
Go SELF, level 3, lesson 3
Locked
Concert Entry
Concert Entry
1
Task
Go SELF, level 3, lesson 3
Locked
Editor permissions
Editor permissions
1
Task
Go SELF, level 3, lesson 3
Locked
Division check
Division check
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION