19年上半年Go语言使用笔记

GO语言笔记

Posted by UlyC on June 6, 2019

语言须能精致准确表达出想要表达的对象。

举例来说,好小说家不该滥用“不可思议”“难以捉摸”等词,因为他有责任把这种“不可思议”与“难以捉摸”写清楚,传达给读者。 ​ —— F.R.利维斯 《伟大的传统》

19年上半年Go语言使用笔记

公司里新项目基本都换成了Go语言,,老项目有条件的也在慢慢用Go重构。

所以这大半年除了偶尔维护Python2老代码的时候还写下Python, 其他大部分时间基本都在写JS和Go。:stuck_out_tongue:

下面是 半年来使用中记的一些记录,整理了下发出来。

1. 发送Get以外的额请求

以 PATCH方法为例:

package main  

import (  
   "bytes"
   "compress/gzip"
   "encoding/json"
   "fmt"
   "io/ioutil"
   "net/http"
)

func main() {
   region := "js"
   gwIp := "1.1.1.1"

   stateChangeBody := `{"region":"` + region+`","gw_ip":"`+ gwIp+`"}`
   var stateChangeBytes = []byte(stateChangeBody) 
    //stateChangeBody 若使用结构体 , stateChangeBytes就用 json.Marshal(stateChangeBody)

   fmt.Println("body:",bytes.NewBuffer(stateChangeBytes))

   stateChangeReq, _:= http.NewRequest("PATCH", "http://localhost:8000/api/gateway/state",  bytes.NewBuffer(stateChangeBytes))
    
    //请求的body是 io.reader interface,需要实现
    //strings.NewReader  byte.NewReader bytes.NewBuffer  实现了read 方法
    

   client := &http.Client{}
   stateChangeResp, _ := client.Do(stateChangeReq)
	
    // 若响应使用gzip 编码 ,需要使用gzip.NewReader 解压, 
	  //reader, err := gzip.NewReader(stateChangeResp.Body)
	  //if err != nil {
	  //	return nil, fmt.Errorf("GZIP Read ERROR %s", err.Error())
	  //}
	  //Response, err := ioutil.ReadAll(reader)
	  //if err != nil {
	  //	return nil, fmt.Errorf("BODY READ ERR %s", err.Error())
	  //}
    
   stateChangeResponse, _ := ioutil.ReadAll(stateChangeResp.Body)
   //fmt.Println("response Body:", string(stateChangeRespData))

   var stateChangeRespData map[string]interface{}

   if err := json.Unmarshal(stateChangeResponse, &stateChangeRespData); err != nil {
      fmt.Println("response err:", err)
   }

   if _, ok := stateChangeRespData["res"]; !ok {
      fmt.Println("response err:","无数据")
   }

   if stateChangeRespData["res"]=="faild" {
      fmt.Println("result:","更新失败")
   }
}

2. invalid character ‘<’ looking for beginning of value

使用json.Unmarshal()反序列化时出现。

func main()  {  
   timeStamp := time.Now().Format("2006/01/02 15:04")
   metric := "probe_duration_seconds"
   ip := "1.1.1.1"
   probeIpApi :=  "http://api/ip?start_time=" + timeStamp + "&end_time="+ timeStamp + "&metric="+ metric + "&ip=" + ip  
    
   probeIpResp, err := http.Get(probeIpApi)  
   if err != nil {
      fmt.Println("请求API失败", err)  
      return
   }
    
   defer probeIpResp.Body.Close()  
   probeIpRespBody,err := ioutil.ReadAll(probeIpResp.Body)  
   if err != nil {
      fmt.Println("请求API io读取失败", err)
      return
   }
  
   fmt.Println(string(probeIpRespBody))  

   var probeIpRespDat =make(map[string]interface{},0)  
   if err := json.Unmarshal(probeIpRespBody, &probeIpRespDat); err != nil{ 
      fmt.Println("请求API 反序列化失败", err)
      return
   }
   ipConnectedness := probeIpRespDat
   fmt.Println(ipConnectedness)
}

原因

打印出来返回的结果是

<html>
<head><title>502 Bad Gateway</title></head>
</html>

返回的为html时会报这个错误。

而未返回结果的原因是,url中的time含有空格导致。

解决

确保返回的数据正确就可解决。

还有确保请求时的url正确编码有两种方法。

  1. 将url拼完后进行一次编码:
    probeIpApi :=  "http://api/ip?start_time=" + timeStamp + "&end_time="+ timeStamp + "&metric="+ metric + "&ip=" + ip  
       
    u,_ := url.Parse(probeIpApi)  
    q := u.Query()
    u.RawQuery = q.Encode()    //urlencode
    fmt.Println(u.String())
    probeIpResp, err := http.Get(u.String())
    
  2. 将ulr中参数通过URL.Query()添加:
    req, err := http.NewRequest("GET", "http://api/ip", nil)   
    if err != nil {
            log.Print(err)
            os.Exit(1) 
        }  
       
    q := req.URL.Query()  
    q.Add("start_time", start_time)
    q.Add("end_time", end_time)
    q.Add("metric", metric)
    q.Add("ip", ip)
       
    req.URL.RawQuery = q.Encode()
    client := &http.Client{}
    resp, err = client.Do(req)
    

