Skip to main content

Tables, Matrices, and Data Types Across Programming Languages

Published: April 24, 2026 Updated: August 28, 2026 Larry Qu 8 min read

Engineers constantly move data between CSV files, JSON APIs, in-memory objects, and databases. Bugs happen when we conflate different representations — treating a matrix like a table, or a JSON array like a typed record set. This article explains the mental models and shows concrete Go (and Python/JS) code for each pattern.

Table vs Matrix: Not the Same Thing

Matrix

A matrix is a 2D structure where every cell has the same type — usually numeric. It’s shaped m × n and optimized for linear algebra.

// Go matrix: [][]float64
matrix := [][]float64{
    {1.0, 2.0, 3.0},
    {4.0, 5.0, 6.0},
    {7.0, 8.0, 9.0},
}

// Matrix operations
func transpose(m [][]float64) [][]float64 {
    rows, cols := len(m), len(m[0])
    t := make([][]float64, cols)
    for i := range t {
        t[i] = make([]float64, rows)
        for j := range t[i] {
            t[i][j] = m[j][i]
        }
    }
    return t
}

func dotProduct(a, b []float64) float64 {
    var sum float64
    for i := range a {
        sum += a[i] * b[i]
    }
    return sum
}

Table

A table is row/column data where columns have different types. It represents real-world entities — users, orders, events.

// Go table: []struct
type Order struct {
    ID          string
    CustomerID  string
    Amount      float64
    Status      string
    CreatedAt   time.Time
}

orders := []Order{
    {ID: "ord-1", CustomerID: "c-100", Amount: 49.99, Status: "paid", CreatedAt: time.Now()},
    {ID: "ord-2", CustomerID: "c-101", Amount: 12.50, Status: "pending", CreatedAt: time.Now()},
}

Key difference: matrix rows are interchangeable (same type); table rows represent records with schema (different types per column).

Row-Oriented vs Column-Oriented Models

Row-Oriented (Record Model)

Each row is a complete record. Natural for APIs, CRUD operations, and transactional workloads.

// Row model in Go — []struct
type Event struct {
    UserID    string
    EventType string
    Timestamp time.Time
    Value     float64
}

events := []Event{
    {"u1", "click", time.Now(), 1.0},
    {"u2", "view", time.Now(), 1.0},
}

// API serialization — natural with row model
json.NewEncoder(w).Encode(events)

// Per-record access — O(1)
event := events[42]

Column-Oriented (Columnar Model)

Each column is a separate slice. Faster for analytics (scan one column, skip others), better compression.

// Column model — separate slices per field
type EventColumns struct {
    UserIDs    []string
    EventTypes []string
    Timestamps []time.Time
    Values     []float64
}

cols := EventColumns{
    UserIDs:    []string{"u1", "u2", "u3"},
    EventTypes: []string{"click", "view", "click"},
    Values:     []float64{1.0, 1.0, 2.0},
}

// Analytics scan — only touches the Values slice
var totalClicks float64
for i, t := range cols.EventTypes {
    if t == "click" {
        totalClicks += cols.Values[i]
    }
}

Apache Arrow (used by Parquet, DuckDB, Polars) is the standard columnar format for Go data engineering.

Language Comparison

Concept Go Python JavaScript Database
Single record struct dict / @dataclass object row / document
Table (records) []struct list[dict] / DataFrame array of objects table / collection
Matrix [][]float64 numpy.ndarray nested arrays numeric column arrays
Schema struct type TypedDict / dataclass TypeScript interface CREATE TABLE
Column access for _, r := range rows { r.Name } df["name"] rows.map(r => r.name) SELECT name FROM ...

Go Table Patterns

Typed Record Set with Filtering

package main

import (
    "fmt"
    "strings"
    "time"
)

type User struct {
    ID        string
    Name      string
    Email     string
    Role      string
    CreatedAt time.Time
    Active    bool
}

type UserTable []User

func (t UserTable) Filter(fn func(User) bool) UserTable {
    result := make(UserTable, 0)
    for _, u := range t {
        if fn(u) {
            result = append(result, u)
        }
    }
    return result
}

