How to send emails in Go (Golang) using SMTP and Email APIs

Your Golang send email practical guide - SMTP and Email APIs methods
Alexey Kachalov Alexey Kachalov 17 december 2025, 13:07 853
For beginners

As a modern Golang apps developer, you'll likely need to send emails from your code at some point, whether that's for user signups, password resets, notifications, whatever. The good news? Go makes this pretty straightforward, offering solid options for handling email via both SMTP and dedicated email APIs.

In this tutorial, I'll walk you through the different ways to send emails in Go using SMTP and Email APIs, with real code examples you can copy and paste to try out as you follow along.

What is Go and Why Use It for Email Sending

Go, also known as Golang, is an open-source programming language created by Google engineers Robert Griesemer, Rob Pike, and Ken Thompson in 2007. It was publicly released in 2009. 

The language was designed to address shortcomings in existing programming languages while maintaining simplicity and efficiency.

Go offers several advantages that make it particularly suitable for email-sending applications:

  • Performance and Efficiency: Go compiles to native machine code, delivering execution speeds comparable to C/C++ while maintaining memory safety through automatic garbage collection. This makes it great for high-throughput email systems that need to process thousands of messages quickly.
  • Built-in Concurrency: Go's goroutines and channels provide lightweight concurrency primitives that excel at handling multiple email-sending operations simultaneously. A single Go program can efficiently manage thousands of concurrent email deliveries without the overhead of traditional threading models.
  • Standard Library Support: The net/smtp package in Go's standard library provides native SMTP functionality, removing external dependencies for basic email operations.
  • Excellent HTTP Support: Go's net/http package makes integrating with modern Email APIs straightforward, allowing developers to use advanced features like templates, analytics, and delivery tracking.

Understanding Email Sending Methods: SMTP vs Email API

Before talking about email functionality, let’s look at the two primary approaches available in Go: SMTP (Simple Mail Transfer Protocol) and Email APIs.

SMTP

SMTP is the standard Internet protocol for email transmission. When you send an email via SMTP in Go, your application connects directly to an SMTP server (either your own or a third-party’s) and transmits messages using the SMTP protocol.

Email API

Email APIs provide HTTP-based interfaces for sending emails. Instead of managing SMTP connections, you make HTTP requests to API endpoints with JSON payloads containing your email data.

For production applications, Email APIs typically offer better developer experience, automation, and operational features, while SMTP is used mostly for legacy applications and proof-of-concept scenarios.

Setting Up Your Go Development Environment

Before sending emails in Go, you need to install Go on your local machine and set up your development environment. If you already work with Go, you can skip this section.

Installing Go on Your System

Go is available on Windows, macOS, and Linux. Follow these steps for your operating system:

For Windows:

  • Download the Windows installer (.msi file) from here.
  • Run the installer and follow the prompts.
  • Go will be installed to C:\Go by default.
  • The installer automatically adds Go to your PATH.

For macOS:

  • Download the .pkg installer from here.
  • Run the package installer.
  • Go will be installed to /usr/local/go.
  • Add Go to your PATH by adding this line to ~/.bash_profile or ~/.zshrc:

export PATH=$PATH:/usr/local/go/bin

 

  • Apply the changes:

source ~/.bash_profile

For Linux:

  • Download the Linux tarball from here.

wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz

 

  • Remove any previous Go installation and extract the archive:

sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz

 

  • Add Go to your PATH in ~/.profile or /etc/profile:

export PATH=$PATH:/usr/local/go/bin

 

  • Apply the changes:

source ~/.profile

Whatever installation option you choose based on your system, run the go version command to verify your installation. You should get a response like this on your terminal:

Setting Up Go Development Environment  | UniOne Blog

Setting Up Your Project

Once Go is installed, create a new directory for your email project:

mkdir golang-email-tutorial
cd golang-email-tutorial

Then, initialize a new Go module:

go mod init email-tutorial

This creates a go.mod file that tracks your project's dependencies.

Sending Emails in Go Using SMTP

The SMTP approach gives you direct control over email transmission. We'll use the Gomail package for this tutorial due to its simplicity and powerful SMTP support.

Gomail is a Go package for sending emails, designed to be simple and efficient. It supports various email features like attachments, embedded images, HTML and text templates, SSL/TLS, and sending multiple emails over a single SMTP connection. 

While it primarily sends emails via SMTP, its flexible interface makes it easy to integrate other methods, like using provider-specific APIs or local mail servers. 

