快速上手 - Go

依赖与安装

Go 包 github.com/apache/tsfile/go/tsfile 是基于 C++ 实现的 cgo 绑定,因此必须先有一份 C/C++ 构建产物,Go 代码才能编译。 获取方式有两种:

  • 安装我们发布的 C/C++ 包或 SDK,无需自行构建 C++,适用于 Linux、macOS 和 Windows。
  • 从源码构建 C++ 库,适用于需要修改 TsFile 或所在平台没有发布包的情况。

两种方式下 Go 模块都从 GitHub 获取,目前尚未发布到模块代理。

依赖

  • Go >= 1.22,且 CGO_ENABLED=1
  • C/C++ 编译工具链和 CMake,仅在从源码构建原生库时需要
  • JDK >= 17 和 Maven,仅在从源码构建原生库时需要
  • Windows 上受支持的 cgo 工具链为 MinGW-w64

方式一:使用已发布的 C/C++ 包(Linux、macOS、Windows)

按下载与安装页面安装运行库和开发包,可以是 SDK 压缩包、DEB/RPM 包、Homebrew Formula 或 Windows ZIP。头文件安装在 include/tsfile 子目录下,因此需要把 cgo 的头文件路径指向该目录,库路径指向安装前缀的库目录:

export TSFILE_HOME=/path/to/tsfile-sdk   # 或:$(brew --prefix local/tsfile250/tsfile)
export CGO_CFLAGS="-I$TSFILE_HOME/include/tsfile"
export CGO_LDFLAGS="-L$TSFILE_HOME/lib -Wl,-rpath,$TSFILE_HOME/lib"

其中 -rpath 让编译出的程序在运行时能找到 libtsfile;如果共享库安装在系统标准目录下可以去掉。 对于安装在 /usr 下的系统包,链接器无需 -L 就能找到库,但头文件仍需要 -I/usr/include/tsfile。 这些环境变量会追加到包内自带的编译参数之后,而包内参数指向源码检出的 cpp/target/build 目录;使用已安装的包时 该目录并不存在,链接器会打印一条搜索路径警告,可以忽略。

Windows 上解压 ZIP 后,在 PowerShell 中设置同样的变量即可。ZIP 内含 bin\tsfile.dll 和 lib\tsfile.lib 导入库, MinGW 的链接器可以直接使用它,-ltsfile 照常通过 -L 找到:

$env:TSFILE_HOME = "C:\path\to\tsfile-2.5.0-windows-x86_64"
$env:CGO_CFLAGS = "-I$env:TSFILE_HOME\include\tsfile"
$env:CGO_LDFLAGS = "-L$env:TSFILE_HOME\lib -ltsfile"
$env:Path = "$env:TSFILE_HOME\bin;$env:Path"

Windows 没有运行时的库搜索路径,编译出的程序通过 PATH 查找 tsfile.dll:保持 bin 在 PATH 中, 或者把 tsfile.dll 复制到可执行文件旁边。该 DLL 静态链接了 C 运行库,只依赖系统库,因此不需要安装 Visual C++ 运行库。

cgo 只在编译该包时读取这些变量,因此需要在每个构建程序的终端里设置,或者用 go env -w CGO_CFLAGS=... CGO_LDFLAGS=... 记录下来。运行已经编译好的程序时不需要它们。

方式二:从源码构建原生库

克隆源码,并在仓库根目录构建 C++ 库:

git clone https://github.com/apache/tsfile.git
cd tsfile
./mvnw -P with-cpp -DskipTests package

构建完成后会在 cpp/target/build/include 生成头文件、在 cpp/target/build/lib 生成库,Go 包默认就链接这里。

Windows 上需要使用 MinGW 构建。MinGW-w64 既是本项目在 Windows 上默认的 C++ 生成器,也是 cgo 编译 Go 包所用的编译器, 因此 -Dcpp.toolchain=mingw 构建出的 DLL 和导入库可以直接配合 Go 工具链使用。源码构建会把 tsfile.dll 输出到 cpp\target\build\lib(而不是发布包的 bin 目录),因此除了编译器所在的 MinGW-w64 bin 目录外,还要把该目录加入 PATH(或把 DLL 复制到可执行文件旁边):

