Привет, Хабр!

Go ценят за предсказуемость и простые решения в стандартной библиотеке, а в сервисах чаще всего упираемся в IP, разбор host:port, CIDR и сериализацию. Сегодня это удобно закрывается стандартным net/netip: компактные value‑типы, корректный парсинг адресов и портов, работа с зонами, проверка принадлежности сетям и быстрые операции без лишних аллокаций. В статье рассмотрим этот пакет подробнее.

netip вводит три базовых типа: Addr для адреса, AddrPort для адреса с портом и Prefix для CIDR. В отличие от net.IP это компактные и сравнимые значения, их можно использовать как ключи map без конвертации в строки.

В Go 1.24 в стандартной библиотеке появились интерфейсы encoding.TextAppender и encoding.BinaryAppender. netip их реализует, поэтому теперь можно дописывать адреса в уже готовый буфер без лишних аллокаций через AppendText и AppendBinary.

Если нужна работа с большими множествами адресов и префиксов, есть go4.org/netipx с IPSet и билдером. Пакет сам по себе позиционируется как продолжение идей из inet.af/netaddr, которые не вошли в стандартную библиотеку.

Синтаксис

Создание и парсинг

ip1 := netip.MustParseAddr("192.0.2.1")   // пойдет для констант
ip2, err := netip.ParseAddr("2001:db8::1")
if err != nil { /* handle */ }
ip3, ok := netip.AddrFromSlice([]byte{10,0,0,1}) // 4 или 16 байт

Addr{} — невалидное нулевое значение. Это не 0.0.0.0 и не ::. Is4() — только чистый IPv4. Для IPv4-в‑IPv6 есть Is4In6() и Unmap().

Свойства и проверки

_ = ip2.Is4()                   // только IPv4
_ = ip2.Is6()                   // IPv6 и IPv4-в-IPv6
_ = ip2.Is4In6()                // "::ffff:1.2.3.4"
_ = ip2.IsLoopback()
_ = ip2.IsPrivate()
_ = ip2.IsGlobalUnicast()
_ = ip2.BitLen()                // 32 для IPv4, 128 для IPv6

IPv4-в‑IPv6 считается IPv6, поэтому BitLen() будет 128, и Is4() вернет false. Если логика «только IPv4» — сначала Unmap().

Конверсия в байты и строки

b := ip2.AsSlice()              // 4 или 16 байт
a4 := ip1.As4()                 // паника, если не IPv4
a16 := ip2.As16()               // всегда 16 байт, зона отброшена

s := ip2.String()               // стандартная форма
sx := ip2.StringExpanded()      // IPv6 без сжатий и с ведущими нулями

У As4() защита от неверного использования — он паникует для IPv6. Метод String вернет строку вида ::ffff:127.0.0.1, что соответствует контракту netip.

Сравнение и порядок

a := netip.MustParseAddr("10.0.0.1")
b := netip.MustParseAddr("10.0.0.2")
_ = a == b
_ = a.Less(b)                   // порядок сортировки
_ = a.Compare(b)                // -1, 0, +1

Порядок определен так: сначала длина (IPv4 перед IPv6), потом значение. IPv6 с зоной идет сразу после того же адреса без зоны.

Зоны

ipz := netip.MustParseAddr("fe80::1").WithZone("eth0")
_ = ipz.Zone()                  // "eth0"
ipz = ipz.WithZone("")          // убрать зону

Зона — локальный идентификатор области видимости IPv6 по RFC 4007. Для строк "host:port" в net IPv6 должен быть в скобках, зона указывается через %zone.

Итерация по адресам

next := a.Next()                // следующий адрес
prev := a.Prev()                // предыдущий адрес

Используемо для сканирования диапазонов в тестах и генераторов.

netip.AddrPort: безошибочный host:port

Разбирать "[v6%zone]:port" руками — плохая идея. Делаем так:

ap, err := netip.ParseAddrPort("[fe80::1%eth0]:8080")
if err != nil { /* handle */ }
ip := ap.Addr()
port := ap.Port()

buf := make([]byte, 0, 64)
buf = ap.AppendTo(buf)          // без аллокаций, Go 1.24+

AppendTo и AppendText опираются на новые интерфейсы encoding.*Appender и позволяют писать в существующий буфер.

netip.Prefix: парсинг, маскирование, проверки

Парсинг и канонизация

p, err := netip.ParsePrefix("192.168.1.0/24")
if err != nil { /* handle */ }

p = p.Masked()                  // обнулит хостовые биты

В префиксах зоны запрещены. Это сознательное правило: fe80::/64 — ок, fe80::%eth0/64 — ошибка. Поведение синхронизировано между net и netip.

Проверка принадлежности и пересечения

ok := p.Contains(ip)            // входит ли адрес в сеть
one := p.IsSingleIP()           // /32 или /128
ov := p.Overlaps(other)         // есть ли пересечение

IPv4 в IPv6 не считается IPv4. Адрес ::ffff:10.0.0.1 не попадет в 10.0.0.0/8. Документация это проговаривает отдельно. Также Contains вернет false, если у ip есть зона, поскольку префиксы зону срезают.