To get started with Gomail, install the package using the command below:

go get gopkg.in/mail.v2

For sending emails via SMTP, you also need an SMTP server. You may either use your own or the one provided by a dedicated service like UniOne.

Follow the steps below to get started with UniOne’s SMTP service:

  1. Sign up for a free UniOne account here.
  2. Add and verify your domain name. To learn more about this, refer to our guide on Setting up the domain’s DNS records. Alternatively, as a new user, you can use the free, pre-verified sandbox domain, which is a temporary domain for testing your email sending functionality before setting up your production domain.
  3. Navigate to Settings -> SMTP Configuration to get your SMTP credentials for sending emails.

Send Plain Text Email via SMTP

Create a file named smtp_plain.go in your directory with the following code. Insert your actual credentials and replace smtp.eu1.unione.io with the server you’ve registered at.

package main

import (
    "fmt"
    "log"
    "gopkg.in/mail.v2"
)

func main() {
    // Create a new email message
    message := mail.NewMessage()
   
    // Set email headers
    message.SetHeader("From", "sender@yourdomain.com")
    message.SetHeader("To", "recipient@example.com")
    message.SetHeader("Subject", "Test Email from Go")
   
    // Set plain text body
    message.SetBody("text/plain", "This is a plain text email sent from a Go application.")
   
    // Configure SMTP dialer
    // Replace with your SMTP credentials
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    // Send the email
    if err := dialer.DialAndSend(message); err != nil {
        log.Fatalf("Failed to send email: %v", err)
    }
   
    fmt.Println("Email sent successfully!")
}

This code above shows how plain text emails are sent via SMTP using the gopkg.in/mail.v2 package. It creates a new message object, configures essential headers (sender, recipient, subject), sets the plain text body content, and establishes an SMTP connection using mail.NewDialer() with your credentials (host, port, username, and password). 

It finally sends the email through DialAndSend(), which handles both the connection establishment and message transmission in a single operation, with error handling to catch and report any delivery failures.

Note: Replace the placeholder SMTP credentials in the code sample with your actual credentials obtained from the SMTP Configuration page in your UniOne dashboard.

To run this code:

go run smtp_plain.go

You should get an email sent confirmation in your terminal like this:

Send Plain Text Email in Go via SMTP | UniOne Blog

When you check the inbox of the email specified in To, you should see a message like this:

Test Email from Go | UniOne Blog

Pretty cool, right? :)

Send HTML Email via SMTP

HTML emails provide rich formatting capabilities. Here's how to send HTML emails while maintaining a plain text fallback:

package main

import (
    "fmt"
    "log"
    "gopkg.in/mail.v2"
)

func main() {
    message := mail.NewMessage()
   
    message.SetHeader("From", "sender@yourdomain.com")
    message.SetHeader("To", "recipient@example.com")
    message.SetHeader("Subject", "HTML Email from Go")
   
    // Set plain text version (fallback)
    message.SetBody("text/plain", "This is the plain text version of the email.")
   
    // Add HTML version
    message.AddAlternative("text/html", `
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body { font-family: Arial, sans-serif; }
                .header { background-color: #4CAF50; color: white; padding: 20px; }
                .content { padding: 20px; }
            </style>
        </head>
        <body>
            <div class="header">
                <h1>Welcome to Our Service</h1>
            </div>
            <div class="content">
                <p>This is an <strong>HTML email</strong> sent from Go.</p>
                <p>It includes formatting and styling.</p>
            </div>
        </body>
        </html>
    `)
   
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    if err := dialer.DialAndSend(message); err != nil {
        log.Fatalf("Failed to send HTML email: %v", err)
    }
   
    fmt.Println("HTML email sent successfully!")
}

Always include both plain text and HTML versions using SetBody() for plain text and AddAlternative() for HTML. Email clients that cannot render HTML will display the plain text version. In addition, HTML-only emails are often despised by spam filters.

When you run the code, you should get an email in your inbox like this:

HTML Email from Go | UniOne Blog

Send Email to Multiple Recipients via SMTP

Sending to multiple recipients requires adding multiple email addresses to the "To" header:

package main

import (
    "fmt"
    "log"
    "gopkg.in/mail.v2"
)

