Go Client for Apache Kafka
confluent-kafka-go is the Go client for Apache Kafka® that Confluent develops and maintains. It includes a producer and a consumer, and it wraps librdkafka, the C/C++ client for Kafka, through cgo.
To build a Go client application step by step, see Getting Started with Apache Kafka and Go.
Install the Go client
Install the Go client with go get. On Linux (glibc and musl) and macOS,
librdkafka is bundled with the Go client, so you don’t need to install it
separately.
The Go client is available on GitHub and as the confluent-kafka-go package to pin to specific versions. The Changelog in the GitHub repository lists release updates.
On other platforms, first install
the C client,
including its development package, and a C build toolchain that includes
pkg-config. The following table lists the additional packages for each
platform:
Platform |
Install command |
|---|---|
Red Hat-based Linux distributions |
|
Debian-based Linux distributions |
|
macOS with Homebrew |
|
Install librdkafka before you run these commands on Red Hat-based and Debian-based distributions.
Next, use go get to install the library:
go get gopkg.in/confluentinc/confluent-kafka-go.v1/kafka
Your Go code can now import and use the client. To confirm that the installation
succeeded, build and run the go-kafkacat command-line utility:
go get gopkg.in/confluentinc/confluent-kafka-go.v1/examples/go-kafkacat
$GOPATH/bin/go-kafkacat --help
The source code is also available in the
ZIP and TAR archives under
the directory src/.
Go client code examples
For a step-by-step tutorial that includes producer and consumer code samples, see Getting Started with Apache Kafka and Go. For more samples, see the Go client examples and the clients documentation.
Kafka producer
Create a producer
The Go client passes configuration to the producer in a ConfigMap object. A
ConfigMap is a map of librdkafka configuration properties:
import (
"github.com/confluentinc/confluent-kafka-go/kafka"
)
p, err := kafka.NewProducer(&kafka.ConfigMap{
"bootstrap.servers": "host1:9092,host2:9092",
"client.id": "myProducer",
"acks": "all"})
if err != nil {
fmt.Printf("Failed to create producer: %s\n", err)
os.Exit(1)
}
Produce messages asynchronously
To produce a message asynchronously, call the Produce() method. Pass a
Message object and, optionally, a chan Event that receives the result of
the send. The Message object contains an opaque interface{} field that
carries arbitrary data to the event handler:
err = p.Produce(&kafka.Message{
TopicPartition: kafka.TopicPartition{Topic: topic, Partition: kafka.PartitionAny},
Value: []byte(value)},
nil, // delivery channel
)
Use a goroutine to handle delivery reports concurrently with other event types, such as errors and statistics. If you set a delivery channel, delivery reports arrive on that channel, and the client may start a goroutine to wait on them.
Always process the Events() channel, even when you set a delivery channel,
because it contains client-level errors and control messages:
go func() {
for e := range p.Events() {
switch ev := e.(type) {
case *kafka.Message:
if ev.TopicPartition.Error != nil {
fmt.Printf("Failed to deliver message: %v\n", ev.TopicPartition)
} else {
fmt.Printf("Successfully produced record to topic %s partition [%d] @ offset %v\n",
*ev.TopicPartition.Topic, ev.TopicPartition.Partition, ev.TopicPartition.Offset)
}
}
}
}()
Produce messages synchronously
To produce a message synchronously, receive from the delivery channel that you
pass to the Produce() method. Synchronous writes reduce throughput, so use
them only when your application requires them:
delivery_chan := make(chan kafka.Event, 10000)
err = p.Produce(&kafka.Message{
TopicPartition: kafka.TopicPartition{Topic: topic, Partition: kafka.PartitionAny},
Value: []byte(value)},
delivery_chan
)
e := <-delivery_chan
m := e.(*kafka.Message)
if m.TopicPartition.Error != nil {
fmt.Printf("Delivery failed: %v\n", m.TopicPartition.Error)
} else {
fmt.Printf("Delivered message to topic %s [%d] at offset %v\n",
*m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset)
}
close(delivery_chan)
Alternatively, call the Flush() method to wait for all messages to be
acknowledged:
p.Flush()
Important
Flush() serves only the producer’s Events() channel, not
application-specified delivery channels. If you call Flush()
and no goroutine processes the delivery channel, its buffer can
fill up and cause the flush to time out.
Kafka consumer
Create a consumer
The Go client passes configuration to the consumer in a ConfigMap object. A
ConfigMap is a map of librdkafka configuration properties:
import (
"github.com/confluentinc/confluent-kafka-go/kafka"
)
consumer, err := kafka.NewConsumer(&kafka.ConfigMap{
"bootstrap.servers": "host1:9092,host2:9092",
"group.id": "foo",
"auto.offset.reset": "smallest"})
Consume messages with Poll
A typical Kafka consumer application runs a consume loop that repeatedly calls
the Consumer.Poll method. Poll retrieves records one by one from records
that the consumer pre-fetches efficiently in background threads. Call
Consumer.SubscribeTopics first to join a consumer group and choose the
topics to consume from:
err = consumer.SubscribeTopics(topics, nil)
for run == true {
ev := consumer.Poll(100)
switch e := ev.(type) {
case *kafka.Message:
// application-specific processing
case kafka.Error:
fmt.Fprintf(os.Stderr, "%% Error: %v\n", e)
run = false
default:
fmt.Printf("Ignored %v\n", e)
}
}
consumer.Close()
Consumer.Poll returns as soon as a message or event is available, or after
the timeout in milliseconds, whichever comes first. Choose the timeout as
follows:
A short value, such as
100, lets your application be interrupted.-1waits indefinitely. Avoid it, because librdkafka cannot respond to SIGINT (the interrupt signal), so the application can’t be interrupted.0causes busy looping. Avoid it.
Call Consumer.Close when you finish using the consumer. Consumer.Close
does the following:
Closes active sockets and cleans up internal state.
Triggers a group rebalance immediately, which reassigns the consumer’s partitions to another member of the group.
If you don’t close the consumer, the broker triggers the rebalance only after the session timeout expires.
Commit offsets synchronously
Call Commit() to commit offsets synchronously. Other commit methods
accept more specific input:
Commit()commits the current offsets.CommitOffsets()commits a list of offsets that you provide.CommitMessage()commits the offset relative to a consumedMessage.
When you commit offsets manually, disable the enable.auto.commit
configuration:
msg_count := 0
for run == true {
ev := consumer.Poll(100)
switch e := ev.(type) {
case *kafka.Message:
msg_count += 1
if msg_count % MIN_COMMIT_COUNT == 0 {
consumer.Commit()
}
fmt.Printf("%% Message on %s:\n%s\n",
e.TopicPartition, string(e.Value))
case kafka.PartitionEOF:
fmt.Printf("%% Reached %v\n", e)
case kafka.Error:
fmt.Fprintf(os.Stderr, "%% Error: %v\n", e)
run = false
default:
fmt.Printf("Ignored %v\n", e)
}
}
This example triggers a synchronous commit every MIN_COMMIT_COUNT messages.
You can also trigger the commit when a timeout expires, so that the committed
position updates regularly.
At-least-once and at-most-once delivery
Committing after processing gives at-least-once delivery. If a failure occurs between processing a message and committing its offset, the consumer processes the message again. The synchronous commit example in the previous section works this way.
Committing before processing gives at-most-once delivery. If a failure occurs between the commit and processing, the message is lost, so handle commit failures carefully. The following example commits first, so it gives at-most-once delivery:
for run == true {
ev := consumer.Poll(100)
switch e := ev.(type) {
case *kafka.Message:
err = consumer.CommitMessage(e)
if err == nil {
msg_process(e)
}
case kafka.PartitionEOF:
fmt.Printf("%% Reached %v\n", e)
case kafka.Error:
fmt.Fprintf(os.Stderr, "%% Error: %v\n", e)
run = false
default:
fmt.Printf("Ignored %v\n", e)
}
}
This example calls Consumer.CommitMessage() before processing the message
for simplicity. Committing on every message adds significant overhead in
practice. A better approach is to collect a batch of messages, run the
synchronous commit, and then process the messages only if the commit succeeds.
Commit offsets asynchronously
To commit asynchronously, call Commit() in a goroutine:
msg_count := 0
for run == true {
ev := consumer.Poll(100)
switch e := ev.(type) {
case *kafka.Message:
msg_count += 1
if msg_count % MIN_COMMIT_COUNT == 0 {
go func() {
offsets, err := consumer.Commit()
}()
}
fmt.Printf("%% Message on %s:\n%s\n",
e.TopicPartition, string(e.Value))
case kafka.PartitionEOF:
fmt.Printf("%% Reached %v\n", e)
case kafka.Error:
fmt.Fprintf(os.Stderr, "%% Error: %v\n", e)
run = false
default:
fmt.Printf("Ignored %v\n", e)
}
}
Handle rebalance events
To receive rebalance events, set go.application.rebalance.enable to
true when you create the consumer. The Poll() method then returns
rebalance events. Handle them as follows:
On
AssignedPartitions, callAssign().On
RevokedPartitions, callUnassign().
consumer, err := kafka.NewConsumer(&kafka.ConfigMap{
"bootstrap.servers": "host1:9092,host2:9092",
"group.id": "foo",
"go.application.rebalance.enable": true})
msg_count := 0
for run == true {
ev := consumer.Poll(100)
switch e := ev.(type) {
case kafka.AssignedPartitions:
fmt.Fprintf(os.Stderr, "%% %v\n", e)
c.Assign(e.Partitions)
case kafka.RevokedPartitions:
fmt.Fprintf(os.Stderr, "%% %v\n", e)
c.Unassign()
case *kafka.Message:
msg_count += 1
if msg_count % MIN_COMMIT_COUNT == 0 {
consumer.Commit()
}
fmt.Printf("%% Message on %s:\n%s\n",
e.TopicPartition, string(e.Value))
case kafka.PartitionEOF:
fmt.Printf("%% Reached %v\n", e)
case kafka.Error:
fmt.Fprintf(os.Stderr, "%% Error: %v\n", e)
run = false
default:
fmt.Printf("Ignored %v\n", e)
}
}
Go client API reference
See the Go client API documentation.