Кейсы

Достать IP клиента из http.Request

func peerAddrPort(r *http.Request) (netip.AddrPort, bool) {
    ap, err := netip.ParseAddrPort(r.RemoteAddr)
    if err != nil { return netip.AddrPort{}, false }
    return ap, ap.IsValid()
}

func peerIP(r *http.Request) (netip.Addr, bool) {
    ap, ok := peerAddrPort(r)
    if !ok { return netip.Addr{}, false }
    ip := ap.Addr()
    if ip.Is4In6() { ip = ip.Unmap() }   // унифицируем IPv4
    return ip, ip.IsValid()
}

Если работаете за прокси, дополняем разбором X-Forwarded-For или Forwarded. Двигаемся справа налево по цепочке адресов и останавливаемся на первом, который не принадлежит списку доверенных прокси сетей. Для списка есть IPSet.

ACL на CIDR с IPSet из netipx

import "go4.org/netipx"

type Filter struct {
    allow *netipx.IPSet
    deny  *netipx.IPSet
}

func NewFilter(allow, deny []string) (*Filter, error) {
    build := func(list []string) (*netipx.IPSet, error) {
        var b netipx.IPSetBuilder
        for _, s := range list {
            p, err := netip.ParsePrefix(s)
            if err != nil { return nil, fmt.Errorf("bad CIDR %q: %w", s, err) }
            b.AddPrefix(p.Masked())
        }
        return b.IPSet()
    }
    a, err := build(allow)
    if err != nil { return nil, err }
    d, err := build(deny)
    if err != nil { return nil, err }
    return &Filter{allow: a, deny: d}, nil
}

func (f *Filter) Allowed(ip netip.Addr) bool {
    if !ip.IsValid() { return false }
    if ip.Zone() != "" { ip = ip.WithZone("") }
    if f.deny.Contains(ip) { return false }
    if f.allow == nil || f.allow.RangeCount() == 0 { return true }
    return f.allow.Contains(ip)
}

IPSet поддерживает конкурентный доступ и хранит диапазоны компактно. Билдер принимает Prefix, любые IPv4-в‑IPv6 автоматически нормализуйте через Masked() и Unmap().

Логи и метрики без мусора через AppendTo

func logPeer(w io.Writer, ap netip.AddrPort) {
    buf := make([]byte, 0, 64)
    buf = append(buf, "peer="...)
    buf = ap.AppendTo(buf)          // без промежуточных строк
    buf = append(buf, '\n')
    _, _ = w.Write(buf)
}

За этим стоят encoding.TextAppender и BinaryAppender из Go 1.24.

JSON c ключами netip.Addr без обвязки

encoding/json сериализует ключи map, если тип реализует encoding.TextMarshaler. netip.Addr это умеет, значит map[netip.Addr]int маршалится прямо.

m := map[netip.Addr]uint64{
    netip.MustParseAddr("10.0.0.1"): 3,
    netip.MustParseAddr("2001:db8::1"): 7,
}
b, _ := json.Marshal(m)  // {"10.0.0.1":3,"2001:db8::1":7}

Порядок ключей не гарантирован — это свойство map.

Стабильная совместимость с net

Иногда нужен net.IP для низкоуровневых API.

// net.IP -> netip.Addr
func toAddr(ip net.IP) (netip.Addr, bool) {
    a, ok := netip.AddrFromSlice(ip) // без лишних предположений
    if !ok { return netip.Addr{}, false }
    return a, true
}

// netip.Addr -> net.IP
func toNetIP(a netip.Addr) net.IP {
    return append(net.IP(nil), a.AsSlice()...) // копия
}

При сборке строк "host:port" юзаем net.JoinHostPort, он сам расставит скобки для IPv6 и обработает зону.


Итоги

netip это рабочий инструмент, берите и пользуйтесь, делитесь своими кейсами в комментариях, а за всеми деталями и нюансами можно пройти сюда — https://pkg.go.dev/net/netip.

Если вам интересно разобраться в Go и освоить его системно, приглашаем на курс «Golang Developer. Basic». Это практическая программа, которая поможет вам уверенно работать с языком и его стандартной библиотекой.

До начала курса вы можете присоединиться к открытым урокам:

20 августа в 20:00 — «Пакет с пакетами: организация кода в Go»
На этом занятии будет разбор того, как в Go устроена работа с пакетами: от базовой структуры проекта до рекомендаций по организации модулей.

4 сентября в 20:00 — «Hello, Go! С нуля до первого кода за 1,5 часа»
Занятие рассчитано на тех, кто только начинает знакомство с языком. Мы разберём базовый синтаксис, правила написания простейшей программы и запустим её.

17 сентября в 20:00 — «Как перестать бояться и уйти в бэкенд? Все про переход на Golang с фронта»
Открытый урок для разработчиков, знакомых с фронтендом, которые рассматривают для себя переход к бэкенду.

Присоединяйтесь и убедитесь сами — взгляните на отзывы участников курса «Golang Developer. Basic», которые отмечают практическую направленность занятий, поддержку преподавателей и удобный формат домашних заданий.

Комментарии (0)