func main() {
    message := mail.NewMessage()
   
    message.SetHeader("From", "sender@yourdomain.com")
   
    // Add multiple recipients
    message.SetHeader("To",
        "recipient1@example.com",
        "recipient2@example.com",
        "recipient3@example.com",
    )
   
    // Optionally add CC and BCC
    message.SetHeader("Cc", "cc-recipient@example.com")
    message.SetHeader("Bcc", "bcc-recipient@example.com")
   
    message.SetHeader("Subject", "Email to Multiple Recipients")
    message.SetBody("text/plain", "This email is sent to multiple recipients.")
   
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    if err := dialer.DialAndSend(message); err != nil {
        log.Fatalf("Failed to send email: %v", err)
    }
   
    fmt.Println("Email sent to multiple recipients successfully!")
}

The SetHeader("To", ...) method accepts multiple email addresses as variadic arguments, while SetHeader("Cc", ...) and SetHeader("Bcc", ...) allow you to add carbon copy and blind carbon copy recipients, respectively. Once configured, the DialAndSend() method sends one email that reaches all specified recipients in their respective recipient categories (To, CC, or BCC).

An important note: in UniOne’s SMTP service, these headers are treated in a non-standard way. By default, it sends each recipient from the “To” and “CC” headers their own copy of the letter with a single email address in the final “To” header. To revert to the standard behaviour, you must enable strict mode by setting the strict parameter to true in the X-UNIONE header like this: X-UNIONE: {"strict":true}, or contact technical support to enable this mode for all your emails. Also, the “To” header must not be empty even if CC addresses are present.

Send Email with Attachments via SMTP

Attachments are the way you and I send images, documents, invoices, or reports via email. Gomail handles file attachments seamlessly:

package main

import (
    "fmt"
    "log"
    "gopkg.in/mail.v2"
)

func main() {
    message := mail.NewMessage()
   
    message.SetHeader("From", "sender@yourdomain.com")
    message.SetHeader("To", "recipient@example.com")
    message.SetHeader("Subject", "Email with Attachment")
   
    message.SetBody("text/html", `
        <p>Please find the attached document.</p>
    `)
   
    // Attach files (provide the full file path)
    message.Attach("/path/to/document.pdf")
    message.Attach("/path/to/image.png")
   
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    if err := dialer.DialAndSend(message); err != nil {
        log.Fatalf("Failed to send email with attachment: %v", err)
    }
   
    fmt.Println("Email with attachments sent successfully!")
}

The Attach() method automatically handles MIME encoding and sets appropriate content types based on file extensions.

Run the code, and you should get an email like this:

Send Email with Attachment in Go | UniOne Blog

Send Email with Embedded Images via SMTP

Embedded images display inline within the email body, providing a better user experience than attachments:

package main

import (
    "fmt"
    "log"
    "gopkg.in/mail.v2"
)

func main() {
    message := mail.NewMessage()
   
    message.SetHeader("From", "sender@yourdomain.com")
    message.SetHeader("To", "recipient@example.com")
    message.SetHeader("Subject", "Email with Embedded Image")
   
    // Embed image and assign it a Content-ID
    message.Embed("/path/to/logo.png")
   
    message.SetBody("text/html", `
        <!DOCTYPE html>
        <html>
        <body>
            <h2>Our Company Logo</h2>
            <img src="cid:logo.png" alt="Company Logo" width="200">
            <p>This image is embedded in the email.</p>
        </body>
        </html>
    `)
   
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    if err := dialer.DialAndSend(message); err != nil {
        log.Fatalf("Failed to send email with embedded image: %v", err)
    }
   
    fmt.Println("Email with embedded image sent successfully!")
}

Reference embedded images in HTML using cid:filename.ext, where filename.ext matches the embedded file name.

Send Emails Asynchronously via SMTP

For applications that send multiple emails, asynchronous sending prevents blocking your main application flow. Go's goroutines make this straightforward:

package main

import (
    "fmt"
    "log"
    "sync"
    "time"
    "gopkg.in/mail.v2"
)

func sendEmailAsync(recipient string, dialer *mail.Dialer, wg *sync.WaitGroup) {
    defer wg.Done()
   
    message := mail.NewMessage()
    message.SetHeader("From", "sender@yourdomain.com")
    message.SetHeader("To", recipient)
    message.SetHeader("Subject", "Async Email Notification")
    message.SetBody("text/plain", "This email was sent asynchronously.")
   
    if err := dialer.DialAndSend(message); err != nil {
        log.Printf("Failed to send email to %s: %v", recipient, err)
        return
    }
   
    fmt.Printf("Email sent to %s\n", recipient)
}