.\mvnw.cmd -P with-cpp -Dcpp.toolchain=mingw -DskipTests package
$env:Path = "$PWD\cpp\target\build\lib;C:\mingw64\bin;$env:Path"

每打开一个新的终端,都要在构建或运行 Go 程序前执行上面的 PATH 设置。

在项目中使用该模块

使用已安装的包(方式一)时,可以直接在你的 Go 项目中拉取模块:

go get github.com/apache/tsfile/go@develop
go mod tidy

模块目前还没有发布 tag,@develop 会解析成分支头部的伪版本号。为了构建可复现,请把该伪版本号记录在 go.mod 中, 或者固定到某个具体 commit。

使用源码构建(方式二)时,用 replace 指向检出目录下的 go 目录,Go 包就会使用它旁边构建好的头文件和库:

go mod edit -replace github.com/apache/tsfile/go=/path/to/tsfile/go
go get github.com/apache/tsfile/go

如果想直接运行自带的示例,在检出目录的 go 目录下执行:

cd go
go run ./examples/table_read_write

写入流程

构造 Writer

schema := tsfile.TableSchema{
	Table: "table1",
	Columns: []tsfile.ColumnSchema{
		{Name: "id1", DataType: tsfile.DataTypeString, Category: tsfile.ColumnCategoryTag},
		{Name: "id2", DataType: tsfile.DataTypeString, Category: tsfile.ColumnCategoryTag},
		{Name: "s1", DataType: tsfile.DataTypeInt32, Category: tsfile.ColumnCategoryField},
		{Name: "s2", DataType: tsfile.DataTypeBoolean, Category: tsfile.ColumnCategoryField},
	},
}

writer, err := tsfile.NewWriter("test.tsfile", schema, tsfile.WithMemoryThreshold(10*1024*1024))
if err != nil {
	return err
}
defer writer.Close()

NewWriter 会创建或截断文件。WithMemoryThreshold 是可选参数,默认为 128 MB。

写入数据

tablet, err := tsfile.NewTablet([]tsfile.TabletColumn{
	{Name: "id1", DataType: tsfile.DataTypeString},
	{Name: "id2", DataType: tsfile.DataTypeString},
	{Name: "s1", DataType: tsfile.DataTypeInt32},
	{Name: "s2", DataType: tsfile.DataTypeBoolean},
}, 5)
if err != nil {
	return err
}
defer tablet.Close()

for row := 0; row < 5; row++ {
	if err := tablet.AddTimestamp(row, int64(row)); err != nil {
		return err
	}
	tablet.SetString(row, 0, "id1_filed_1")
	tablet.SetString(row, 1, "id2_filed_1")
	tablet.SetInt32(row, 2, int32(row))
	tablet.SetBool(row, 3, true)
}

if err := writer.WriteTableTablet(tablet); err != nil {
	return err
}

Tablet 的行和列下标都从 0 开始。每个 setter 都会校验下标和列类型,当下标越界或写入值与声明类型不符时返回错误,应与 AddTimestamp 一样检查这些错误。

也可以通过 Arrow record batch 或 table 写入数据。其 schema 必须包含 int64 或 timestamp[ns](不带时区)类型的 time 列,其后紧跟表 schema 中的全部列:

arrowSchema := arrow.NewSchema([]arrow.Field{
	{Name: "time", Type: arrow.PrimitiveTypes.Int64},
	{Name: "id1", Type: arrow.BinaryTypes.String},
	{Name: "id2", Type: arrow.BinaryTypes.String},
	{Name: "s1", Type: arrow.PrimitiveTypes.Int32},
	{Name: "s2", Type: arrow.FixedWidthTypes.Boolean},
}, nil)
builder := array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
defer builder.Release()