注意:此种方法会将 空格 编码为“+”而不是“%20”。

W3C标准规定,当Content-Type为application/x-www-form-urlencoded时,URL中查询参数名和参数值中空格要用加号 “+” 替代,所以几乎所有使用该规范的浏览器在表单提交后,URL查询参数中空格都会被编成加号”+”。

而在另一份规范(RFC 2396,定义URI)里, URI里的保留字符都需转义成%HH格式(Section 3.4 Query Component),因此空格会被编码成”%20”,加号”+”本身也作为保留字而被编成”%2B”。

对于某些遵循RFC 2396标准的应用来说,它可能不接受查询字符串中出现加号”+”,认为它是非法字符。所以一个安全的举措是URL中统一使用”%20”来编码空格字符。

上面的例子中可以在最后,再调用replaceAll("\\+", "%20"),将所有加号+替换为%20。

3.Get Resource List Err: CLIENT DO ERROR Post : unsupported protocol scheme

request 发送请求时, 目标url不合法.

两次遇到这个问题,由于配置中 本地测试时 高危的url注释未打开.

4.结构体的坑之大小写

结构体名字没有首字母大写别的包无法调用

struct的属性是否被导出,遵循大小写的原则:首字母大写的被导出,首字母小写的不被导出。

所以:

  1. 如果struct名称首字母是小写的,这个struct不会被导出。连同它里面的字段也不会导出,即使有首字母大写的字段名
  2. 如果struct名称首字母大写,则struct会被导出,但只会导出它内部首字母大写的字段,那些小写首字母的字段不会被导出

但是这里有一个特殊情况(这个限制不仅仅是结构体),当你有另一个首字母大写的结构体Test,包含了一个首字母小写的结构体test类型的参数,那么就够在包外通过调用这个首字母大写的结构体,间接的调用这个首字母小写的结构体的内容(前提必须是参数首字母大写)

var hi test = test{"hello"}  

type test struct {
   Test string
}

type Test struct {
   Test1 test
}

func (m * Test) InitHi(){
   m.Test1=hi
}

结构体中参数首字母没有大写时,别的包虽然可以调用这个结构体,但是找不到这个结构体中没有首字母大写的参数。

5.float类型精确度(保留小数位数)

Go没有python那样现成的内置函数, 比如round(number , n)或者Decimal包, 只能自己封装啦:

  • 转换成字符串,截取,再转为float

func FloatRound(f float64, n int) float64 {  
   format := "%." + strconv.Itoa(n) + "f"
   res, _ := strconv.ParseFloat(fmt.Sprintf(format, f), 64)
   return res
}
  • 用math包

func Round(f float64, n int) float64 {  
	n10 := math.Pow10(n)
	return math.Trunc((f+0.5/n10)*n10) / n10  
    
    //Trunc保留整数部分
    //比如1.5334要保留一位的话, (1.5334*10)保留整数位,再除以 10 就可以了,加0.5是为了四舍五入.
}

6. Go中时间戳相关

import (
	"strconv"
	"time"
)  

func main() {
	timwStamp := time.Now()  								
	//timwStamp: 2019-05-13 17:19:37.3712671 +0800 CST m=+0.008498901 ,  
	//type: time.Time
    
	timeStampUnix := time.Now().Unix()  					
       //timwStamp--Unix:  1557739177
       //type  :  int64

	timeString := strconv.FormatInt(timeStamp,10)  			
    	//timeString:  1557739177
   	//type: string

	timeNow := time.Unix(timeStamp, 0)  					
    	//timwNow--Unix :  2019-05-13 17:19:37 +0800 CST
   	//type: time.Time
    
	lastTime := timeNow.Format("2006-01-02 15:04:05")
	//lastTime:   2019-05-13 17:19:37
   	 //type: time.Time
    
	timeParse,_ := time.Parse("2006-01-02 15:04:05",timeString)  
    	//timeParse:   0001-01-01 00:00:00 +0000 UTC
    	// type: time.Time
}

7.Go语音中单引号和双引号区别,及rune类型

Go语言的字符串类型string在本质上就与其他语言的字符串类型不同:

  • Java的String、C++的std::string以及Python3的str类型都只是定宽字符序列
  • Go语言的字符串是一个用UTF-8编码的变宽字符序列,它的每一个字符都用一个或多个字节表示

