登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go fitsio 怎样向FITS表追加一条结构化记录

来源:17golang原创

时间:2026-10-31 01:10:56 464浏览 收藏

我第一次把观测结果写成 FITS 表时,最容易混淆的是“写入一行”和“把表写进文件”其实是两件事。用 Go fitsio 时,可以先准备一个带 fits 标签的结构体,把它作为表模式,再调用 Table.Write 填入一条记录,最后调用 File.Write 提交这个表 HDU。

官方地址:https://pkg.go.dev/github.com/astrogo/fitsio

本文里的“追加”限定为向新建 FITS 文件中的表追加一条记录。fitsio 的公开文档把 Open 定义为只读入口,所以不要把下面的写法理解成对已有 FITS 文件的原地更新方案。

先判断你手里的是新文件还是已有文件

如果目标是生成一个新的观测文件,处理边界很清楚:os.File 负责文件句柄,fitsio.Create 创建写入模式的 FITS 对象,NewTableFrom 根据结构体建立表,Table.Write 写当前表行,File.Write 把表作为 HDU 写入文件。

如果目标是打开已有文件再改写,先停下来确认库版本和实现是否提供可用的读写路径。官方包文档中 Open 是只读打开,本文不把“追加”包装成原地修改。

用结构体先确定 FITS 表的列布局

说明文件资源、表对象、当前行数据、HDU 提交和关闭动作的静态责任边界。
图2:边界说明图,展示 Table.Write、File.Write 与资源关闭的职责,不是并发运行证据。

我更愿意先写一个专门表示一行数据的结构体,而不是直接手工拼列。这样列名和 Go 字段放在一起,后面排查“写入列顺序不对”时,定位范围会小很多。结构体标签使用 FITS 列名,字段类型则决定写入值的 Go 类型。

Go Observation 结构体字段映射到 FITS OBSERVATIONS 表列并交给 Table.Write 的静态说明图
图1:结构说明图,展示 Go 结构体到 FITS 表列的对应关系,不是运行截图。
type Observation struct {
    // fits 标签固定列名,字段类型对应要写入的值类型。
    ID int64 `fits:"ID"`
    // Flux 保存测量值,使用浮点类型避免把小数截断。
    Flux float64 `fits:"FLUX"`
    // Band 保存观测波段,列名与结构体字段可以保持同义但不必同名。
    Band string `fits:"BAND"`
}

这里的结构体值既可以作为 NewTableFrom 的模式样本,也可以作为后面 Table.Write 的一行数据。模式样本不需要填业务值;真正写入时再创建一个带值的 Observation。

用 NewTableFrom 和 Table.Write 写入一条记录

写表时我会把“表结构”和“当前行数据”分开命名。这样即使后续要连续写多行,也只需要重复构造数据值并调用 Write,不必重新创建表。

package main

import (
    "log"
    "os"

    fitsio "github.com/astrogo/fitsio"
)

type Observation struct {
    // 标签确定 FITS 列名,类型决定写入值的形态。
    ID int64 `fits:"ID"`
    Flux float64 `fits:"FLUX"`
    Band string `fits:"BAND"`
}

func writeOne(path string) error {
    // 外层文件由调用方创建和关闭,避免把底层句柄交给 fitsio 代管。
    out, err := os.Create(path)
    if err != nil {
        return err
    }
    defer out.Close()

    fits, err := fitsio.Create(out)
    if err != nil {
        return err
    }
    defer fits.Close()

    // 零值结构体只用来描述表的列布局,不代表要写入的那一行。
    table, err := fitsio.NewTableFrom("OBSERVATIONS", Observation{}, fitsio.BINARY_TBL)
    if err != nil {
        return err
    }
    defer table.Close()

    row := Observation{ID: 1001, Flux: 12.75, Band: "r"}
    // Table.Write 把这一组字段写入表的当前行。
    if err := table.Write(row); err != nil {
        return err
    }

    // File.Write 负责把已经填充的表 HDU 写入 FITS 文件。
    if err := fits.Write(table); err != nil {
        return err
    }
    return nil
}