timeBuilder := builder.Field(0).(*array.Int64Builder)
id1Builder := builder.Field(1).(*array.StringBuilder)
id2Builder := builder.Field(2).(*array.StringBuilder)
s1Builder := builder.Field(3).(*array.Int32Builder)
s2Builder := builder.Field(4).(*array.BooleanBuilder)
for row := 5; row < 10; row++ {
	timeBuilder.Append(int64(row))
	id1Builder.Append("id1_filed_1")
	id2Builder.Append("id2_filed_1")
	s1Builder.Append(int32(row))
	s2Builder.Append(true)
}

record := builder.NewRecord()
defer record.Release()
if err := writer.WriteArrowBatch(record); err != nil {
	return err
}

关闭文件

writer.Close()

示例代码

使用这些接口的示例代码可以在以下链接中找到 https://github.com/apache/tsfile/blob/develop/go/examples/table_read_write/main.go

查询流程

构造 Reader

reader, err := tsfile.NewReader("test.tsfile")
if err != nil {
	return err
}
defer reader.Close()

构造 Query Request

result, err := reader.Query(
	"table1",
	[]string{"id1", "id2", "s1", "s2"},
	tsfile.WithTimeRange(2, 8),
	tsfile.WithTagFilter(tsfile.And(tsfile.Eq("id1", "id1_filed_1"), tsfile.Eq("id2", "id2_filed_1"))),
	tsfile.WithLimit(100),
)
if err != nil {
	return err
}
defer result.Close()

时间范围、Tag 过滤、offset、limit 和批次大小都是可选参数,均会下推到原生读取器。

查询数据

metadata := result.Metadata()
for _, column := range metadata {
	fmt.Printf("%s(%d) ", column.Name, column.DataType)
}
fmt.Println()

for {
	hasNext, err := result.Next()
	if err != nil {
		return err
	}
	if !hasNext {
		break
	}
	for column := 1; column <= len(metadata); column++ {
		isNull, err := result.IsNull(column)
		if err != nil {
			return err
		}
		if isNull {
			fmt.Print("null ")
			continue
		}
		switch metadata[column-1].DataType {
		case tsfile.DataTypeBoolean:
			value, err := result.Bool(column)
			if err != nil {
				return err
			}
			fmt.Print(value, " ")
		case tsfile.DataTypeInt32, tsfile.DataTypeDate:
			value, err := result.Int32(column)
			if err != nil {
				return err
			}
			fmt.Print(value, " ")
		case tsfile.DataTypeInt64, tsfile.DataTypeTimestamp:
			value, err := result.Int64(column)
			if err != nil {
				return err
			}
			fmt.Print(value, " ")
		case tsfile.DataTypeFloat:
			value, err := result.Float32(column)
			if err != nil {
				return err
			}
			fmt.Print(value, " ")
		case tsfile.DataTypeDouble:
			value, err := result.Float64(column)
			if err != nil {
				return err
			}
			fmt.Print(value, " ")
		case tsfile.DataTypeString, tsfile.DataTypeText:
			value, err := result.String(column)
			if err != nil {
				return err
			}
			fmt.Print(value, " ")
		}
	}
	fmt.Println()
}

结果集的列下标从 1 开始:第 1 列是时间戳,其后按查询顺序排列所选列。类型化 getter(Bool、Int32、Int64、 Float32、Float64、String、Bytes)在列类型不匹配时返回错误,因此需要像上例一样按元数据分发。

以 Arrow 批次查询数据

当 WithBatchSize 为正数时,查询运行在批次模式下,数据以 Arrow record batch 的形式返回:

result, err := reader.Query("table1", []string{"id1", "id2", "s1", "s2"}, tsfile.WithBatchSize(1024))
if err != nil {
	return err
}
defer result.Close()

for {
	record, err := result.ReadArrowRecordBatch()
	if errors.Is(err, io.EOF) {
		break
	}
	if err != nil {
		return err
	}
	fmt.Println(record.NumRows())
	record.Release()
}

关闭文件

result.Close()
reader.Close()

关闭 reader 时也会关闭由它创建的所有结果集。

示例代码

使用这些接口的示例代码可以在以下链接中找到 https://github.com/apache/tsfile/blob/develop/go/examples/table_read_write/main.go

注意:以上读写示例均基于表模型接口,接口定义介绍可见Go 接口定义。 若需了解树模型相关内容,请联系我们。