Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Metazi

Status

📜📞🔧❌

Back-End Developer

Interview Process

flowchart LR
    sr(Send Resume) --> hr(HR Call) --> ti(Technical Interview) --rejected--x hri(HR Interview) -.-> o(Offer)

Apply Way

Jobvision

Interview Date

  • Sent Resume
    1404/07/01

  • HR Call
    1404/07/06

  • Technical Interview
    1404/07/07

  • Rejection Email
    1404/07/08

Interview Duration

  • Technical Interview
    45 minutes

Interview Platform

Google Meet

Technical Interview

  • Tell me about yourself.

  • What is init in Go? How does it work and how does it differ from main?

    Answer

    go.dev

    init() is a special function: func init(). It runs automatically after package-level variables are initialized. You can have multiple init()s in a package. You cannot call init() yourself.

    Order: evaluate package vars → run that package’s init()s → repeat by dependency order → finally call main.main().

    main() is the program entry point (package main, func main()) and runs once after all init()s finish.

  • What is _ in imported packages?

    Answer

    _ is the blank identifier. When used in an import like import _ "pkg/path" it means: import the package solely for its side effects (package initialization) but do not bind any name to refer to it in the importing file. The compiler won’t complain about an unused import because the blank identifier intentionally discards the package name.

    import (
        _ "github.com/lib/pq" // register pq as a database/sql driver via its init()
    )
    

    (Aside: _ is also used to discard values in assignments: _, ok := m["x"].)

  • Why use it? If there is no need, should we remove it? Why do people still use it?

    Answer

    Use them when the package’s init() performs necessary side effects (driver registration, plugin/handler registration, global initialization). If a package has no needed side effects, remove the import.

  • Why write configs in init instead of main? What are the benefits? What are the problems if we write all code in init instead of main?

    Answer

    Pros: init() runs before main(), so it can prepare package-level state.

    Cons: init() cannot accept context or return errors, runs during tests, runs before flag/config parsing, and makes shutdown/signal handling hard. Prefer main() for orchestration, lifecycle and graceful shutdown.

  • Multiple packages each run an init that Printfs the package name. You import them in main, but main() is empty. What is the output?

    Answer
    // pkg/a/a.go
    package a
    
    import "fmt"
    
    func init() { fmt.Println("init a") }
    
    // pkg/b/b.go
    package b
    
    import "fmt"
    
    func init() { fmt.Println("init b") }
    
    // main.go
    package main
    
    import (
        _ "example.com/project/pkg/a"
        _ "example.com/project/pkg/b"
    )
    
    func main() {} // empty
    

    Important notes about the order:

    • Initialization (and therefore the init() prints) runs before main() is called.
    • The order between packages follows the import dependency graph: if b imports a, init a will always appear before init b.
    • If the packages are independent (neither imports the other), the relative order is unspecified — you should not rely on a particular ordering.
    • main() being empty produces no additional output.
  • If you have multiple goroutines that fetch data from different APIs and you want to gather all results and aggregate them, or wait with a timeout then aggregate, how can you do this?

    Answer
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    type Result struct {
        URL  string
        Body string
        Err  error
    }
    
    func fetch(url string) (string, error) {
        // do actual HTTP call...
        return "data-for-" + url, nil
    }
    
    func fetchPartial(urls []string, timeout time.Duration) map[string]string {
        var wg sync.WaitGroup
        resultsCh := make(chan Result, len(urls))
    
        for _, u := range urls {
            wg.Add(1)
            go func(u string) {
                defer wg.Done()
                body, err := fetch(u)
                resultsCh <- Result{URL: u, Body: body, Err: err}
            }(u)
        }
    
        // close channel when all goroutines are done
        go func() {
            wg.Wait()
            close(resultsCh)
        }()
    
        aggregated := make(map[string]string)
        timeoutCh := time.After(timeout)
    
    Loop:
        for {
            select {
            case r, ok := <-resultsCh:
                if !ok {
                    break Loop // channel closed, all responses received
                }
                if r.Err == nil {
                    aggregated[r.URL] = r.Body
                } else {
                    // optionally log r.Err
                }
            case <-timeoutCh:
                // timeout occurred — stop waiting for more, but goroutines may still be running.
                // To avoid leaks in real code, pass a context to requests so they cancel.
                break Loop
            }
        }
        return aggregated
    }
    
    func main() {
        urls := []string{"a", "b", "c"}
        out := fetchPartial(urls, 2*time.Second)
        fmt.Println(out)
    }
    
  • If you run main.go, how many goroutines are initialized?

    Answer

    At least one main goroutine plus several runtime goroutines (GC, timers, netpoller). You commonly see 2–4 at startup, but exact number varies. GOMAXPROCS controls parallelism (Ps), not total goroutine count.

  • How does Go manage goroutines?

    Answer

    The runtime scheduler uses three core concepts: G, M, and P.

    • G (goroutine): the user-level unit of concurrency (stack, state, metadata). Goroutines are cheap to create and their stacks grow and shrink as needed.
    • M (machine): an OS thread that actually executes Go code. The runtime creates and reclaims Ms as needed (e.g., to service blocking syscalls).
    • P (processor): a scheduler context that holds a run queue of runnable Gs and scheduling state. Only an M that has an associated P can run Go code. GOMAXPROCS controls how many Ps exist (how many goroutines can run in parallel).

    How scheduling works (high level):

    • Each P has a local run queue of runnable goroutines. When a goroutine becomes runnable it’s pushed to a P’s queue. When a P runs out of work it will steal goroutines from other Ps or pull from a global queue; this provides locality and load balancing.
    • The scheduler chooses a G from a P’s run queue and runs it on the M bound to that P. If that G blocks (channel wait, sleep, blocking syscall), it is parked and another G is scheduled.

Score

6/10