Question
I need a queue that stores (string, int) pairs in Go. A named struct works:
type job struct {
url string
depth int
}
queue := make(chan job)
queue <- job{url: url, depth: depth}
Does Go provide a built-in pair or tuple data type? Go functions can return multiple values, but are those values first-class tuple values in Go's type system? I also tried Python-like syntax, but it does not compile:
queue := make(chan (string, int))
What is the idiomatic Go approach for representing and passing a pair of values?
Short Answer
Go does not have a built-in, first-class pair or tuple type. The idiomatic way to group related values is a struct, usually a named struct with meaningful field names. Go's multiple return values are useful for function calls, but they cannot be stored, passed around, or used as a channel element as one tuple value.
Concept
Go deliberately keeps its type system simple: a value has one type, such as int, string, []byte, or a struct type. When you need one value that contains several related pieces of data, define a struct.
type job struct {
url string
depth int
}
A job value now combines a URL and its crawl depth into one typed value. It can be assigned to variables, placed in slices, sent through channels, used as a map value, and passed to functions.
Go functions may return multiple values:
func splitURL(raw string) (string, int) {
return raw, len(raw)
}
However, (string, int) is not a tuple type that you can name or store. It is a function signature result list. The returned values must be received in a supported context, commonly with assignment:
url, length := splitURL("https://example.com")
This distinction matters because a channel carries one value per send. If that value has several parts, package those parts into a struct.
Mental Model
Think of a Go function with multiple return values as a clerk handing you two separate items across a counter. You must receive them into separate variables:
name, age := getPerson()
A struct is a labelled container that holds both items together. Once the items are in the container, you can put the container on a queue, save it in a slice, or hand it to another function:
person := Person{Name: "Ava", Age: 30}
For queue work, a job struct is the container and a chan job is the conveyor belt carrying those containers.
Syntax and Examples
A named struct is the usual choice when the values describe one meaningful thing.
package main
import "fmt"
type Job struct {
URL string
Depth int
}
func main() {
queue := make(chan Job, 2)
queue <- Job{URL: "https://example.com", Depth: 0}
queue <- Job{URL: "https://go.dev", Depth: 1}
first := <-queue
fmt.Println(first.URL, first.Depth)
}
Job is a new type with two fields:
URLstores the string.Depthstores the integer.chan Jobis a channel whose elements areJobvalues.Job{URL: ..., Depth: ...}is a struct literal using field names.
Named fields in a literal are usually clearer and safer than positional fields:
// Valid, but easier to break if field order changes.
job := Job{"https://example.com", 0}
// Preferred for readability.
job = Job{URL: "https://example.com", Depth: 0}
You can also use an anonymous struct for a small, local case:
queue := make(chan struct {
url string
depth int
})
But this becomes repetitive and less reusable. Prefer a named job or Job type when the data has a meaningful role.
Step by Step Execution
Consider this buffered work queue:
type job struct {
url string
depth int
}
func main() {
queue := make(chan job, 1)
next := job{url: "https://example.com", depth: 2}
queue <- next
received := <-queue
fmt.Println(received.url)
fmt.Println(received.depth)
}
Execution flow:
type job structdefines a type that groupsurlanddepth.make(chan job, 1)creates a channel that can hold onejobbefore a receiver is ready.nextis created as onejobvalue containing both fields.queue <- nextsends that onejobvalue into the channel.received := <-queuereceives ajobvalue from the channel.received.urlevaluates to"https://example.com".received.depthevaluates to2.
The channel never sends two unrelated values. It sends one value whose type is job.
Real World Use Cases
Structs are used for pair-like data throughout Go programs:
- Web crawlers: a URL and crawl depth, such as
Job{URL: link, Depth: depth + 1}. - Background workers: a task identifier and retry count.
- API processing: a request payload and metadata such as a correlation ID.
- File processing: a file path and its processing status.
- Caching: a value and expiration time.
- Database work: a record ID and a pending operation.
Meaningful names are especially valuable in these cases. RetryCount communicates more than “the second int in a pair.”
Real Codebase Usage
In real Go code, developers generally define small domain types rather than use generic pairs.
Queue a unit of work
type CrawlJob struct {
URL string
Depth int
}
func enqueue(queue chan<- CrawlJob, url string, depth int) {
queue <- CrawlJob{URL: url, Depth: depth}
}
The chan<- CrawlJob parameter means this function can only send jobs, making its intent explicit.
Return multiple values for a short-lived result
Multiple returns are commonly used for a primary result and an error:
page, err := fetch(url)
if err != nil {
return err
}
They are also useful for map lookups:
score, found := scores[userID]
if !found {
score = 0
}
Here, the values are consumed immediately. If the result needs to travel through a queue or be retained as a single unit, define a struct instead.
Add validation with a guard clause
func process(job CrawlJob) error {
if job.URL == "" {
return errors.New("job URL is required")
}
if job.Depth < 0 {
return errors.New("job depth cannot be negative")
}
// Process a valid job.
return nil
}
A struct provides a clear place to validate fields before work begins.
Common Mistakes
Trying Python tuple syntax
This does not compile in Go:
queue := make(chan (string, int))
Go does not define (string, int) as a standalone tuple type. Use a struct:
queue := make(chan job)
Treating multiple return values as one assignable value
This does not compile:
func values() (string, int) {
return "item", 3
}
x := values() // Error: multiple-value values() in single-value context
Receive each result separately:
name, count := values()
Or package the results in a struct when one value is required.
Sending fields separately through separate channels
urls := make(chan string)
depths := make(chan int)
This can cause coordination problems: a URL may accidentally be matched with the wrong depth, especially with multiple goroutines. Send one job struct instead.
Using positional literals when field order is unclear
j := job{"https://example.com", 2}
This is valid, but named fields are easier to read:
j := job{url: "https://example.com", depth: 2}
Exporting or hiding fields unintentionally
Lowercase identifiers are package-private:
type job struct {
url string
}
Use uppercase names if another package must create or read the values:
type Job struct {
URL string
}
Comparisons
| Approach | Can represent related values as one value? | Can be sent through chan? | Best use |
|---|---|---|---|
| Named struct | Yes | Yes | Most application data and work items |
| Anonymous struct | Yes | Yes | Small local, one-off data shapes |
| Multiple return values | No | Not directly | Returning a result plus an error or status |
| Slice/array | Yes | Yes | A sequence of same-typed values |
| Map | Yes | Yes | Lookup data keyed by another value |
Structs versus multiple return values
// Multiple returns: consume immediately.
func parsePort(s string) (int, error) {
// ...
return 8080, nil
}
// Struct: one value that can be stored or queued.
type Server struct {
Host string
Port int
}
Use multiple returns when values are naturally the immediate outcome of a function call. Use a struct when the values together represent an object, message, configuration item, or unit of work.
Cheat Sheet
// Define a pair-like type.
type Job struct {
URL string
Depth int
}
// Create a channel of jobs.
queue := make(chan Job)
// Send one struct value.
queue <- Job{URL: "https://example.com", Depth: 0}
// Receive one struct value.
job := <-queue
fmt.Println(job.URL, job.Depth)
// Buffered channel.
queue = make(chan Job, 10)
// Function returning multiple separate values.
func lookup(id int) (string, bool) {
return "found", true
}
name, ok := lookup(1)
Rules to remember:
- Go has no built-in generic pair or tuple type.
(string, int)is valid in a function result signature, not as a general value type.- A channel sends exactly one value of its element type at a time.
- Use a struct to group values that belong together.
- Prefer descriptive field names and named struct literals.
- Use exported type and field names (
Job,URL) only when other packages need access.
FAQ
Does Go have a built-in tuple type?
No. Go has multiple return values, but it does not have a first-class tuple type that can be stored in a variable, slice, map, or channel.
How do I make a pair in Go?
Define a struct with two fields:
type Pair struct {
First string
Second int
}
In application code, prefer names that describe the data, such as URL and Depth.
Can I send two values through one Go channel?
Not as two separate channel elements in one send operation. Put both values in a struct and send that struct.
Why does make(chan (string, int)) fail?
Because (string, int) is not a Go type expression for a tuple. Parenthesized type lists are supported in specific syntax such as function parameters and results, not as a general data type.
Can a function's multiple return values be assigned to one variable?
Usually no. Multiple results must be received by a matching list of variables, except in special language contexts. If you need one storable value, return a struct.
Should I use an anonymous struct for a pair?
It is valid for short, local code, but a named struct is usually more readable, reusable, and easier to document.
Are structs expensive to send over channels?
Small structs are commonly sent by value. If the object is large or shared mutable state is required, a channel of pointers may be appropriate, but use pointers only when they solve a real need.
Mini Project
Description
Build a small crawl-job queue. Each queued item contains a URL and the depth at which it was discovered. This demonstrates why related values should travel through a channel as one struct value.
Goal
Create, enqueue, and process URL/depth jobs using a typed Go channel.
Requirements
Use a Job struct with URL and Depth fields.
Create a buffered channel that stores Job values.
Enqueue at least three jobs.
Receive and print every job with its URL and depth.
Close the channel after all jobs have been added.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.