func main() {
    if err := writeOne("observations.fits"); err != nil {
        log.Fatal(err)
    }
}

这个示例的关键不是把所有 API 都塞进一个函数,而是保留三条可检查的边界:NewTableFrom 定义表,Table.Write 写记录,File.Write 写 HDU。需要多条记录时,创建一次 table,按顺序多次调用 table.Write,最后再调用一次 fits.Write。

写入失败时,先保留可回退的输出

生产任务里我不会直接把正式文件名交给写入流程。因为结构体类型不匹配、表定义失败或底层写入失败时,目标路径可能只得到半成品。更稳妥的做法是先写临时文件,所有写入动作返回成功后再改名。

func publishTable(finalPath string, row Observation) error {
    tempPath := finalPath + ".tmp"
    // 临时路径失败时只影响待提交文件,不覆盖上一次正式结果。
    out, err := os.Create(tempPath)
    if err != nil {
        return err
    }

    fits, err := fitsio.Create(out)
    if err != nil {
        out.Close()
        os.Remove(tempPath)
        return err
    }
    table, err := fitsio.NewTableFrom("OBSERVATIONS", Observation{}, fitsio.BINARY_TBL)
    if err != nil {
        fits.Close()
        out.Close()
        os.Remove(tempPath)
        return err
    }

    // 先写行,再提交 HDU;任何一步失败都不替换正式文件。
    err = table.Write(row)
    if err == nil {
        err = fits.Write(table)
    }
    table.Close()
    closeErr := fits.Close()
    fileErr := out.Close()
    if err == nil {
        err = closeErr
    }
    if err == nil {
        err = fileErr
    }
    if err != nil {
        os.Remove(tempPath)
        return err
    }

    // 只有临时文件完整关闭后才替换正式文件。
    return os.Rename(tempPath, finalPath)
}

回退动作不是“重新调用一次 Write”。失败后应关闭已打开的资源、删除临时文件,并保留原正式文件;确认错误原因已修复后,再以新的临时文件重新生成。

用一张责任表确认是否真的写成功

对象负责什么常见误区
Observation{}提供列结构样本把零值模式误认为业务记录
Table.Write(row)把一行字段写入表以为它已经把整个 HDU 提交到文件
File.Write(table)写入表 HDU漏掉这一步就只是在内存中准备表对象
Table.Close / File.Close释放 fitsio 对象资源以为 File.Close 会自动关闭底层 os.File

官方文档特别说明,File.Close 不会关闭底层的 io.Reader 或 io.Writer。所以示例里仍然由 defer out.Close() 或显式关闭 os.File。遇到“文件句柄还占用”或输出无法重命名时,优先检查这条边界。

这套写法适合什么场景

如果你的任务是从 Go 服务或批处理程序生成一个新的 FITS 表,这种结构体驱动的写法足够直接:列布局集中、写行入口明确、失败时可以用临时文件回退。若你的任务是修改已有 FITS 文件、追加到已有 HDU,或者需要 FITS 原地更新,则应先确认当前库版本是否提供对应的读写能力,再决定是否换用专门的 FITS 工具链。

我自己的判断是:当记录结构稳定、输出文件由当前任务负责生成时,NewTableFrom + Table.Write + File.Write 很顺手;当文件生命周期复杂、需要原地增量更新时,不要只因为 API 名字里有 Write 就假定它支持数据库式追加。

相关问题

Table.Write 可以连续调用吗?

可以把同一个表对象作为目标,按记录逐次调用 Table.Write。表的创建和 HDU 提交不要在每一行之间重复进行。

为什么写了 Table.Write 仍然没有输出文件内容?

Table.Write 面向表的当前行;还需要把表对象交给 File.Write,并正常关闭 FITS 对象和底层文件。

fitsio.Create 会关闭 os.File 吗?

不要依赖这种假设。底层 os.File 仍应由创建它的代码负责关闭,临时文件提交流程也应在关闭成功后再改名。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>