func main() {
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    recipients := []string{
        "user1@example.com",
        "user2@example.com",
        "user3@example.com",
    }
   
    var wg sync.WaitGroup
   
    for _, recipient := range recipients {
        wg.Add(1)
        go sendEmailAsync(recipient, dialer, &wg)
    }
   
    // Wait for all goroutines to complete
    wg.Wait()
   
    fmt.Println("All emails sent!")
}

Code Explanation:

  • sync.WaitGroup tracks all goroutines to ensure the program waits for completion;
  • Each email is sent in a separate goroutine using the go keyword;
  • defer wg.Done() decrements the wait group counter when the function completes;
  • wg.Wait() blocks execution until all emails are sent.

Send Bulk Emails via SMTP

Bulk email sending requires rate limiting, or throttling, to avoid overwhelming SMTP servers or triggering spam filters. With some SMTP providers, including UniOne, you do not need to implement throttling by yourself – the provider queues all your messages and takes care of all sending limits, which, by the way, may be different for different target domains. If you still need to add throttling capability to your code, follow this example:

package main

import (
    "fmt"
    "log"
    "sync"
    "time"
    "gopkg.in/mail.v2"
)

func sendBulkEmail(recipient string, dialer *mail.Dialer, wg *sync.WaitGroup, throttle <-chan time.Time) {
    defer wg.Done()
   
    // Wait for throttle permission
    <-throttle
   
    message := mail.NewMessage()
    message.SetHeader("From", "sender@yourdomain.com")
    message.SetHeader("To", recipient)
    message.SetHeader("Subject", "Monthly Newsletter")
    message.SetBody("text/html", `
        <h2>Monthly Newsletter</h2>
        <p>Here are this month's updates...</p>
    `)
   
    if err := dialer.DialAndSend(message); err != nil {
        log.Printf("Failed to send email to %s: %v", recipient, err)
        return
    }
   
    fmt.Printf("Email sent to %s\n", recipient)
}

func main() {
    dialer := mail.NewDialer("smtp.eu1.unione.io", 587, "your-username", "your-password")
   
    // Load your recipient list (example data)
    recipients := []string{
        "subscriber1@example.com",
        "subscriber2@example.com",
        // Add more recipients...
    }
   
    var wg sync.WaitGroup
   
    // Create throttle: 1 email per second
    throttle := time.Tick(1 * time.Second)
   
    for _, recipient := range recipients {
        wg.Add(1)
        go sendBulkEmail(recipient, dialer, &wg, throttle)
    }
   
    wg.Wait()
   
    fmt.Println("Bulk email campaign completed!")
}

The time.Tick() function creates a channel that sends a value at specified intervals, effectively limiting email sending to one per second in this example. Adjust the interval based on your SMTP provider's limits.

Sending Emails in Go Using Email API

Email APIs provide a modern, HTTP-based approach to sending emails with enhanced features like templates, analytics, and webhook notifications. This section demonstrates how to integrate UniOne's Email API with your Go application.

UniOne's Email API uses HTTPS POST requests with JSON payloads. Be sure to specify the correct hostname:

Authentication is handled via API key in the X-API-KEY HTTP header. You can obtain your API key from your dashboard by navigating to Account -> Security -> API key.

Send Plain Text Email via Email API

Create a file named api_plain.go and enter the code below:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

// Recipient represents an email recipient with optional substitutions and metadata
type Recipient struct {
    Email         string            `json:"email"`
    Substitutions map[string]string `json:"substitutions,omitempty"`
    Metadata      map[string]string `json:"metadata,omitempty"`
}

// MessageBody contains the email content
type MessageBody struct {
    Plaintext string `json:"plaintext"`
    HTML      string `json:"html,omitempty"`
}

// Message represents the complete email message structure
type Message struct {
    Recipients  []Recipient `json:"recipients"`
    Body        MessageBody `json:"body"`
    Subject     string      `json:"subject"`
    FromEmail   string      `json:"from_email"`
    FromName    string      `json:"from_name,omitempty"`
    ReplyTo     string      `json:"reply_to,omitempty"`
    TrackLinks  int         `json:"track_links,omitempty"`
    TrackRead   int         `json:"track_read,omitempty"`
}

// EmailRequest is the top-level request structure
type EmailRequest struct {
    Message Message `json:"message"`
}

