forked from TrueCloudLab/distribution
b22c6b7a4e
Adds READMEs and enforces vendor is done at repository root Signed-off-by: Derek McGowan <derek@mcgstyle.net>
709 lines
17 KiB
Markdown
709 lines
17 KiB
Markdown
INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini)
|
|
===
|
|
|
|
![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)
|
|
|
|
Package ini provides INI file read and write functionality in Go.
|
|
|
|
[简体中文](README_ZH.md)
|
|
|
|
## Feature
|
|
|
|
- Load multiple data sources(`[]byte` or file) with overwrites.
|
|
- Read with recursion values.
|
|
- Read with parent-child sections.
|
|
- Read with auto-increment key names.
|
|
- Read with multiple-line values.
|
|
- Read with tons of helper methods.
|
|
- Read and convert values to Go types.
|
|
- Read and **WRITE** comments of sections and keys.
|
|
- Manipulate sections, keys and comments with ease.
|
|
- Keep sections and keys in order as you parse and save.
|
|
|
|
## Installation
|
|
|
|
To use a tagged revision:
|
|
|
|
go get gopkg.in/ini.v1
|
|
|
|
To use with latest changes:
|
|
|
|
go get github.com/go-ini/ini
|
|
|
|
Please add `-u` flag to update in the future.
|
|
|
|
### Testing
|
|
|
|
If you want to test on your machine, please apply `-t` flag:
|
|
|
|
go get -t gopkg.in/ini.v1
|
|
|
|
Please add `-u` flag to update in the future.
|
|
|
|
## Getting Started
|
|
|
|
### Loading from data sources
|
|
|
|
A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many data sources as you want**. Passing other types will simply return an error.
|
|
|
|
```go
|
|
cfg, err := ini.Load([]byte("raw data"), "filename")
|
|
```
|
|
|
|
Or start with an empty object:
|
|
|
|
```go
|
|
cfg := ini.Empty()
|
|
```
|
|
|
|
When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later.
|
|
|
|
```go
|
|
err := cfg.Append("other file", []byte("other raw data"))
|
|
```
|
|
|
|
If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error.
|
|
|
|
```go
|
|
cfg, err := ini.LooseLoad("filename", "filename_404")
|
|
```
|
|
|
|
The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual.
|
|
|
|
#### Ignore cases of key name
|
|
|
|
When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing.
|
|
|
|
```go
|
|
cfg, err := ini.InsensitiveLoad("filename")
|
|
//...
|
|
|
|
// sec1 and sec2 are the exactly same section object
|
|
sec1, err := cfg.GetSection("Section")
|
|
sec2, err := cfg.GetSection("SecTIOn")
|
|
|
|
// key1 and key2 are the exactly same key object
|
|
key1, err := cfg.GetKey("Key")
|
|
key2, err := cfg.GetKey("KeY")
|
|
```
|
|
|
|
#### MySQL-like boolean key
|
|
|
|
MySQL's configuration allows a key without value as follows:
|
|
|
|
```ini
|
|
[mysqld]
|
|
...
|
|
skip-host-cache
|
|
skip-name-resolve
|
|
```
|
|
|
|
By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options:
|
|
|
|
```go
|
|
cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
|
|
```
|
|
|
|
The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read.
|
|
|
|
### Working with sections
|
|
|
|
To get a section, you would need to:
|
|
|
|
```go
|
|
section, err := cfg.GetSection("section name")
|
|
```
|
|
|
|
For a shortcut for default section, just give an empty string as name:
|
|
|
|
```go
|
|
section, err := cfg.GetSection("")
|
|
```
|
|
|
|
When you're pretty sure the section exists, following code could make your life easier:
|
|
|
|
```go
|
|
section := cfg.Section("")
|
|
```
|
|
|
|
What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you.
|
|
|
|
To create a new section:
|
|
|
|
```go
|
|
err := cfg.NewSection("new section")
|
|
```
|
|
|
|
To get a list of sections or section names:
|
|
|
|
```go
|
|
sections := cfg.Sections()
|
|
names := cfg.SectionStrings()
|
|
```
|
|
|
|
### Working with keys
|
|
|
|
To get a key under a section:
|
|
|
|
```go
|
|
key, err := cfg.Section("").GetKey("key name")
|
|
```
|
|
|
|
Same rule applies to key operations:
|
|
|
|
```go
|
|
key := cfg.Section("").Key("key name")
|
|
```
|
|
|
|
To check if a key exists:
|
|
|
|
```go
|
|
yes := cfg.Section("").HasKey("key name")
|
|
```
|
|
|
|
To create a new key:
|
|
|
|
```go
|
|
err := cfg.Section("").NewKey("name", "value")
|
|
```
|
|
|
|
To get a list of keys or key names:
|
|
|
|
```go
|
|
keys := cfg.Section("").Keys()
|
|
names := cfg.Section("").KeyStrings()
|
|
```
|
|
|
|
To get a clone hash of keys and corresponding values:
|
|
|
|
```go
|
|
hash := cfg.Section("").KeysHash()
|
|
```
|
|
|
|
### Working with values
|
|
|
|
To get a string value:
|
|
|
|
```go
|
|
val := cfg.Section("").Key("key name").String()
|
|
```
|
|
|
|
To validate key value on the fly:
|
|
|
|
```go
|
|
val := cfg.Section("").Key("key name").Validate(func(in string) string {
|
|
if len(in) == 0 {
|
|
return "default"
|
|
}
|
|
return in
|
|
})
|
|
```
|
|
|
|
If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance):
|
|
|
|
```go
|
|
val := cfg.Section("").Key("key name").Value()
|
|
```
|
|
|
|
To check if raw value exists:
|
|
|
|
```go
|
|
yes := cfg.Section("").HasValue("test value")
|
|
```
|
|
|
|
To get value with types:
|
|
|
|
```go
|
|
// For boolean values:
|
|
// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
|
|
// false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
|
|
v, err = cfg.Section("").Key("BOOL").Bool()
|
|
v, err = cfg.Section("").Key("FLOAT64").Float64()
|
|
v, err = cfg.Section("").Key("INT").Int()
|
|
v, err = cfg.Section("").Key("INT64").Int64()
|
|
v, err = cfg.Section("").Key("UINT").Uint()
|
|
v, err = cfg.Section("").Key("UINT64").Uint64()
|
|
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
|
|
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
|
|
|
|
v = cfg.Section("").Key("BOOL").MustBool()
|
|
v = cfg.Section("").Key("FLOAT64").MustFloat64()
|
|
v = cfg.Section("").Key("INT").MustInt()
|
|
v = cfg.Section("").Key("INT64").MustInt64()
|
|
v = cfg.Section("").Key("UINT").MustUint()
|
|
v = cfg.Section("").Key("UINT64").MustUint64()
|
|
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
|
|
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
|
|
|
|
// Methods start with Must also accept one argument for default value
|
|
// when key not found or fail to parse value to given type.
|
|
// Except method MustString, which you have to pass a default value.
|
|
|
|
v = cfg.Section("").Key("String").MustString("default")
|
|
v = cfg.Section("").Key("BOOL").MustBool(true)
|
|
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
|
|
v = cfg.Section("").Key("INT").MustInt(10)
|
|
v = cfg.Section("").Key("INT64").MustInt64(99)
|
|
v = cfg.Section("").Key("UINT").MustUint(3)
|
|
v = cfg.Section("").Key("UINT64").MustUint64(6)
|
|
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
|
|
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
|
|
```
|
|
|
|
What if my value is three-line long?
|
|
|
|
```ini
|
|
[advance]
|
|
ADDRESS = """404 road,
|
|
NotFound, State, 5000
|
|
Earth"""
|
|
```
|
|
|
|
Not a problem!
|
|
|
|
```go
|
|
cfg.Section("advance").Key("ADDRESS").String()
|
|
|
|
/* --- start ---
|
|
404 road,
|
|
NotFound, State, 5000
|
|
Earth
|
|
------ end --- */
|
|
```
|
|
|
|
That's cool, how about continuation lines?
|
|
|
|
```ini
|
|
[advance]
|
|
two_lines = how about \
|
|
continuation lines?
|
|
lots_of_lines = 1 \
|
|
2 \
|
|
3 \
|
|
4
|
|
```
|
|
|
|
Piece of cake!
|
|
|
|
```go
|
|
cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
|
|
cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
|
|
```
|
|
|
|
Well, I hate continuation lines, how do I disable that?
|
|
|
|
```go
|
|
cfg, err := ini.LoadSources(ini.LoadOptions{
|
|
IgnoreContinuation: true,
|
|
}, "filename")
|
|
```
|
|
|
|
Holy crap!
|
|
|
|
Note that single quotes around values will be stripped:
|
|
|
|
```ini
|
|
foo = "some value" // foo: some value
|
|
bar = 'some value' // bar: some value
|
|
```
|
|
|
|
That's all? Hmm, no.
|
|
|
|
#### Helper methods of working with values
|
|
|
|
To get value with given candidates:
|
|
|
|
```go
|
|
v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
|
|
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
|
|
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
|
|
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
|
|
v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
|
|
v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
|
|
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
|
|
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
|
|
```
|
|
|
|
Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates.
|
|
|
|
To validate value in a given range:
|
|
|
|
```go
|
|
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
|
|
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
|
|
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
|
|
vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
|
|
vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
|
|
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
|
|
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
|
|
```
|
|
|
|
##### Auto-split values into a slice
|
|
|
|
To use zero value of type for invalid inputs:
|
|
|
|
```go
|
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
|
// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
|
|
vals = cfg.Section("").Key("STRINGS").Strings(",")
|
|
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
|
|
vals = cfg.Section("").Key("INTS").Ints(",")
|
|
vals = cfg.Section("").Key("INT64S").Int64s(",")
|
|
vals = cfg.Section("").Key("UINTS").Uints(",")
|
|
vals = cfg.Section("").Key("UINT64S").Uint64s(",")
|
|
vals = cfg.Section("").Key("TIMES").Times(",")
|
|
```
|
|
|
|
To exclude invalid values out of result slice:
|
|
|
|
```go
|
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
|
// Input: how, 2.2, are, you -> [2.2]
|
|
vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
|
|
vals = cfg.Section("").Key("INTS").ValidInts(",")
|
|
vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
|
|
vals = cfg.Section("").Key("UINTS").ValidUints(",")
|
|
vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
|
|
vals = cfg.Section("").Key("TIMES").ValidTimes(",")
|
|
```
|
|
|
|
Or to return nothing but error when have invalid inputs:
|
|
|
|
```go
|
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
|
// Input: how, 2.2, are, you -> error
|
|
vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
|
|
vals = cfg.Section("").Key("INTS").StrictInts(",")
|
|
vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
|
|
vals = cfg.Section("").Key("UINTS").StrictUints(",")
|
|
vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
|
|
vals = cfg.Section("").Key("TIMES").StrictTimes(",")
|
|
```
|
|
|
|
### Save your configuration
|
|
|
|
Finally, it's time to save your configuration to somewhere.
|
|
|
|
A typical way to save configuration is writing it to a file:
|
|
|
|
```go
|
|
// ...
|
|
err = cfg.SaveTo("my.ini")
|
|
err = cfg.SaveToIndent("my.ini", "\t")
|
|
```
|
|
|
|
Another way to save is writing to a `io.Writer` interface:
|
|
|
|
```go
|
|
// ...
|
|
cfg.WriteTo(writer)
|
|
cfg.WriteToIndent(writer, "\t")
|
|
```
|
|
|
|
By default, spaces are used to align "=" sign between key and values, to disable that:
|
|
|
|
```go
|
|
ini.PrettyFormat = false
|
|
```
|
|
|
|
## Advanced Usage
|
|
|
|
### Recursive Values
|
|
|
|
For all value of keys, there is a special syntax `%(<name>)s`, where `<name>` is the key name in same section or default section, and `%(<name>)s` will be replaced by corresponding value(empty string if key not found). You can use this syntax at most 99 level of recursions.
|
|
|
|
```ini
|
|
NAME = ini
|
|
|
|
[author]
|
|
NAME = Unknwon
|
|
GITHUB = https://github.com/%(NAME)s
|
|
|
|
[package]
|
|
FULL_NAME = github.com/go-ini/%(NAME)s
|
|
```
|
|
|
|
```go
|
|
cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon
|
|
cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini
|
|
```
|
|
|
|
### Parent-child Sections
|
|
|
|
You can use `.` in section name to indicate parent-child relationship between two or more sections. If the key not found in the child section, library will try again on its parent section until there is no parent section.
|
|
|
|
```ini
|
|
NAME = ini
|
|
VERSION = v1
|
|
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
|
|
|
|
[package]
|
|
CLONE_URL = https://%(IMPORT_PATH)s
|
|
|
|
[package.sub]
|
|
```
|
|
|
|
```go
|
|
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
|
|
```
|
|
|
|
#### Retrieve parent keys available to a child section
|
|
|
|
```go
|
|
cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
|
|
```
|
|
|
|
### Auto-increment Key Names
|
|
|
|
If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter.
|
|
|
|
```ini
|
|
[features]
|
|
-: Support read/write comments of keys and sections
|
|
-: Support auto-increment of key names
|
|
-: Support load multiple files to overwrite key values
|
|
```
|
|
|
|
```go
|
|
cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"}
|
|
```
|
|
|
|
### Map To Struct
|
|
|
|
Want more objective way to play with INI? Cool.
|
|
|
|
```ini
|
|
Name = Unknwon
|
|
age = 21
|
|
Male = true
|
|
Born = 1993-01-01T20:17:05Z
|
|
|
|
[Note]
|
|
Content = Hi is a good man!
|
|
Cities = HangZhou, Boston
|
|
```
|
|
|
|
```go
|
|
type Note struct {
|
|
Content string
|
|
Cities []string
|
|
}
|
|
|
|
type Person struct {
|
|
Name string
|
|
Age int `ini:"age"`
|
|
Male bool
|
|
Born time.Time
|
|
Note
|
|
Created time.Time `ini:"-"`
|
|
}
|
|
|
|
func main() {
|
|
cfg, err := ini.Load("path/to/ini")
|
|
// ...
|
|
p := new(Person)
|
|
err = cfg.MapTo(p)
|
|
// ...
|
|
|
|
// Things can be simpler.
|
|
err = ini.MapTo(p, "path/to/ini")
|
|
// ...
|
|
|
|
// Just map a section? Fine.
|
|
n := new(Note)
|
|
err = cfg.Section("Note").MapTo(n)
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Can I have default value for field? Absolutely.
|
|
|
|
Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type.
|
|
|
|
```go
|
|
// ...
|
|
p := &Person{
|
|
Name: "Joe",
|
|
}
|
|
// ...
|
|
```
|
|
|
|
It's really cool, but what's the point if you can't give me my file back from struct?
|
|
|
|
### Reflect From Struct
|
|
|
|
Why not?
|
|
|
|
```go
|
|
type Embeded struct {
|
|
Dates []time.Time `delim:"|"`
|
|
Places []string `ini:"places,omitempty"`
|
|
None []int `ini:",omitempty"`
|
|
}
|
|
|
|
type Author struct {
|
|
Name string `ini:"NAME"`
|
|
Male bool
|
|
Age int
|
|
GPA float64
|
|
NeverMind string `ini:"-"`
|
|
*Embeded
|
|
}
|
|
|
|
func main() {
|
|
a := &Author{"Unknwon", true, 21, 2.8, "",
|
|
&Embeded{
|
|
[]time.Time{time.Now(), time.Now()},
|
|
[]string{"HangZhou", "Boston"},
|
|
[]int{},
|
|
}}
|
|
cfg := ini.Empty()
|
|
err = ini.ReflectFrom(cfg, a)
|
|
// ...
|
|
}
|
|
```
|
|
|
|
So, what do I get?
|
|
|
|
```ini
|
|
NAME = Unknwon
|
|
Male = true
|
|
Age = 21
|
|
GPA = 2.8
|
|
|
|
[Embeded]
|
|
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
|
|
places = HangZhou,Boston
|
|
```
|
|
|
|
#### Name Mapper
|
|
|
|
To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name.
|
|
|
|
There are 2 built-in name mappers:
|
|
|
|
- `AllCapsUnderscore`: it converts to format `ALL_CAPS_UNDERSCORE` then match section or key.
|
|
- `TitleUnderscore`: it converts to format `title_underscore` then match section or key.
|
|
|
|
To use them:
|
|
|
|
```go
|
|
type Info struct {
|
|
PackageName string
|
|
}
|
|
|
|
func main() {
|
|
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
|
|
// ...
|
|
|
|
cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
|
|
// ...
|
|
info := new(Info)
|
|
cfg.NameMapper = ini.AllCapsUnderscore
|
|
err = cfg.MapTo(info)
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Same rules of name mapper apply to `ini.ReflectFromWithMapper` function.
|
|
|
|
#### Value Mapper
|
|
|
|
To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values:
|
|
|
|
```go
|
|
type Env struct {
|
|
Foo string `ini:"foo"`
|
|
}
|
|
|
|
func main() {
|
|
cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
|
|
cfg.ValueMapper = os.ExpandEnv
|
|
// ...
|
|
env := &Env{}
|
|
err = cfg.Section("env").MapTo(env)
|
|
}
|
|
```
|
|
|
|
This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`.
|
|
|
|
#### Other Notes On Map/Reflect
|
|
|
|
Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature:
|
|
|
|
```go
|
|
type Child struct {
|
|
Age string
|
|
}
|
|
|
|
type Parent struct {
|
|
Name string
|
|
Child
|
|
}
|
|
|
|
type Config struct {
|
|
City string
|
|
Parent
|
|
}
|
|
```
|
|
|
|
Example configuration:
|
|
|
|
```ini
|
|
City = Boston
|
|
|
|
[Parent]
|
|
Name = Unknwon
|
|
|
|
[Child]
|
|
Age = 21
|
|
```
|
|
|
|
What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome.
|
|
|
|
```go
|
|
type Child struct {
|
|
Age string
|
|
}
|
|
|
|
type Parent struct {
|
|
Name string
|
|
Child `ini:"Parent"`
|
|
}
|
|
|
|
type Config struct {
|
|
City string
|
|
Parent
|
|
}
|
|
```
|
|
|
|
Example configuration:
|
|
|
|
```ini
|
|
City = Boston
|
|
|
|
[Parent]
|
|
Name = Unknwon
|
|
Age = 21
|
|
```
|
|
|
|
## Getting Help
|
|
|
|
- [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
|
|
- [File An Issue](https://github.com/go-ini/ini/issues/new)
|
|
|
|
## FAQs
|
|
|
|
### What does `BlockMode` field do?
|
|
|
|
By default, library lets you read and write values so we need a locker to make sure your data is safe. But in cases that you are very sure about only reading data through the library, you can set `cfg.BlockMode = false` to speed up read operations about **50-70%** faster.
|
|
|
|
### Why another INI library?
|
|
|
|
Many people are using my another INI library [goconfig](https://github.com/Unknwon/goconfig), so the reason for this one is I would like to make more Go style code. Also when you set `cfg.BlockMode = false`, this one is about **10-30%** faster.
|
|
|
|
To make those changes I have to confirm API broken, so it's safer to keep it in another place and start using `gopkg.in` to version my package at this time.(PS: shorter import path)
|
|
|
|
## License
|
|
|
|
This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.
|