Привет, Хабр!
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», которые отмечают практическую направленность занятий, поддержку преподавателей и удобный формат домашних заданий.