// EmailResponse represents the API response
type EmailResponse struct {
    Status       string            `json:"status"`
    JobID        string            `json:"job_id"`
    Emails       []string          `json:"emails"`
    FailedEmails map[string]string `json:"failed_emails,omitempty"`
}

func main() {
    // API configuration - use US or EU endpoint based on your account
    apiURL := "https://eu1.unione.io/en/transactional/api/v1/email/send.json"
    apiKey := "your-api-key-here" // Get this from https://eu1.unione.io/en/settings/security/api
   
    // Create email request
    emailReq := EmailRequest{
        Message: Message{
            Recipients: []Recipient{
                {
                    Email: "recipient@example.com",
                },
            },
            Body: MessageBody{
                Plaintext: "This is a plain text email sent via UniOne Email API from Go.",
            },
            Subject:   "Plain Text Email from Go",
            FromEmail: "sender@yourdomain.com",
            FromName:  "Your Application",
        },
    }
   
    // Marshal request to JSON
    jsonData, err := json.Marshal(emailReq)
    if err != nil {
        log.Fatalf("Failed to marshal JSON: %v", err)
    }
   
    // Create HTTP request
    req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
    if err != nil {
        log.Fatalf("Failed to create request: %v", err)
    }
   
    // Set headers
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Accept", "application/json")
    req.Header.Set("X-API-KEY", apiKey)
   
    // Send request
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("Failed to send request: %v", err)
    }
    defer resp.Body.Close()
   
    // Read response
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("Failed to read response: %v", err)
    }
   
    // Parse response
    var emailResp EmailResponse
    if err := json.Unmarshal(body, &emailResp); err != nil {
        log.Fatalf("Failed to parse response: %v", err)
    }
   
    // Check response
    if resp.StatusCode == 200 && emailResp.Status == "success" {
        fmt.Printf("Email sent successfully!\n")
        fmt.Printf("Job ID: %s\n", emailResp.JobID)
        fmt.Printf("Sent to: %v\n", emailResp.Emails)
    } else {
        fmt.Printf("Failed to send email. Status: %d\n", resp.StatusCode)
        fmt.Printf("Response: %s\n", string(body))
    }
}

 

  • The job_id returned in the response can be used to track email delivery status.
  • Failed emails are returned in the failed_emails object with rejection reasons.
  • Maximum request size is 10 MB.

When you run the code, you should get an output in your terminal like this:

Send Plain Text Email in Go via Email API | UniOne Blog

When you check the email, you should see this:

Plain Text Email from Go | UniOne Blog

Send HTML Email via Email API

HTML emails require both html and plaintext fields in the body:

