diff --git a/.github/workflows/test-v1.yml b/.github/workflows/test-v1.yml new file mode 100644 index 0000000..7a2f262 --- /dev/null +++ b/.github/workflows/test-v1.yml @@ -0,0 +1,45 @@ +name: CI for v1 Module + +on: + push: + paths-ignore: + - .github/workflows/test-v2.yml + - 'v2/**' + pull_request: + paths-ignore: + - .github/workflows/test-v2.yml + - 'v2/**' + +jobs: + test-v1: + strategy: + matrix: + go-version: [1.23.x, 1.24.x] + os: [ubuntu-latest] + + runs-on: ${{matrix.os}} + + defaults: + run: + working-directory: ./ + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{matrix.go-version}} + + - name: Lint Code + uses: golangci/golangci-lint-action@v6 + + - name: Check format + run: test -z "`gofmt -l .`" + + - name: Run tests + run: go test -v ./... + + - name: Build example + run: cd example && ./linux64_build.sh diff --git a/.github/workflows/test-v2.yml b/.github/workflows/test-v2.yml new file mode 100644 index 0000000..0d4279f --- /dev/null +++ b/.github/workflows/test-v2.yml @@ -0,0 +1,50 @@ +name: CI for v2 Module + +on: + push: + paths: + - .github/workflows/test-v2.yml + - 'v2/**' + pull_request: + paths: + - .github/workflows/test-v2.yml + - 'v2/**' + +jobs: + test-v2: + strategy: + matrix: + go-version: [1.23.x, 1.24.x] + os: [ubuntu-latest] + + runs-on: ${{matrix.os}} + + defaults: + run: + working-directory: ./v2 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: ./v2/go.mod + + - name: Tidy modules + run: go mod tidy + + - name: Lint Code + uses: golangci/golangci-lint-action@v6 + with: + working-directory: ./v2 + + - name: Check format + run: test -z "`gofmt -l .`" + + - name: Run tests + run: go test ./... -v + + - name: Build example + run: cd example && ./linux64_build.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index ced6f74..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,24 +0,0 @@ -on: [push, pull_request] -name: Test -jobs: - test: - strategy: - matrix: - go-version: [1.23.x, 1.24.x] - os: [ubuntu-latest] - runs-on: ${{matrix.os}} - steps: - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: ${{matrix.go-version}} - - name: Checkout - uses: actions/checkout@v2 - - name: gofmt - run: test -z "`gofmt -l .`" - - name: golint - run: test -z "`golint ./...`" - - name: go test - run: go test -v ./... - - name: Build example - run: cd example && ./linux64_build.sh diff --git a/.gitignore b/.gitignore index 86d68c5..4fcc071 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ example/sonyflake_server +v2/example/sonyflake_server diff --git a/README.md b/README.md index 7a3732e..e38715b 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,16 @@ As a result, Sonyflake has the following advantages and disadvantages: - The lifetime (174 years) is longer than that of Snowflake (69 years) - It can work in more distributed machines (2^16) than Snowflake (2^10) -- It can generate 2^8 IDs per 10 msec at most in a single machine/thread (slower than Snowflake) +- It can generate 2^8 IDs per 10 msec at most in a single instance (fewer than Snowflake) However, if you want more generation rate in a single host, -you can easily run multiple Sonyflake ID generators concurrently using goroutines. +you can easily run multiple Sonyflake instances parallelly using goroutines. Installation ------------ ``` -go get github.com/sony/sonyflake +go get github.com/sony/sonyflake/v2 ``` Usage @@ -43,42 +43,42 @@ You can configure Sonyflake by the struct Settings: ```go type Settings struct { + TimeUnit time.Duration StartTime time.Time - MachineID func() (uint16, error) - CheckMachineID func(uint16) bool + MachineID func() (int, error) + CheckMachineID func(int) bool } ``` +- TimeUnit is the time unit of Sonyflake. + If TimeUnit is 0, the default time unit is used, which is 10 msec. + TimeUnit must be equal to or greater than 1 msec. + - StartTime is the time since which the Sonyflake time is defined as the elapsed time. - If StartTime is 0, the start time of the Sonyflake is set to "2014-09-01 00:00:00 +0000 UTC". - If StartTime is ahead of the current time, Sonyflake is not created. + If StartTime is 0, the start time of the Sonyflake instance is set to "2025-01-01 00:00:00 +0000 UTC". + StartTime must be before the current time. -- MachineID returns the unique ID of the Sonyflake instance. - If MachineID returns an error, Sonyflake is not created. - If MachineID is nil, default MachineID is used. - Default MachineID returns the lower 16 bits of the private IP address. +- MachineID returns the unique ID of a Sonyflake instance. + If MachineID returns an error, the instance will not be created. + If MachineID is nil, the default MachineID is used, which returns the lower 16 bits of the private IP address. -- CheckMachineID validates the uniqueness of the machine ID. - If CheckMachineID returns false, Sonyflake is not created. +- CheckMachineID validates the uniqueness of a machine ID. + If CheckMachineID returns false, the instance will not be created. If CheckMachineID is nil, no validation is done. In order to get a new unique ID, you just have to call the method NextID. ```go -func (sf *Sonyflake) NextID() (uint64, error) +func (sf *Sonyflake) NextID() (int64, error) ``` -NextID can continue to generate IDs for about 174 years from StartTime. +NextID can continue to generate IDs for about 174 years from StartTime by default. But after the Sonyflake time is over the limit, NextID returns an error. -> **Note:** -> Sonyflake currently does not use the most significant bit of IDs, -> so you can convert Sonyflake IDs from `uint64` to `int64` safely. - AWS VPC and Docker ------------------ -The [awsutil](https://github.com/sony/sonyflake/blob/master/awsutil) package provides +The [awsutil](https://github.com/sony/sonyflake/blob/master/v2/awsutil) package provides the function AmazonEC2MachineID that returns the lower 16-bit private IP address of the Amazon EC2 instance. It also works correctly on Docker by retrieving [instance metadata](http://docs.aws.amazon.com/en_us/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). @@ -89,7 +89,7 @@ So if each EC2 instance has a unique private IP address in AWS VPC, the lower 16 bits of the address is also unique. In this common case, you can use AmazonEC2MachineID as Settings.MachineID. -See [example](https://github.com/sony/sonyflake/blob/master/example) that runs Sonyflake on AWS Elastic Beanstalk. +See [example](https://github.com/sony/sonyflake/blob/master/v2/example) that runs Sonyflake on AWS Elastic Beanstalk. License ------- diff --git a/example/sonyflake_server.go b/example/sonyflake_server.go index 5b96e5f..df20e0a 100644 --- a/example/sonyflake_server.go +++ b/example/sonyflake_server.go @@ -33,10 +33,17 @@ func handler(w http.ResponseWriter, r *http.Request) { } w.Header()["Content-Type"] = []string{"application/json; charset=utf-8"} - w.Write(body) + _, err = w.Write(body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } func main() { http.HandleFunc("/", handler) - http.ListenAndServe(":8080", nil) + err := http.ListenAndServe(":8080", nil) + if err != nil { + panic(err) + } } diff --git a/mock/sonyflake_mock.go b/mock/sonyflake_mock.go index 70bc01a..f861ce1 100644 --- a/mock/sonyflake_mock.go +++ b/mock/sonyflake_mock.go @@ -1,6 +1,5 @@ -// Package mock offers implementations of interfaces defined in types.go -// This allows complete control over input / output for any given method that consumes -// a given type +// Package mock offers mock implementations of interfaces defined in types.go. +// This allows complete control over input / output for any given method that consumes a given type. package mock import ( @@ -10,7 +9,7 @@ import ( "github.com/sony/sonyflake/types" ) -// NewSuccessfulInterfaceAddrs returns a single private IP address +// NewSuccessfulInterfaceAddrs returns a single private IP address. func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs { ifat := make([]net.Addr, 0, 1) ifat = append(ifat, &net.IPNet{IP: []byte{192, 168, 0, 1}, Mask: []byte{255, 0, 0, 0}}) @@ -20,14 +19,14 @@ func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs { } } -// NewFailingInterfaceAddrs returns an error +// NewFailingInterfaceAddrs returns an error. func NewFailingInterfaceAddrs() types.InterfaceAddrs { return func() ([]net.Addr, error) { return nil, fmt.Errorf("test error") } } -// NewFailingInterfaceAddrs returns an empty slice of addresses +// NewNilInterfaceAddrs returns an empty slice of addresses. func NewNilInterfaceAddrs() types.InterfaceAddrs { return func() ([]net.Addr, error) { return []net.Addr{}, nil diff --git a/types/types.go b/types/types.go index 57fa350..1b53613 100644 --- a/types/types.go +++ b/types/types.go @@ -1,8 +1,8 @@ -// Package Types defines type signatures used throughout SonyFlake. This allows for -// fine-tuned control over imports, and the ability to mock out imports as well +// Package types defines type signatures used throughout sonyflake. +// This provides the ability to mock out imports. package types import "net" -// InterfaceAddrs defines the interface used for retrieving network addresses +// InterfaceAddrs defines the interface used for retrieving network addresses. type InterfaceAddrs func() ([]net.Addr, error) diff --git a/v2/awsutil/awsutil.go b/v2/awsutil/awsutil.go new file mode 100644 index 0000000..ab2936e --- /dev/null +++ b/v2/awsutil/awsutil.go @@ -0,0 +1,64 @@ +// Package awsutil provides utility functions for using Sonyflake on AWS. +package awsutil + +import ( + "errors" + "io" + "net" + "net/http" + "os/exec" + "regexp" + "strconv" + "time" +) + +func amazonEC2PrivateIPv4() (net.IP, error) { + res, err := http.Get("http://169.254.169.254/latest/meta-data/local-ipv4") + if err != nil { + return nil, err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + ip := net.ParseIP(string(body)) + if ip == nil { + return nil, errors.New("invalid ip address") + } + return ip.To4(), nil +} + +// AmazonEC2MachineID retrieves the private IP address of the Amazon EC2 instance +// and returns its lower 16 bits. +// It works correctly on Docker as well. +func AmazonEC2MachineID() (int, error) { + ip, err := amazonEC2PrivateIPv4() + if err != nil { + return 0, err + } + + return int(ip[2])<<8 + int(ip[3]), nil +} + +// TimeDifference returns the time difference between the localhost and the given NTP server. +func TimeDifference(server string) (time.Duration, error) { + output, err := exec.Command("/usr/sbin/ntpdate", "-q", server).CombinedOutput() + if err != nil { + return time.Duration(0), err + } + + re, _ := regexp.Compile("offset (.*) sec") + submatched := re.FindSubmatch(output) + if len(submatched) != 2 { + return time.Duration(0), errors.New("invalid ntpdate output") + } + + f, err := strconv.ParseFloat(string(submatched[1]), 64) + if err != nil { + return time.Duration(0), err + } + return time.Duration(f*1000) * time.Millisecond, nil +} diff --git a/v2/example/Dockerfile b/v2/example/Dockerfile new file mode 100644 index 0000000..4ac65f6 --- /dev/null +++ b/v2/example/Dockerfile @@ -0,0 +1,6 @@ +FROM ubuntu:24.04 + +ADD ./sonyflake_server / +ENTRYPOINT ["/sonyflake_server"] + +EXPOSE 8080 diff --git a/v2/example/Dockerrun.aws.json b/v2/example/Dockerrun.aws.json new file mode 100644 index 0000000..eb8d0a2 --- /dev/null +++ b/v2/example/Dockerrun.aws.json @@ -0,0 +1,8 @@ +{ + "AWSEBDockerrunVersion": "1", + "Ports": [ + { + "ContainerPort": "8080" + } + ] +} diff --git a/v2/example/README.md b/v2/example/README.md new file mode 100644 index 0000000..17fbcab --- /dev/null +++ b/v2/example/README.md @@ -0,0 +1,21 @@ +Example +======= + +This example runs Sonyflake on AWS Elastic Beanstalk. + +Setup +----- + +1. Build the cross compiler for linux/amd64 if using other platforms. + + ``` + cd $GOROOT/src && GOOS=linux GOARCH=amd64 ./make.bash + ``` + +2. Build sonyflake_server in the example directory. + + ``` + ./linux64_build.sh + ``` + +3. Upload the example directory to AWS Elastic Beanstalk. diff --git a/v2/example/linux64_build.sh b/v2/example/linux64_build.sh new file mode 100755 index 0000000..a164783 --- /dev/null +++ b/v2/example/linux64_build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +GOOS=linux GOARCH=amd64 go build sonyflake_server.go diff --git a/v2/example/sonyflake_server.go b/v2/example/sonyflake_server.go new file mode 100644 index 0000000..aee4beb --- /dev/null +++ b/v2/example/sonyflake_server.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/sony/sonyflake/v2" + "github.com/sony/sonyflake/v2/awsutil" +) + +var sf *sonyflake.Sonyflake + +func init() { + var st sonyflake.Settings + st.MachineID = awsutil.AmazonEC2MachineID + + var err error + sf, err = sonyflake.New(st) + if err != nil { + panic(err) + } +} + +func handler(w http.ResponseWriter, r *http.Request) { + id, err := sf.NextID() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + body, err := json.Marshal(sonyflake.Decompose(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header()["Content-Type"] = []string{"application/json; charset=utf-8"} + _, err = w.Write(body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func main() { + http.HandleFunc("/", handler) + err := http.ListenAndServe(":8080", nil) + if err != nil { + panic(err) + } +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..ee45ee3 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,3 @@ +module github.com/sony/sonyflake/v2 + +go 1.22 diff --git a/v2/mock/sonyflake_mock.go b/v2/mock/sonyflake_mock.go new file mode 100644 index 0000000..1bff6ab --- /dev/null +++ b/v2/mock/sonyflake_mock.go @@ -0,0 +1,36 @@ +// Package mock offers mock implementations of interfaces defined in types.go. +// This allows complete control over input / output for any given method that consumes a given type. +package mock + +import ( + "errors" + "net" + + "github.com/sony/sonyflake/v2/types" +) + +// NewSuccessfulInterfaceAddrs returns a single private IP address. +func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs { + ifat := make([]net.Addr, 0, 1) + ifat = append(ifat, &net.IPNet{IP: []byte{192, 168, 0, 1}, Mask: []byte{255, 0, 0, 0}}) + + return func() ([]net.Addr, error) { + return ifat, nil + } +} + +var ErrFailedToGetAddresses = errors.New("failed to get addresses") + +// NewFailingInterfaceAddrs returns an error. +func NewFailingInterfaceAddrs() types.InterfaceAddrs { + return func() ([]net.Addr, error) { + return nil, ErrFailedToGetAddresses + } +} + +// NewNilInterfaceAddrs returns an empty slice of addresses. +func NewNilInterfaceAddrs() types.InterfaceAddrs { + return func() ([]net.Addr, error) { + return []net.Addr{}, nil + } +} diff --git a/v2/sonyflake.go b/v2/sonyflake.go new file mode 100644 index 0000000..67b6939 --- /dev/null +++ b/v2/sonyflake.go @@ -0,0 +1,232 @@ +// Package sonyflake implements Sonyflake, a distributed unique ID generator inspired by Twitter's Snowflake. +// +// A Sonyflake ID is composed of +// +// 39 bits for time in units of 10 msec +// 8 bits for a sequence number +// 16 bits for a machine id +package sonyflake + +import ( + "errors" + "net" + "sync" + "time" + + "github.com/sony/sonyflake/v2/types" +) + +// These constants are the bit lengths of Sonyflake ID parts. +const ( + BitLenTime = 39 // bit length of time + BitLenSequence = 8 // bit length of sequence number + BitLenMachine = 63 - BitLenTime - BitLenSequence // bit length of machine id +) + +// Settings configures Sonyflake: +// +// TimeUnit is the time unit of Sonyflake. +// If TimeUnit is 0, the default time unit is used, which is 10 msec. +// TimeUnit must be equal to or greater than 1 msec. +// +// StartTime is the time since which the Sonyflake time is defined as the elapsed time. +// If StartTime is 0, the start time of the Sonyflake instance is set to "2025-01-01 00:00:00 +0000 UTC". +// StartTime must be before the current time. +// +// MachineID returns the unique ID of a Sonyflake instance. +// If MachineID returns an error, the instance will not be created. +// If MachineID is nil, the default MachineID is used, which returns the lower 16 bits of the private IP address. +// +// CheckMachineID validates the uniqueness of a machine ID. +// If CheckMachineID returns false, the instance will not be created. +// If CheckMachineID is nil, no validation is done. +type Settings struct { + TimeUnit time.Duration + StartTime time.Time + MachineID func() (int, error) + CheckMachineID func(int) bool +} + +// Sonyflake is a distributed unique ID generator. +type Sonyflake struct { + mutex *sync.Mutex + timeUnit int64 + startTime int64 + elapsedTime int64 + sequence int + machine int +} + +var ( + ErrStartTimeAhead = errors.New("start time is ahead of now") + ErrNoPrivateAddress = errors.New("no private ip address") + ErrOverTimeLimit = errors.New("over the time limit") + ErrInvalidMachineID = errors.New("invalid machine id") + ErrInvalidTimeUnit = errors.New("invalid time unit") +) + +const defaultTimeUnit = 1e7 // nsec, i.e. 10 msec + +var defaultInterfaceAddrs = net.InterfaceAddrs + +// New returns a new Sonyflake configured with the given Settings. +// New returns an error in the following cases: +// - Settings.StartTime is ahead of the current time. +// - Settings.MachineID returns an error. +// - Settings.CheckMachineID returns false. +func New(st Settings) (*Sonyflake, error) { + if st.StartTime.After(time.Now()) { + return nil, ErrStartTimeAhead + } + + sf := new(Sonyflake) + sf.mutex = new(sync.Mutex) + sf.sequence = 1<= time.Millisecond { + sf.timeUnit = int64(st.TimeUnit) + } else { + return nil, ErrInvalidTimeUnit + } + + if st.StartTime.IsZero() { + sf.startTime = sf.toInternalTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) + } else { + sf.startTime = sf.toInternalTime(st.StartTime) + } + + var err error + if st.MachineID == nil { + sf.machine, err = lower16BitPrivateIP(defaultInterfaceAddrs) + } else { + sf.machine, err = st.MachineID() + } + if err != nil { + return nil, err + } + + if st.CheckMachineID != nil && !st.CheckMachineID(sf.machine) { + return nil, ErrInvalidMachineID + } + + return sf, nil +} + +// NextID generates a next unique ID as int64. +// After the Sonyflake time overflows, NextID returns an error. +func (sf *Sonyflake) NextID() (int64, error) { + const maskSequence = 1<= 1<= 16 && ip[1] < 32) || ip[0] == 192 && ip[1] == 168 || ip[0] == 169 && ip[1] == 254) +} + +func lower16BitPrivateIP(interfaceAddrs types.InterfaceAddrs) (int, error) { + ip, err := privateIPv4(interfaceAddrs) + if err != nil { + return 0, err + } + + return int(ip[2])<<8 + int(ip[3]), nil +} + +func (sf *Sonyflake) ToTime(id int64) time.Time { + return time.Unix(0, (sf.startTime+Time(id))*sf.timeUnit) +} + +// Time returns the Sonyflake time when the given ID was generated. +func Time(id int64) int64 { + return id >> (BitLenSequence + BitLenMachine) +} + +// SequenceNumber returns the sequence number of a Sonyflake ID. +func SequenceNumber(id int64) int { + const maskSequence = int64((1<> BitLenMachine) +} + +// MachineID returns the machine ID of a Sonyflake ID. +func MachineID(id int64) int { + const maskMachine = int64(1< sleepTime+1 { + t.Errorf("unexpected time: %d", actualTime) + } + + actualSequence := SequenceNumber(id) + if actualSequence != 0 { + t.Errorf("unexpected sequence: %d", actualSequence) + } + + actualMachine := MachineID(id) + if actualMachine != defaultMachineID(t) { + t.Errorf("unexpected machine: %d", actualMachine) + } + + fmt.Println("sonyflake id:", id) + fmt.Println("decompose:", Decompose(id)) +} + +func TestNextID_InSequence(t *testing.T) { + now := time.Now() + sf := newSonyflake(t, Settings{ + TimeUnit: time.Millisecond, + StartTime: now, + }) + startTime := sf.toInternalTime(now) + machineID := int64(defaultMachineID(t)) + + var numID int + var lastID int64 + var maxSeq int64 + + currentTime := startTime + for currentTime-startTime < 100 { + id := nextID(t, sf) + currentTime = sf.toInternalTime(time.Now()) + numID++ + + if id == lastID { + t.Fatal("duplicated id") + } + if id < lastID { + t.Fatal("must increase with time") + } + lastID = id + + parts := Decompose(id) + + actualTime := parts["time"] + overtime := startTime + actualTime - currentTime + if overtime > 0 { + t.Errorf("unexpected overtime: %d", overtime) + } + + actualSequence := parts["sequence"] + if actualSequence > maxSeq { + maxSeq = actualSequence + } + + actualMachine := parts["machine"] + if actualMachine != machineID { + t.Errorf("unexpected machine: %d", actualMachine) + } + } + + if maxSeq != 1<= time.Duration(sf.timeUnit) { + t.Errorf("unexpected time: %v", tm) + } +} + +func TestPrivateIPv4(t *testing.T) { + testCases := []struct { + description string + expected net.IP + interfaceAddrs types.InterfaceAddrs + error error + }{ + { + description: "returns an error", + expected: nil, + interfaceAddrs: mock.NewFailingInterfaceAddrs(), + error: mock.ErrFailedToGetAddresses, + }, + { + description: "empty address list", + expected: nil, + interfaceAddrs: mock.NewNilInterfaceAddrs(), + error: ErrNoPrivateAddress, + }, + { + description: "success", + expected: net.IP{192, 168, 0, 1}, + interfaceAddrs: mock.NewSuccessfulInterfaceAddrs(), + error: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + actual, err := privateIPv4(tc.interfaceAddrs) + + if !errors.Is(err, tc.error) { + t.Fatalf("unexpected error: %v", err) + } + + if !net.IP.Equal(actual, tc.expected) { + t.Errorf("unexpected ip: %s", actual) + } + }) + } +} + +func TestLower16BitPrivateIP(t *testing.T) { + testCases := []struct { + description string + expected int + interfaceAddrs types.InterfaceAddrs + error error + }{ + { + description: "returns an error", + expected: 0, + interfaceAddrs: mock.NewFailingInterfaceAddrs(), + error: mock.ErrFailedToGetAddresses, + }, + { + description: "empty address list", + expected: 0, + interfaceAddrs: mock.NewNilInterfaceAddrs(), + error: ErrNoPrivateAddress, + }, + { + description: "success", + expected: 1, + interfaceAddrs: mock.NewSuccessfulInterfaceAddrs(), + error: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + actual, err := lower16BitPrivateIP(tc.interfaceAddrs) + + if !errors.Is(err, tc.error) { + t.Fatalf("unexpected error: %v", err) + } + + if actual != tc.expected { + t.Errorf("unexpected ip: %d", actual) + } + }) + } +} diff --git a/v2/types/types.go b/v2/types/types.go new file mode 100644 index 0000000..1b53613 --- /dev/null +++ b/v2/types/types.go @@ -0,0 +1,8 @@ +// Package types defines type signatures used throughout sonyflake. +// This provides the ability to mock out imports. +package types + +import "net" + +// InterfaceAddrs defines the interface used for retrieving network addresses. +type InterfaceAddrs func() ([]net.Addr, error)