To convert a slice of floats to a byte slice for writing to a binary file in Go, you can use the encoding/binary package to handle byte order conversion safely and efficiently. Here's how you can do it:

Step-by-Step Explanation:

  1. Import Required Packages: Use bytes for the buffer and encoding/binary for handling binary data with specified endianness.
  2. Create a Buffer: A bytes.Buffer collects the binary data.
  3. Write Floats to Buffer: Use binary.Write to encode the entire float slice into the buffer with the desired byte order (LittleEndian or BigEndian).
  4. Handle Errors: Check for errors during the write operation.
  5. Extract Byte Slice: Convert the buffer's contents to a []byte for writing to a file.

Example Code:

Go:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "bytes"
    "encoding/binary"
    "os"
)

func main() {
    // Example float64 slice
    floats := []float64{3.14, 1.618, 2.718}

    // Create a buffer and write the floats to it in LittleEndian format
    var buf bytes.Buffer
    err := binary.Write(&buf, binary.LittleEndian, floats)
    if err != nil {
        panic(err)
    }

    // Get the byte slice
    byteSlice := buf.Bytes()

    // Write the byte slice to a file
    err = os.WriteFile("output.bin", byteSlice, 0644)
    if err != nil {
        panic(err)
    }
}

Notes:

This approach ensures portability and correct handling of byte order, making it suitable for writing binary files that may be read on different architectures.

Benchmark

Go:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func encode1(fs []float32) []byte {
    buf := make([]byte, len(fs)*4)
    for i, f := range fs {
        u := math.Float32bits(f)
        binary.LittleEndian.PutUint32(buf[i*4:], u)
    }
    return buf
}

func encode2(fs []float32) []byte {
    var buf bytes.Buffer
    binary.Write(&buf, binary.LittleEndian, fs)
    return buf.Bytes()
}

func BenchmarkEncode1(b *testing.B) {
    encode1(make([]float32, b.N))
}

func BenchmarkEncode2(b *testing.B) {
    encode2(make([]float32, b.N))
}

Results:

plaintext:
1
2
BenchmarkEncode1-8      604999933                1.899 ns/op
BenchmarkEncode2-8      228542809                5.191 ns/op

Lastly, the absolute fastest method is to use unsafe:

Go:
1
2
3
4
5
6
7
func encodeUnsafe(fs []float32) []byte {
    return unsafe.Slice((*byte)(unsafe.Pointer(&fs[0])), len(fs)*4)
}

func decodeUnsafe(bs []byte) []float32 {
    return unsafe.Slice((*float32)(unsafe.Pointer(&bs[0])), len(bs)/4)
}

for float64

Go:
1
2
3
4
5
6
7
8
9
func float64ToBytes(f64 float64) []byte {
    bts := make([]byte, 8)
    (*(*[]float64)(unsafe.Pointer(&bts)))[0] = f64
    return bts
}

func bytesToF64(bts []byte) float64 {
    return *(*float64)(unsafe.Pointer(&bts[0]))
}

You could even make it with zero allocation like this

Go:
1
2
3
4
5
6
7
8
func float64ToBytes(f64 float64) []byte {
    var bts []byte
    sh := (*reflect.SliceHeader)(unsafe.Pointer(&bts))
    sh.Data = uintptr(unsafe.Pointer(&f64))
    sh.Len = 8
    sh.Cap = sh.Len
    return bts
}

Use math.Float64bits to get the float64 as a uint64

Go:
1
2
3
4
5
6
7
8
9
10
11
func float64ToByte(f float64) []byte {
   var buf [8]byte
   binary.BigEndian.PutUint64(buf[:], math.Float64bits(f))
   return buf[:]
}

func float64frombytes(bytes []byte) float64 {
   bits := binary.BigEndian.Uint64(bytes)
   float := math.Float64frombits(bits)
   return float
}