// Use the same struct definitions from the previous example
func main() {
    apiURL := "https://eu1.unione.io/en/transactional/api/v1/email/send.json"
    apiKey := "your-api-key-here"
   
    htmlContent := `
<!DOCTYPE html>
<html>
<head>
    <style>
        .container {
            max-width: 600px;
            margin: 0 auto;
            font-family: Arial, sans-serif;
        }
        .header {
            background-color: #4CAF50;
            color: white;
            padding: 20px;
            text-align: center;
        }
        .content {
            padding: 20px;
            background-color: #f9f9f9;
        }
        .button {
            background-color: #4CAF50;
            color: white;
            padding: 12px 24px;
            text-decoration: none;
            border-radius: 4px;
            display: inline-block;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Welcome to Our Service!</h1>
        </div>
        <div class="content">
            <p>Hello,</p>
            <p>Thank you for signing up. We're excited to have you on board.</p>
            <p><a href="https://example.com/get-started" class="button">Get Started</a></p>
        </div>
    </div>
</body>
</html>
`

    emailReq := EmailRequest{
        Message: Message{
            Recipients: []Recipient{
                {
                    Email: "recipient@example.com",
                },
            },
            Body: MessageBody{
                Plaintext: "Welcome to Our Service! Thank you for signing up. Visit https://example.com/get-started to get started.",
                HTML:      htmlContent,
            },
            Subject:    "Welcome to Our Service!",
            FromEmail:  "noreply@yourdomain.com",
            FromName:   "Your Service Team",
            TrackLinks: 1, // Enable link tracking
            TrackRead:  1, // Enable open tracking
        },
    }

 

  • track_links: 1 – logs an event when recipients click links in your email
  • track_read: 1 – logs an event when recipients open your email

Tracking data is available via webhooks or the UniOne dashboard.

Send Email to Multiple Recipients via Email API

The UniOne API supports up to 500 recipients per request:

emailReq := EmailRequest{
    Message: Message{
        Recipients: []Recipient{
            {
                Email: "user1@example.com",
                Substitutions: map[string]string{
                    "name": "John",
                },
            },
            {
                Email: "user2@example.com",
                Substitutions: map[string]string{
                    "name": "Jane",
                },
            },
            {
                Email: "user3@example.com",
                Substitutions: map[string]string{
                    "name": "Bob",
                },
            },
        },
        Body: MessageBody{
            Plaintext: "Hello {{name}}, this is a personalized message!",
            HTML:      "<p>Hello <strong>{{name}}</strong>, this is a personalized message!</p>",
        },
        Subject:        "Personalized Message",
        FromEmail:      "sender@yourdomain.com",
        FromName:       "Your Application",
        TemplateEngine: "simple", // Use simple template engine for substitutions
    },
}

 

  • Each recipient can have unique substitution values.
  • Use {{variable_name}} syntax in your content.
  • Supports simple, velocity, or liquid template engines.
  • Variables work in subject, html, plaintext, and from_name fields.

Send Email with Attachments via Email API

Attachments must be base64-encoded. UniOne supports attachments up to 7MB each:

// Attachment represents a file attachment
type Attachment struct {
    Type    string `json:"type"`
    Name    string `json:"name"`
    Content string `json:"content"` // Base64-encoded file content
}

// Update Message struct to include attachments
type Message struct {
    Recipients  []Recipient  `json:"recipients"`
    Body        MessageBody  `json:"body"`
    Subject     string       `json:"subject"`
    FromEmail   string       `json:"from_email"`
    FromName    string       `json:"from_name,omitempty"`
    Attachments []Attachment `json:"attachments,omitempty"`
}

func main() {
    apiURL := "https://eu1.unione.io/en/transactional/api/v1/email/send.json"
    apiKey := "your-api-key-here"
   
    // Read file and encode to base64
    fileData, err := os.ReadFile("/path/to/document.pdf")
    if err != nil {
        log.Fatalf("Failed to read file: %v", err)
    }
    encodedFile := base64.StdEncoding.EncodeToString(fileData)
   
    // Create email with attachment
    emailReq := EmailRequest{
        Message: Message{
            Recipients: []Recipient{
                {Email: "recipient@example.com"},
            },
            Body: MessageBody{
                Plaintext: "Please find the attached document.",
                HTML:      "<p>Please find the attached <strong>document</strong>.</p>",
            },
            Subject:   "Document Attached",
            FromEmail: "sender@yourdomain.com",
            FromName:  "Document Service",
            Attachments: []Attachment{
                {
                    Type:    "application/pdf",
                    Name:    "document.pdf",
                    Content: encodedFile,
                },
            },
        },
    }

Important notes:

  • Maximum attachment size is about 7MB (9,786,710 bytes when base64-encoded; keep request size below 10Mb).
  • The / (forward slash) symbol is not allowed in attachment names.
  • Common MIME types are: application/pdf, image/jpeg, image/png, text/plain, application/vnd.ms-excel.

Send Emails Asynchronously via Email API

Use goroutines for concurrent API requests, just like with SMTP:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "sync"
    "time"
)

func sendEmailAsync(recipient string, apiURL, apiKey string, wg *sync.WaitGroup) {
    defer wg.Done()
   
    emailReq := EmailRequest{
        Message: Message{
            Recipients: []Recipient{
                {Email: recipient},
            },
            Body: MessageBody{
                Plaintext: "This email was sent asynchronously via UniOne API.",
            },
            Subject:   "Async Notification",
            FromEmail: "noreply@yourdomain.com",
        },
    }
   
    jsonData, err := json.Marshal(emailReq)
    if err != nil {
        log.Printf("Failed to marshal for %s: %v", recipient, err)
        return
    }
   
    req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-KEY", apiKey)
   
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        log.Printf("Request failed for %s: %v", recipient, err)
        return
    }
    defer resp.Body.Close()
   
    body, _ := io.ReadAll(resp.Body)
   
    var emailResp EmailResponse
    if err := json.Unmarshal(body, &emailResp); err == nil && emailResp.Status == "success" {
        fmt.Printf("Email sent to %s, Job ID: %s\n", recipient, emailResp.JobID)
    } else {
        fmt.Printf("Failed to send to %s: %s\n", recipient, string(body))
    }
}

