在有些场景下,想要解析yaml文件,但是环境受限只能使用shell脚本,不能使用python等高级语言解析。调研后,本文记录下解析shell脚本,以备后用。
shell解析函数如下,
function parse_yaml {
local prefix=$2
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -ne "s|^\($s\):|\1|" \
-e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 |
awk -F$fs '{
indent = length($1)/2;
vname[indent] = $2;
for (i in vname) {if (i > indent) {delete vname[i]}}
if (length($3) > 0) {
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
}
}'
}
脚本中的解析函数调用格式为:parse_yaml <path_to_conf.yaml> <prefix>
。调用后就会有prefix为前缀的配置变量。
例如,yaml文件内容如下:
#conf_path: ~/conf.yaml
## global definitions
global:
debug: yes
verbose: no
debugging:
detailed: no
header: "debugging started"
## output
output:
file: "yes"
执行解析配置文件
eval $(parse_yaml "~/conf.yaml" "conf_")
输出解析后的一些配置值
echo $conf_global_debug #output: yes
echo $conf_global_debugging_header #output: debugging started
echo $conf_output_file #output: yes
如上,我们就解析出来对应的配置了~
熟悉slice底层实现的同学都知道,slice底层结构实包含的是指针、长度和容量,指针指向底层的数据数组。当我们对切片进行截取或追加时,如果不注意截取和追加的基本规则,可能会有意想不到的结果。下面先通过两个例子来看看问题,然后我们再具体分析。
例1 (运行)
s := []int{1, 2, 3, 4, 5}
a := s[2:3] // diff
a = append(a, 1)
fmt.Println(s, a)
例2 (运行)
s := []int{1, 2, 3, 4, 5}
a := s[2:3:3] //diff
a = append(a, 1)
fmt.Println(s, a)
这两个例子输出是什么呢? 会一样吗? 我们可以先思考下,尝试自己执行看看和理解的是否一致。
在看答案前,我们先巩固下slice截取和追加的一些基本原则。
首先slice的截取,slice一般的截取格式是 a := s[low:high:max]
,主要包含以下几点:
1)low表示截取开始的index,包含该index;
2)high表示截取结束的index,不包含index;
3)max表示截取后的容量的index,不包含index;
4)不设置max则max为被截取slice的cap;
因此截取后的a
,起始index为low,len为high-low,cap为max-low。
而追加的规则就比较简单了,slice追加 a = append(a, ele)
,主要有两个原则:
1)slice有剩余空间即cap-len>0,在原slice上追加;
2)slice无剩余空间,则重新分配底层数组空间,扩容copy后,再进行追加;
有了上面的基本原则,我们就能分析出来上面的结果了。
在例1中,a的起始index为2,len为3-2=1,cap则为5-2=3。因此append时,a中还有容量,在原slice上追加,此时也会改变s的内容。因此输出为:
//example 1 output
[1 2 3 1 5] [3 1]
而例2,a的起始index同样为2,len也为3-2=1,但是cap为3-2=1。因此append时,a中没有容量,需要在重新分配底层数据空间,再追加。因此输出为:
//example 2 output
[1 2 3 4 5] [3 1]
因此,由于存在多个slice指向同一个底层数据数组的情况,append的时候就需要根据当前slice的cap决定是否影响底层数据数组的内容。
对于slice了解较少的,可以复习下go官网的blog slices-intro。同时,也可以参考前面文章golang基础-01-array、slice和map
分析下slice截取追加前后底层指针及容量的变化情况。
在开发过程中,经常通过map实现kv数据的处理。源码src/runtime/map.go
中可以看到map的底层结构,网上也有很多对map的分析,此处不再赘述。本文先综述map的结构,然后根据具体的一个map变量分析map底层是怎么存储的。
map的实现是通过hash来实现,底层是一个hmap和若干个bmap组成,基本结构见上图。
总结下来就是以下几点:
1. map变量是一个指针,指向hmap结构体;
2. hmap中包含map中元素的个数count,桶的个数(2^B),桶数组的地址`buckets`等,
3. hmap的元素`buckets`指向bmap的数组,根据hash确定key落入哪个bmap中;
4. hmap的元素`oldbuckets`指向扩容阶段之前的bmap的数组;
5. bmap中tophash用来快速定位key是否在这个bmap中;
6. bmap中8个kv槽位用来存储key和value;
7. bmap中overflow用来拉链指向新的桶,解决添加超过key时,超过8个槽位的情况;
上文分析了,单个基础变量的内存存储结构,下面我们分析下基础变量组合形式的变量。
对int32
的数组进行分析,运行一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
var arrVar = [...]int32{
'b', 2, 3, 4,
}
fmt.Printf("arrVar 值: %d\n", arrVar)
fmt.Printf("arrVar 内存地址: %p\n", &arrVar)
fmt.Printf("arrVar 内存中占总字节数:%d\n", unsafe.Sizeof(arrVar))
fmt.Printf("arrVar 元素个数:%d\n", len(arrVar))
//第一个元素
ptr1 := (*int32)(unsafe.Pointer(&arrVar))
fmt.Printf("arrVar 第一个元素,地址=%x,值: %d\n", ptr1, *ptr1)
//第二个元素
ptr2 := (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(&arrVar)) + uintptr(4*1)))
fmt.Printf("arrVar 第二个元素,地址=%x,值: %d\n", ptr2, *ptr2)
//第三个元素
ptr3 := (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(&arrVar)) + uintptr(4*2)))
fmt.Printf("arrVar 第三个元素,地址=%x,值: %d\n", ptr3, *ptr3)
//第四个元素
ptr4 := (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(&arrVar)) + uintptr(4*3)))
fmt.Printf("arrVar 第四个元素,地址=%x,值: %d\n", ptr4, *ptr4)
}
输出结果如下:
arrVar 值: [98 2 3 4]
arrVar 内存地址: 0xc000016050
arrVar 内存中占总字节数:16
arrVar 元素个数:4
arrVar 第一个元素,地址=c000016050,值: 98
arrVar 第二个元素,地址=c000016054,值: 2
arrVar 第三个元素,地址=c000016058,值: 3
arrVar 第四个元素,地址=c00001605c,值: 4
结果可以看出,数组的元素类型是int32
,每个占用4个字节,一共4个元素,因此数组总共占用16字节。数组的首地址是c000016050
对应数组第一个元素,最后一个元素的首地址是c00001605c
,可以得出数组元素连续分配,并且地址从低到高排布。
仍然使用int32
类型,对切片进行分析,运行一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
var arrVar = []int32{
'b', 2, 3, 4,
}
fmt.Printf("arrVar 值: %d\n", arrVar)
fmt.Printf("arrVar 内存地址: %p\n", &arrVar)
fmt.Printf("arrVar 内存中占总字节数:%d\n", unsafe.Sizeof(arrVar))
fmt.Printf("arrVar 元素个数:%d\n", len(arrVar))
}
输出结果如下:
arrVar 值: [98 2 3 4]
arrVar 内存地址: 0xc00009c020
arrVar 内存中占总字节数:24
arrVar 元素个数:4
可以看出,切片4个元素,但是切片占用的总字节数为24字节。可以自行试下,无论切片多少元素,slice变量本身占用的字节数不变,都是24字节。
分析golang源码(go1.11)在文件src/runtime/slice.go
中可以看到如下结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
说明slice实际上是由一个指针和两个int变量组成,指针和int都是占8个字节,也就对应了上面的slice总共占用24字节。继续用slice结构体
对切片进行分析,运行一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
var sliceVar = []int32{
'b', 2, 3, 4,
}
fmt.Printf("sliceVar 值: %d\n", sliceVar)
type sliceStruct struct {
array unsafe.Pointer
len int
cap int
}
//将切片转换为sliceStruct结构
ptr := (*sliceStruct)(unsafe.Pointer(&sliceVar))
fmt.Printf("sliceVar 地址=%p,值: %#v\n", &sliceVar, *ptr)
//取出sliceStruct中指针,再找指针值对应地址内存的值
arrPtr := (*[4]int32)(ptr.array)
fmt.Printf("sliceVar 中指针指向的值: %#v\n", *arrPtr)
}
执行结果如下:
sliceVar 值: [98 2 3 4]
sliceVar 地址=0xc00000c060,值: main.sliceStruct{array:(unsafe.Pointer)(0xc000016050), len:4, cap:4}
sliceVar 中指针指向的值: [4]int32{98, 2, 3, 4}
切片对应的结构体中,元素array
指向的是切片元素存储的数组地址,元素len
和cap
分别对应切片的长度和容量。也对应之前知道的知识,切片底层是数组,一个数组可以被多个切片引用,感兴趣的可以自己试验下。
字符串的结构类似切片,不过字符串是不可被改变的,也就只有指针和长度。运行一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
var stringVar string = "hi"
fmt.Printf("sliceVar 值: %s\n", stringVar)
fmt.Printf("sliceVar 内存中占总字节数:%d\n", unsafe.Sizeof(stringVar))
type stringStruct struct {
array unsafe.Pointer
len int
}
//将切片转换为sliceStruct结构
ptr := (*stringStruct)(unsafe.Pointer(&stringVar))
fmt.Printf("sliceVar 地址=%p,值: %#v\n", &stringVar, *ptr)
//取出sliceStruct中指针,再找指针值对应地址内存的值
arrPtr := (*[5]byte)(ptr.array)
fmt.Printf("sliceVar 中指针指向的值: %c, %c\n", (*arrPtr)[0], (*arrPtr)[1])
}
执行结果如下:
sliceVar 值: hi
sliceVar 内存中占总字节数:16
sliceVar 地址=0xc000010210,值: main.stringStruct{array:(unsafe.Pointer)(0x4c1b71), len:2}
sliceVar 中指针指向的值: h, i
字符串包含指针和int两个元素,固定占用16个字节。其中指针指向一个byte的数组,int元素代表的是字符串的长度。
从以上可以看出,数组是一段连续的内存空间。而切片和字符串本身是一个指针,指针再指向具体的内容的数组。
在内存中,基础变量是若干个连续地址组成的内存空间。指针指向低地址,向高地址延展。并且在计算机的内存中通常使用小端序存储,与网络流中的大端序不同的是,小端序内存低地址空间存储变量低位,内存的高地址空间存储变量的高位。
使用unsafe
包中的工具,可以对内存进行操作。下面通过int32
,进行分析基础变量在内存中怎么存储,运行一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
var i32Var int32 = 0x12345678
fmt.Printf("i32Var 值: %x\n", i32Var)
fmt.Printf("i32Var 内存地址: %p\n", &i32Var)
fmt.Printf("i32Var 内存中占总字节数:%d\n", unsafe.Sizeof(i32Var))
//第一个字节
ptr1 := (*byte)(unsafe.Pointer(&i32Var))
fmt.Printf("i32Var 第一个字节,地址=%x,值: %x\n", ptr1, *ptr1)
//第二个字节
ptr2 := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&i32Var)) + uintptr(1)))
fmt.Printf("i32Var 第二个字节,地址=%x,值: %x\n", ptr2, *ptr2)
//第三个字节
ptr3 := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&i32Var)) + uintptr(2)))
fmt.Printf("i32Var 第三个字节,地址=%x,值: %x\n", ptr3, *ptr3)
//第四个字节
ptr4 := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&i32Var)) + uintptr(3)))
fmt.Printf("i32Var 第四个字节,地址=%x,值: %x\n", ptr4, *ptr4)
}
运行输出结果如下:
i32Var 值: 12345678
i32Var 内存地址: 0xc000016050
i32Var 内存中占总字节数:4
i32Var 第一个字节,地址=c000016050,值: 78
i32Var 第二个字节,地址=c000016051,值: 56
i32Var 第三个字节,地址=c000016052,值: 34
i32Var 第四个字节,地址=c000016053,值: 12
由此看出,int32
的变量i32Var占4个字节的内存。变量的首地址是c000016050
,最后一个字节地址是c000016053
,也就是说变量内部的地址空间从低到高排布。第一个字节存储的值是0x78
,对应整个变量值0x12345678
,说明首字节存储变量的低位值,即小端序。感兴趣的话,可以自行分析其他基础类型~
针对http客户端,本文提供两个基础方法HttpGet
和HttpPost
。其中日志模块可以根据自己需要做更改。
client的返回值,是response的byte数组,根据实际情况做解析处理即可。
HttpGet运行实例,运行一下。
func main() {
data := map[string]string {
"key": "value",
}
resp, err := HttpGet("http://httpbin.org/get", data)
if err != nil {
return
}
type RespStruct struct {
Args map[string]string `json:"args"`
Url string `json:"url"`
}
var rs RespStruct
err = json.Unmarshal(resp, &rs)
if err != nil {
return
}
fmt.Println("===rs.url==: ", rs.Url)
}
输出结果
===rs.url==: http://httpbin.org/get?key=value
HttpPost运行实例,运行一下。
func main() {
data := map[string]string {
"key": "value",
}
resp, err := HttpPost("http://httpbin.org/post", data)
if err != nil {
return
}
type RespStruct struct {
Form map[string]string `json:"form"`
Url string `json:"url"`
}
var rs RespStruct
err = json.Unmarshal(resp, &rs)
if err != nil {
return
}
fmt.Println("===rs.form==: ", rs.Form)
}
输出结果
===rs.form==: map[key:value]
package helper
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
)
//change your log
var logInfo = log.Println
var logFatal = log.Fatal
func HttpGet(requestUrl string, data map[string]string) (response []byte, err error) {
client := &http.Client{
Timeout: time.Duration(3 * time.Second),
}
req, err := request("GET", requestUrl, data)
if err != nil {
logFatal(fmt.Sprintf("http req err, err: %s", err.Error()))
return response, err
}
resp, err := client.Do(req)
if err != nil {
logFatal(fmt.Sprintf("http request do err, err: %s", err.Error()))
return response, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logFatal(fmt.Sprintf("http request read io err, err: %s", err.Error()))
return response, err
}
logInfo(requestUrl, data, string(body))
return body, err
}
func HttpPost(requestUrl string, data map[string]string) (response []byte, err error) {
client := &http.Client{
Timeout: time.Duration(3 * time.Second),
}
req, err := request("POST", requestUrl, data)
if err != nil {
logFatal(fmt.Sprintf("http req err, err: %s", err.Error()))
return response, err
}
resp, err := client.Do(req)
if err != nil {
logFatal(fmt.Sprintf("http request do err, err: %s", err.Error()))
return response, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logFatal(fmt.Sprintf("http request read io err, err: %s", err.Error()))
return response, err
}
logInfo(requestUrl, data, string(body))
return body, err
}
func request(method string, requestUrl string, data map[string]string) (req *http.Request, err error) {
v := url.Values{}
if len(data) > 0 {
for key, value := range data {
v.Add(key, value)
}
}
if method == "POST" {
req, err = http.NewRequest(method, requestUrl, strings.NewReader(v.Encode()))
} else {
req, err = http.NewRequest(method, requestUrl+"?"+v.Encode(), nil)
}
if err != nil {
logFatal(fmt.Sprintf("new request err, err: %s", err.Error()))
return nil, err
}
if method == "POST" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
return req, nil
}