# Go で HCL ファイルを読み取る


published: 2025-04-22

HCL は Terraform で採用されているフォーマット。
構造化したり改行を挟めるので、人間にとって読みやすい、と個人的に思う。

プログラムで扱うにはどうすればいいのかと思い、Go 言語で HCL ファイルを読み取ってみた。


## ライブラリ

hashicorp/hcl を用いて HCL をパースする。
HashiCorp 公式のリポジトリ。おそらく Terraform の内部でも使用されている (はず)


- [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl)
## コード

### `sample.hcl`

```hcl
server {
  host = "0.0.0.0"
  port = 3000

  route {
    path   = "/"
    status = 200
  }

  route {
    path   = "/aaa"
    status = 404
  }
}

```

### `go.mod`

```mod
module github.com/enuesaa/lab/data/go-hcl-parse

go 1.24.0

require github.com/hashicorp/hcl/v2 v2.23.0

require (
	github.com/agext/levenshtein v1.2.1 // indirect
	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
	github.com/google/go-cmp v0.6.0 // indirect
	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
	github.com/zclconf/go-cty v1.13.0 // indirect
	golang.org/x/mod v0.8.0 // indirect
	golang.org/x/sys v0.5.0 // indirect
	golang.org/x/text v0.11.0 // indirect
	golang.org/x/tools v0.6.0 // indirect
)

```

### `main.go`

```go
package main

import (
	"fmt"

	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclparse"
)

type Config struct {
	Server Server `hcl:"server,block"`
}

type Server struct {
	Host   string  `hcl:"host"`
	Port   *int    `hcl:"port,optional"`
	Routes []Route `hcl:"route,block"`
}

type Route struct {
	Path   string `hcl:"path"`
	Status int    `hcl:"status"`
}

func main() {
	parser := hclparse.NewParser()

	file, diags := parser.ParseHCLFile("sample.hcl")
	if diags.HasErrors() {
		panic(diags)
	}

	var config Config

	if diags := gohcl.DecodeBody(file.Body, nil, &config); diags.HasErrors() {
		panic(diags)
	}

	fmt.Println(config.Server.Host) // 0.0.0.0

	if config.Server.Port != nil {
		fmt.Println(*config.Server.Port) // 3000
	}
}

```

### memos

**サンプル**

この HCL ファイルを読み取ってみる


mark: `sample.hcl:1`

struct tag でフィールドを指定する


mark: `main.go:15`

オプショナルに。


mark: `main.go:16`

sample.hcl を読み取る


mark: `main.go:28`

parser にバイト列を渡すこともできる。


mark: `main.go:28`

- [ParseHCL](https://pkg.go.dev/github.com/hashicorp/hcl2/hclparse#Parser.ParseHCL)
デコード


mark: `main.go:35`

## Variable を実現するには？

Partial Decoding をすれば、Terraform の `variable` のように値を参照してセットすることができる。


```hcl
// 例えば..
const "apikey" {
  value = "aaa"
}

service {
  apikey = const.apikey
}

```

下記のページや issue のコメントが分かりやすくありがたい。


- [https://hcl.readthedocs.io/en/latest/go_decoding_gohcl.html#partial-decoding](https://hcl.readthedocs.io/en/latest/go_decoding_gohcl.html#partial-decoding)
- [https://github.com/hashicorp/hcl/issues/496](https://github.com/hashicorp/hcl/issues/496)
サンプルコードを書いてみた。


```go
package main

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclparse"
	"github.com/zclconf/go-cty/cty"
)

var mainhcl = `
  const "apikey" {
    value = "aaa"
  }

  service {
    apikey = const.apikey
  }
`

type Const struct {
	Name  string `hcl:"name,label"`
	Value string `hcl:"value"`
}

type Service struct {
	Apikey string `hcl:"apikey"`
}

func Parse() {
	parser := hclparse.NewParser()

	file, diags := parser.ParseHCL([]byte(mainhcl), "main.hcl")
	if diags.HasErrors() {
		panic(diags)
	}

	// 先に const だけデコードする
	type PartialConfig struct {
		Consts []Const  `hcl:"const,block"`
		Remain hcl.Body `hcl:",remain"` // see https://hcl.readthedocs.io/en/latest/go_decoding_gohcl.html#partial-decoding
	}
	var partial PartialConfig

	if diags := gohcl.DecodeBody(file.Body, nil, &partial); diags.HasErrors() {
		panic(diags)
	}

	// デコードした値を hcl.EvalContext にマッピング
	constmap := make(map[string]cty.Value)
	for _, c := range partial.Consts {
		constmap[c.Name] = cty.StringVal(c.Value)
	}
	evar := &hcl.EvalContext{
		Variables: map[string]cty.Value{
			"const": cty.ObjectVal(constmap),
		},
	}

	type Config struct {
		Service Service `hcl:"service,block"`
		Consts  []Const `hcl:"const,block"`
	}
	var config Config

	// 第2引数で hcl.EvalContext を渡す
	if diags := gohcl.DecodeBody(file.Body, evar, &config); diags.HasErrors() {
		panic(diags)
	}

	fmt.Println(config.Service.Apikey) // aaa
}

```

他にも方法があるかもしれない。
また Terraform の場合は、リソースに依存関係があるので、もっと高度な実装がされていると思う。