func (t UserTable) Map(fn func(User) User) UserTable {
    result := make(UserTable, len(t))
    for i, u := range t {
        result[i] = fn(u)
    }
    return result
}

func (t UserTable) GroupBy(keyFn func(User) string) map[string]UserTable {
    groups := make(map[string]UserTable)
    for _, u := range t {
        key := keyFn(u)
        groups[key] = append(groups[key], u)
    }
    return groups
}

func (t UserTable) Find(fn func(User) bool) (User, bool) {
    for _, u := range t {
        if fn(u) { return u, true }
    }
    return User{}, false
}

func main() {
    users := UserTable{
        {ID: "u1", Name: "Alice", Role: "admin",  Active: true},
        {ID: "u2", Name: "Bob",   Role: "user",   Active: true},
        {ID: "u3", Name: "Carol", Role: "admin",  Active: false},
        {ID: "u4", Name: "Dave",  Role: "user",   Active: true},
    }

    // Filter active users
    active := users.Filter(func(u User) bool { return u.Active })
    fmt.Printf("Active: %d\n", len(active)) // 3

    // Group by role
    byRole := users.GroupBy(func(u User) string { return u.Role })
    fmt.Printf("Admins: %d, Users: %d\n", len(byRole["admin"]), len(byRole["user"]))

    // Find specific user
    admin, found := users.Find(func(u User) bool {
        return u.Role == "admin" && u.Active
    })
    if found { fmt.Println("First active admin:", admin.Name) }
}

Reading CSV into a Table

import (
    "encoding/csv"
    "fmt"
    "os"
    "strconv"
    "time"
)

type Product struct {
    ID    string
    Name  string
    Price float64
    Stock int
}

func ReadProductsCSV(path string) ([]Product, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("opening %s: %w", path, err)
    }
    defer f.Close()

    r := csv.NewReader(f)
    r.TrimLeadingSpace = true

    // Skip header
    if _, err := r.Read(); err != nil {
        return nil, err
    }

    var products []Product
    for {
        record, err := r.Read()
        if err != nil {
            break // EOF
        }
        if len(record) < 4 {
            continue // skip malformed rows
        }

        price, _ := strconv.ParseFloat(record[2], 64)
        stock, _ := strconv.Atoi(record[3])

        products = append(products, Product{
            ID:    record[0],
            Name:  record[1],
            Price: price,
            Stock: stock,
        })
    }
    return products, nil
}

JSON API ↔ Database Model Transformation

A common pattern: different shapes for API vs storage:

// API shape (what clients send/receive)
type OrderRequest struct {
    CustomerID string  `json:"customer_id"`
    Items      []struct {
        ProductID string  `json:"product_id"`
        Quantity  int     `json:"quantity"`
    } `json:"items"`
}

type OrderResponse struct {
    ID         string    `json:"id"`
    CustomerID string    `json:"customer_id"`
    Total      float64   `json:"total"`
    Status     string    `json:"status"`
    CreatedAt  time.Time `json:"created_at"`
    Items      []Item    `json:"items"`
}

// Database shape (flat, normalized)
type OrderRow struct {
    ID         string
    CustomerID string
    Total      float64
    Status     string
    CreatedAt  time.Time
}

type OrderItemRow struct {
    OrderID   string
    ProductID string
    Quantity  int
    UnitPrice float64
}

// Transform: DB rows → API response
func toOrderResponse(order OrderRow, items []OrderItemRow) OrderResponse {
    apiItems := make([]Item, len(items))
    for i, item := range items {
        apiItems[i] = Item{
            ProductID: item.ProductID,
            Quantity:  item.Quantity,
            UnitPrice: item.UnitPrice,
            Subtotal:  float64(item.Quantity) * item.UnitPrice,
        }
    }
    return OrderResponse{
        ID:         order.ID,
        CustomerID: order.CustomerID,
        Total:      order.Total,
        Status:     order.Status,
        CreatedAt:  order.CreatedAt,
        Items:      apiItems,
    }
}

Schema Drift and Validation

Schema drift happens when data changes shape over time — age becomes a string in one source, timestamps lose timezone info, fields go missing. Catch it at ingestion:

import (
    "fmt"
    "strings"
    "time"
)