即:一个Go语言字符串是一个任意字节的常量序列

Golang的双引号和反引号都可用于表示一个常量字符串,不同在于:

  • 双引号用来创建可解析的字符串字面量(支持转义,但不能用来引用多行)
  • 反引号用来创建原生的字符串字面量,这些字符串可能由多行组成(不支持任何转义序列),原生的字符串字面量多用于书写多行消息、HTML以及正则表达式
  • 单引号则用于表示Golang的一个特殊类型:rune,类似其他语言的byte但又不完全一样,是指:码点字面量(Unicode code point),不做任何转义的原始内容。

大部分能查到的资料就到此为止,没有解释码点字面量到底是什么.

官方文档解释:

rune is an alias for int32 and is equivalent to int32 in all ways.

//int32的别名,几乎在所有方面等同于int32

It is used, by convention, to distinguish character values from integer values.

//它用来区分字符值和整数值

在Go中 ,byte 是 ` uint8 `的别名,常用来处理ascii字符;

rune的命名应该来自于北欧的『如尼文字』,是int32的别名,表示一个Unicode码点.

被大部分人转载的所谓”码点字面量”其实是翻译的锅.

有关rune类型,相信看完下面的例子就会明白:

	     var a byte = 'a'  
	     // value:  97			即ASCII 编码为 97
	     // type:   uint8
	     // length:   binary.Size(a) == 1			
	     //			 unsafe.Sizeof(a) == 1
        
	     var c rune  = '你'        // 注意: 此处只能用 『单引号』
	     // value:  20320		即Unicode 码为 20320
	     // type:   int32/rune
	     // length:  binary.Size(c) == 4        
	     //   	    unsafe.Sizeof(c) == 4
        
	     
	     b := 'b'
	     // value:  98
	     // type:   int32/rune
	     // length:  binary.Size(b) == 11
	     
	     d := '我'
	     // value:  25105
	     // type:   int32/rune
	     // length: binary.Size(d) == 4
      
	     e := "e"
	     // value:  e
	     // type:   string
	     // length:  len(e)  == 1
        
	     f := "他"
	     // value:  他
	     // type:   string
	     // length:   len(f)  == 3
	     // len([]rune(f))  == 1
	     // utf8.RuneCountInString(f) == 1

可以看到,rune 类型就像文档中说的那样, 主要用于『字符值』而不是『整数值』.

在这个例子中, len(f) == 3是因为Golang中string底层是通过[]byte实现的。

中文字符在unicode下占2个字节,在utf-8编码下占3个字节 (编码问题可参见之前文章 字符编码浅谈及Python中的编码问题 ),而Golang默认编码正好是utf-8

ps:

int, uintuintptr 在 32 位系统上通常为 32 位宽( 4字节 ),在 64 位系统上则为 64 位宽( 8字节)。

    var z int = 8
    // unsafe.Sizeof(z) == 8     我的是64位系统
    // unsafe.Sizeof总是在编译期就进行求值,而不是在运行时

8 . slice [],被jsoniter.Marshal()转换为null,期望保留为[]空slice

type A stuct{
    Value  []string
}

func main() {
    data := &A{}
    data2 := &A{Value: []string{}}
    
    buf, _ := json.Marshal(&data)
    fmt.Println(string(buf))
      
    buf2, _ := json.Marshal(&data2)
    fmt.Println(string(buf2))
}

输出:

  {"Value":null}   
  {"Value":[]} 

9. 利用Map去重

Go没有python中set这种数据结构.

不过当有用到类似set的需求时, 可以直接make 一个map, 将item 设置为key.

value 可以设置为布尔值或者其他 , 这样就可以不用每次都去遍历数组,就可以新添加的item是否与之前的重复.

10. 组合和继承

刚开始用Go的时候,由于它不支持完全的面向对象,是怎么用怎么 难受…

Go中没有继承的概念, 它是通过接口来实现类似功能。

Go中还有一种叫做组合的概念,看如下代码

package main
  
import  "fmt"

type Base struct {
	// nothing
}

func (b *Base) ShowA() {
	fmt.Println("showA")
	b.ShowB()
}
func (b *Base) ShowB() {
	fmt.Println("showB")
}

type Derived struct {
	Base
}

func (d *Derived) ShowB() {
	fmt.Println("Derived showB")
}

func main() {
	d := Derived{}
	d.ShowA()
}

输出结果为:

showA
showB

可以看到,结果并没有输出 "Derived showB"

上面的Derived包含了Base,自动的含有了Base的方法,因为其不是继承,所以不会根据具体传入的对象而执行对应的方法。

Python中的类继承表示Base是Derived的一个父类,具有一种层次关系。

但是Go的组合则是一种 包含关系,是Derived 包含了Base,所以Derived的实例可以使用Base的所有方法。


知识共享许可协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。