func main() {
    apiURL := "https://eu1.unione.io/en/transactional/api/v1/email/send.json"
    apiKey := "your-api-key-here"
   
    recipients := []string{
        "user1@example.com",
        "user2@example.com",
        "user3@example.com",
    }
   
    var wg sync.WaitGroup
   
    for _, recipient := range recipients {
        wg.Add(1)
        go sendEmailAsync(recipient, apiURL, apiKey, &wg)
    }
   
    wg.Wait()
    fmt.Println("All emails processed!")
}

Send Bulk Emails via Email API

For bulk sending, use batching with the 500-recipient limit and implement rate limiting, if necessary. Again, with UniOne you do not need to care about the latter – the service does it for you, and in a more sophisticated way.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "sync"
    "time"
)

func sendBulkBatch(recipients []Recipient, apiURL, apiKey string, wg *sync.WaitGroup, throttle <-chan time.Time) {
    defer wg.Done()
   
    // Wait for throttle
    <-throttle
   
    emailReq := EmailRequest{
        Message: Message{
            Recipients: recipients, // Up to 500 recipients
            Body: MessageBody{
                Plaintext: "Hello {{name}}, here's this month's newsletter.",
                HTML:      "<h2>Monthly Newsletter</h2><p>Hello <strong>{{name}}</strong>, here are the updates...</p>",
            },
            Subject:        "Monthly Newsletter",
            FromEmail:      "newsletter@yourdomain.com",
            FromName:       "Newsletter Team",
            TemplateEngine: "simple",
            TrackLinks:     1,
            TrackRead:      1,
        },
    }
   
    jsonData, _ := json.Marshal(emailReq)
   
    req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-KEY", apiKey)
   
    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        log.Printf("Batch failed: %v", err)
        return
    }
    defer resp.Body.Close()
   
    body, _ := io.ReadAll(resp.Body)
   
    var emailResp EmailResponse
    if err := json.Unmarshal(body, &emailResp); err == nil {
        fmt.Printf("Batch sent! Job ID: %s, Sent: %d, Failed: %d\n",
            emailResp.JobID, len(emailResp.Emails), len(emailResp.FailedEmails))
    }
}

func main() {
    apiURL := "https://eu1.unione.io/en/transactional/api/v1/email/send.json"
    apiKey := "your-api-key-here"
   
    // Example: 1500 subscribers
    allRecipients := make([]Recipient, 1500)
    for i := 0; i < 1500; i++ {
        allRecipients[i] = Recipient{
            Email: fmt.Sprintf("subscriber%d@example.com", i),
            Substitutions: map[string]string{
                "name": fmt.Sprintf("Subscriber %d", i),
            },
        }
    }
   
    // Split into batches of 500
    batchSize := 500
    var wg sync.WaitGroup
    throttle := time.Tick(1 * time.Second) // 1 batch per second
   
    for i := 0; i < len(allRecipients); i += batchSize {
        end := i + batchSize
        if end > len(allRecipients) {
            end = len(allRecipients)
        }
       
        batch := allRecipients[i:end]
        wg.Add(1)
        go sendBulkBatch(batch, apiURL, apiKey, &wg, throttle)
    }
   
    wg.Wait()
    fmt.Println("Bulk campaign completed!")
}

Best Practices for Bulk Sending:

  • Batch recipients in groups of 500 (API maximum).
  • Implement rate limiting to avoid overwhelming the API.
  • Use substitutions for personalization.
  • Handle failed emails in the response.
  • Monitor delivery via webhooks for real-time status updates.

Go Libraries for Email Sending

Several libraries are available for sending emails in Go, each with different features and use cases.

Gomail

Gomail (which was used in this tutorial) is the most popular third-party library for email sending in Go. It provides a clean, intuitive API and handles MIME encoding automatically.

Key Features:

  • Simple API for email composition;
  • Automatic handling of attachments and embedded images;
  • Support for SSL/TLS encryption;
  • Text and HTML email support;
  • Special character encoding.

net/smtp Package

The net/smtp package is Go's standard library solution for SMTP communication. While it provides basic functionality, it lacks features like attachment handling and HTML email support.

Key Features:

  • No external dependencies;
  • Direct SMTP protocol implementation;
  • Plain authentication and CRAM-MD5 authentication;
  • Low-level control over SMTP operations.

Limitations:

  • No built-in MIME attachment support;
  • Manual header construction required;
  • Limited to basic email functionality;
  • Frozen package (no new features being added).