type RawRecord map[string]interface{}

type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("field %q: %s", e.Field, e.Message)
}

func validateOrderRecord(r RawRecord) (*Order, []error) {
    var errs []error

    // Required string field
    id, ok := r["id"].(string)
    if !ok || id == "" {
        errs = append(errs, ValidationError{"id", "required string"})
    }

    // Numeric field — might arrive as string from CSV
    var amount float64
    switch v := r["amount"].(type) {
    case float64:
        amount = v
    case string:
        fmt.Sscanf(v, "%f", &amount)
    default:
        errs = append(errs, ValidationError{"amount", "expected number"})
    }

    if amount <= 0 {
        errs = append(errs, ValidationError{"amount", "must be positive"})
    }

    // Timestamp normalization
    var createdAt time.Time
    if ts, ok := r["created_at"].(string); ok {
        for _, layout := range []string{time.RFC3339, "2006-01-02", "2006-01-02 15:04:05"} {
            if t, err := time.Parse(layout, ts); err == nil {
                createdAt = t
                break
            }
        }
        if createdAt.IsZero() {
            errs = append(errs, ValidationError{"created_at", "unrecognized timestamp format"})
        }
    }

    if len(errs) > 0 {
        return nil, errs
    }

    return &Order{ID: id, Amount: amount, CreatedAt: createdAt}, nil
}

Dynamic Tables with map[string]interface{}

Sometimes you need to handle unknown schemas (building a query engine, data exploration tool):

type DynamicTable struct {
    Columns []string
    Rows    []map[string]interface{}
}

func (t *DynamicTable) AddRow(row map[string]interface{}) {
    // Auto-discover new columns
    for k := range row {
        found := false
        for _, col := range t.Columns {
            if col == k { found = true; break }
        }
        if !found {
            t.Columns = append(t.Columns, k)
        }
    }
    t.Rows = append(t.Rows, row)
}

func (t *DynamicTable) Column(name string) []interface{} {
    result := make([]interface{}, len(t.Rows))
    for i, row := range t.Rows {
        result[i] = row[name]
    }
    return result
}

func (t *DynamicTable) Filter(col string, fn func(interface{}) bool) *DynamicTable {
    filtered := &DynamicTable{Columns: t.Columns}
    for _, row := range t.Rows {
        if fn(row[col]) {
            filtered.Rows = append(filtered.Rows, row)
        }
    }
    return filtered
}

Python and JavaScript Comparison

# Python: row model (list of dicts)
users = [
    {"id": "u1", "name": "Alice", "active": True},
    {"id": "u2", "name": "Bob",   "active": False},
]

# Filter — equivalent to Go's Filter method
active = [u for u in users if u["active"]]

# Column access — extract one field across all rows
names = [u["name"] for u in users]

# DataFrame (column-oriented)
import pandas as pd
df = pd.DataFrame(users)
active_df = df[df["active"]]
names = df["name"].tolist()
// JavaScript: array of objects (same as Python list-of-dict)
const users = [
  { id: "u1", name: "Alice", active: true },
  { id: "u2", name: "Bob",   active: false },
];

const active = users.filter(u => u.active);
const names  = users.map(u => u.name);

// Reduce to column model
const grouped = users.reduce((acc, u) => {
  (acc[u.active ? "active" : "inactive"] ||= []).push(u);
  return acc;
}, {});

Choosing the Right Model

Workload Use
REST API request/response []struct or struct
Database rows []struct matching table schema
CSV import/export []struct with csv tags
Analytics, aggregations Column model or Apache Arrow
Linear algebra, ML features [][]float64 or gonum
Unknown schema at compile time []map[string]interface{}
Cross-language data exchange JSON (row) or Parquet/Arrow (column)

Summary

  • Matrix = uniform type, 2D, math operations → [][]float64
  • Table = typed columns, record-oriented → []struct
  • Row model = fast per-record access, natural for APIs
  • Column model = fast analytics scans, better compression
  • Always validate and normalize at ingestion boundaries — schema drift causes data quality incidents downstream
  • Use typed structs in Go rather than map[string]interface{} everywhere — the compiler catches mismatches

Resources

Comments

👍 Was this article helpful?