It is great for simple notification systems or when external dependencies must be minimized.

Here’s a basic example of this package’s implementation:

package main

import (
    "log"
    "net/smtp"
)

func main() {
    auth := smtp.PlainAuth("", "username@example.com", "password", "smtp.example.com")
   
    to := []string{"recipient@example.com"}
    msg := []byte("To: recipient@example.com\r\n" +
        "Subject: Test Email\r\n" +
        "\r\n" +
        "This is the email body.\r\n")
   
    err := smtp.SendMail("smtp.example.com:587", auth, "sender@example.com", to, msg)
    if err != nil {
        log.Fatal(err)
    }
}

mail Package

The mail package provides another alternative with features like attachment support and connection pooling.

Great for applications needing connection pooling for high-volume sending.

Best Practices for Sending Emails in Go

Implementing email functionality correctly requires attention to deliverability, security, and performance considerations.

Here are some best practices to consider during implementation.

  • Use TLS Encryption: Always enable TLS when connecting to SMTP servers to encrypt credentials and email content:

dialer := mail.NewDialer("smtp.example.com", 587, "username", "password")
dialer.TLSConfig = &tls.Config{
    ServerName: "smtp.example.com",
}

 

  • Store Credentials Securely: Never hardcode API keys or passwords in source code. Use environment variables:

import "os"

apiKey := os.Getenv("EMAIL_API_KEY")
smtpPassword := os.Getenv("SMTP_PASSWORD")

 

  • Implement SPF, DKIM, and DMARC: Configure these email authentication protocols in your DNS records to improve deliverability and prevent spoofing. Most email service providers like UniOne handle DKIM signing automatically.

Conclusion

Go provides powerful capabilities for email sending through both SMTP and modern Email APIs. The choice between these approaches depends on your specific requirements.

Choose SMTP when you need compatibility with legacy code, or want to avoid vendor lock-in. The Gomail library offers the best balance of features and ease of use for SMTP-based sending.

Choose Email APIs when you need top speed and advanced features like delivery analytics, template management, or simplified integration. APIs handle complex tasks like MIME encoding automatically, provide better error reporting and offer event tracking.

For production applications, consider using established email service providers like UniOne, which usually offer both SMTP service and Email API options with features like deliverability optimization, dedicated IPs, and comprehensive analytics.

Remember to implement proper error handling, respect rate limits, maintain sender reputation, and follow email authentication best practices to ensure your emails reach recipients' inboxes reliably.

For production-grade email delivery in your Go applications, consider these UniOne services:

  • SMTP Service – Reliable SMTP infrastructure with advanced features like variable substitution, template management, and detailed analytics.
  • Email API – Developer-friendly HTTP API for transactional and marketing emails with real-time event tracking.
  • Email Testing – Tools to debug your SMTP session before sending at scale.

FAQ

How do I handle bounced emails in Go?

Implement webhook endpoints to receive bounce notifications from your email provider. Most services, including UniOne, provide real-time event webhooks that notify your application of bounces, spam complaints, and other delivery events. Store these events in your database and maintain suppression lists to avoid sending to unreachable addresses.

What's the difference between using net/smtp and Gomail?

The net/smtp package is Go's standard library SMTP implementation, providing basic functionality but requiring manual MIME encoding for attachments and HTML emails. Gomail is a third-party library that simplifies email creation by automatically handling attachments, embedded images, and proper MIME encoding. For production applications, Gomail offers significantly better developer experience.

Do I need a dedicated IP address for sending emails from Go?

For transactional emails with low to moderate volume, shared IPs provided by email services work quite well. Dedicated IPs become important when sending large volumes (100,000+ monthly emails) or when you need complete control over your sender reputation. Dedicated IPs allow you to isolate your sending reputation from other users and are highly recommended for established businesses with consistent sending patterns.

Related Articles
Blog
For beginners
Email Throttling: The Essence, Meaning and Methods
Throttling is an issue many email marketers face. For all we know, email providers could be throttl
Vitalii Piddubnyi
16 july 2024, 08:085 min
Blog
For experts
Send Emails in Ruby: A Practical Guide
Your comprehensive guide on powering the Ruby project with emails.
Denys Romanov
02 july 2025, 11:3320 min
Blog
For experts
Send Email with JavaScript: Every Method Developers Should Know
Start sending emails with JavaScript in minutes.
Vitalii Piddubnyi
18 march 2026, 05:4120 min