21天速成go-第六天

71 阅读15分钟

14 函数

一个函数多种传参方法和调用方法

变长参数

func average(st ...float64) (avg float64) {
   avg = 0
   for _, val := range st {
      avg += val
   }
   avg = avg / float64(len(st))
   return
}

func main() {
   //nums := []float64{1, 2, 3, 4, 5, 6}
   //num
   fmt.Println(average(1, 2, 3, 4, 5))
}

记住这样传参的话,就不能是 nums 数组形式了,要是 nums 一个个数字写进去 还有就是 求平均的时候,要 float64(强制转换下st的int类型)

或者使用

fmt.Println(average(nums...))

这样也可以,后面加上...就类似Python的*用法拆分

如果还是定义是

func average(st []float64) (avg float64) {

那么就不需要...了直接传数组即可

返回返回值是一个函数的写法

func makeGreeter() func() string {
   return func() string {
      return "Hello world!"
   }
}

func main() {
   greet := makeGreeter()
   fmt.Println(greet())
   fmt.Printf("%T\n", greet)
}

返回值是一个函数的话,写法就是 func() string了,这个也是类似命名返回值的写法

闭包

闭包记住一个核心原则,如果闭包捕获到了一个外部变量,那么闭包对这个外部变量的修改,都是对原始值的修改

func wrapper() func() int {
   var x int
   return func() int {
      x++
      return x
   }
}

func main() {
   increment := wrapper()
   fmt.Println(increment())
   fmt.Println(increment())
}

对于这样一段代码,一开始的warpper() 就生成了x的值缓存了,执行到了return的部分,后面对x的增加就会一直有效

比如这样一个过滤器


func myFilter(nums []int, callable func(int) bool) (result []int) {
   for _, val := range nums {
      if callable(val) {
         result = append(result, val)
      }
   }
   return
}

func main() {
   result := myFilter([]int{1, 2, 3, 4}, func(x int) bool {
      return x > 1
   })
   fmt.Println(result)
}

注意几个点,append 左侧一定要有个值来接收,否则的话,会报错 result没有使用 另外一个例子是

func myPrint(nums []int, callable func(int)) {
   for _, val := range nums {
      callable(val)
   }
}

func main() {
   myPrint([]int{1, 2, 3, 4}, func(x int) {
      fmt.Println(x)
   })
}

注意如果函数作为参数的话,写的是函数的签名,比如

func myFilter(nums []int, callable func(int) bool) (result []int) {

或者 func myPrint(nums []int, callable func(int)) {

要记住写的是 callable func(int) 要把参数也写上去

对于string来说,如果转成bytes来看的话,就是一个4位的bytes

func main() {
   var x [256]string

   fmt.Println(len(x))
   fmt.Println(x[0])
   for i := 0; i < 256; i++ {
      x[i] = string(i)
   }
   for _, v := range x {
      fmt.Printf("%v - %T - %v\n", v, v, []byte(v))
   }
}

所以这段代码会输出的是

7 - string - [55]
8 - string - [56]
9 - string - [57]

是一位的bytes

o - string - [111]
p - string - [112]
q - string - [113]
r - string - [114]
s - string - [115]
t - string - [116]
u - string - [117]
v - string - [118]

也是一位的, 对于Unicode或者utf-8字符就是4位的

í - string - [195 173]
î - string - [195 174]
ï - string - [195 175]
ð - string - [195 176]
ñ - string - [195 177]
ò - string - [195 178]

18 切片

切片要扩充大小,只能使用append 如果要index赋值的话,就相当于固定大小的数组

func main() {

   greeting := make([]string, 3, 5)
   // 3 is length - number of elements referred to by the slice
   // 5 is capacity - number of elements in the underlying array

   greeting[0] = "Good morning!"
   greeting[1] = "Bonjour!"
   greeting[2] = "buenos dias!"
   greeting[3] = "suprabadham"
   //greeting = append(greeting, "suprabadham")
   fmt.Println(greeting[2])
}

比如这样,如果greeting[3] 就会报错indexerror 要使用注释掉的append

另外cap 如果扩充的话,每次就是扩容一个cap的大小

比如 greeting := make([]string, 3, 5) 如果产生了一次扩充,即便只新增了4个,也是扩容到了10个的大小

另外注意append每次只能添加一个值,如果要添加多个,要加上...进行拆分,比如

func main() {

   mySlice := []string{"Monday", "Tuesday"}
   myOtherSlice := []string{"Wednesday", "Thursday", "Friday"}

   mySlice = append(mySlice, myOtherSlice...)

   fmt.Println(mySlice)
}

append 的时候进行切片也是可以的

func main() {

   mySlice := []string{"Monday", "Tuesday"}
   myOtherSlice := []string{"Wednesday", "Thursday", "Friday"}

   mySlice = append(mySlice, myOtherSlice...)
   fmt.Println(mySlice)

   mySlice = append(mySlice[:2], mySlice[3:]...)
   fmt.Println(mySlice)

}

对切片进行var 或者 := 声明的时候,不会创建,返回的也是nil 要添加只能append,但是make的话,就会创建,然后分配大小

func main() {
   student := make([]string, 35)
   students := make([][]string, 35)
   student[0] = "Todd"
   // student = append(student, "Todd")
   fmt.Println(student)
   fmt.Println(students)
}

如果 var 或者 := 创建的,那就没法使用student[0] = "Todd", 用make 创建了,就会真的给空间,然后分配默认值

19 map

map的声明和上面说的切片同理

func main() {

   var myGreeting map[string]string
   fmt.Println(myGreeting)
   fmt.Println(myGreeting == nil)
}

var 创建的不会分配空间,也不能使用,返回的也是nil

可以写成

var myGreeting = make(map[string]string)

也可以写成

myGreeting := make(map[string]string)

区别就是 var 和 := 的区别

字典声明也可以使用{}来初始化,而不是一个一个的给值

myGreeting := map[string]string{
   "Tim":   "Good morning!",
   "Jenny": "Bonjour!",
}

就不需要

myGreeting["Tim"] = "Good morning."
myGreeting["Jenny"] = "Bonjour."

另外字典是无序的

func main() {

   myGreeting := map[string]string{
      "Tim":   "Good morning!",
      "Jenny": "Bonjour!",
   }

   myGreeting["Harleen"] = "Howdy"

   fmt.Println(myGreeting)
}

返回的是

map[Harleen:Howdy Jenny:Bonjour! Tim:Good morning!]

不是按照添加顺序来的

字典的删除可以使用delete,

delete(myGreeting, "two")

append 需要左侧有接收的值,但是delete不需要,delete的是key

要注意的是,如果删除一个不存在的key 那么也哈斯不会报错的,delete(myGreeting, 7) 7不存在,照样不会报错

同理如果取到了一个字典中不存在的值,也不会报错,只是会给2个返回值,第二个返回值是false

val, err := myGreeting[7]
fmt.Printf("val: %v, err: %v", val, err)

返回:
val: , err: false

所以还需要判断一下第二个值是不是nil或者true来判断字典中的取值是否存在

rune 的用法

rune 是输出Unicode的字符集的编码,也会是一个在Unicode 字符集里面的第几个

Unicode 是一个字符集,而utf-8是一种编码,这两个是不同的概念,Unicode 字符可以用不同的格式进行编码,比如 utf-8 utf-16 utf-32

utf-8 是一种采用 1~4个字节来表示一个Unicode 字符,他一个重要特性就是,能向后兼容Unicode,即ASCII 的字符在Unicode中也是一个字符来表示,且字节值与ASCII相同。 utf-8的这种特性使得他非常适合传输和存储文本数据,因为他可以无缝处理ASCII 编码,并且对于包含大量ASCII字符的文本来说,使用utf-8编码会非常节省空间

在go中,string类型是用来表示文本的,go 语言string类型内部用utf-8编码来存储Unicode字符,对比区别请看如下代码

s := "H你"
fmt.Println([]rune(s))
fmt.Println([]byte(s))

这段代码输出的是
[72 20320]
[72 228 189 160]

可以看出,对于ASCII就是单个字节,对于中文就是三个字节的变长存储

那么utf-8是怎么知道,后面三个要连起来看,而不是分开或者2个2个一起看呢

[72 228 189 160] 这个go 是怎么知道,前面一个是单独的,后面三个是要合并起来的呢

这就涉及到了ascii 的编码规则了

字节类型:根据第一格字节的二进制表示,可以判断出字符在使用的字节数

  • 0xxxxx 单字节ASCII字符
  • 1xxxxx 多字节字符的第一个字节(2个字节)
  • 11xxxx 3个字节
  • 111xxx 4个字节

也就是看开头有几个1,判断后面要跟上几个是一个字节

解析 [72 228 189 160]

根据 第一个字节 228 (二进制为 11100010)表明这是一个 3 字节字符的第一个字节,所以后面三个是黏在一起来判断

所以对应到这里


func main() {
   s := "H你"
   fmt.Println([]rune(s))
   fmt.Println([]byte(s))

   fmt.Println("------")
   var r rune = '你'
   fmt.Println(r)
   fmt.Println(string(r))
   fmt.Printf("%%s = %s\n", r)
   fmt.Printf("%%c = %c\n", r)

}

输出就是

func main() {
   s := "H你"
   fmt.Println([]rune(s))
   fmt.Println([]byte(s))

   fmt.Println("------")
   var r rune = '你'
   fmt.Println(r)
   fmt.Println(string(r))
   fmt.Printf("%%s = %s\n", r)
   fmt.Printf("%%c = %c\n", r)

}

rune 输出的是数字,要转化要string强制转化或者使用 %c 格式化

func main() {
   word := "Hello"
   letter := rune(word[0])
   fmt.Println(letter)
   fmt.Printf("%s\n", letter)
   fmt.Printf("%c\n", letter)
   fmt.Printf("%s\n", string(letter))
}


func main() {
   word := "Hello"
   letter := rune(word[0])
   fmt.Println(letter)
   fmt.Printf("%s\n", letter)
   fmt.Printf("%c\n", letter)
   fmt.Printf("%s\n", string(letter))
}

string的强制转化,转化数字也是可以的,类似于Python的char

func main() {
   for i := 65; i <= 122; i++ {
      fmt.Println(i, " - ", string(i), " - ", i%12)
   }
}

这样输出的就是 a b c d

struct 结构体

见如下代码

package main

import "fmt"

type person struct {
   First string
   Last  string
   Age   int
}

type doubleZero struct {
   person
   LicenseToKill bool
}

func (d doubleZero) show() {
   fmt.Printf("First %s, Last %s, Age %d\n", d.First, d.Last, d.Age)
}

func main() {
   p1 := doubleZero{
      person{
         "A",
         "b",
         18,
      },
      true,
   }

   p2 := doubleZero{
      person: person{
         First: "c",
         Last:  "d",
         Age:   111,
      },
      LicenseToKill: false,
   }

   p1.show()
   p2.show()
}

关于p2 一开始看到代码疑惑,为什么p2:直接冒号了,少了等于,不应该是 := 创建吗,后来写了p1才知道,相当于是一个命名了,所以才不需要等于号,简写就是p1的形式,不需要参数名

对于一个结构体,或者说类,定义他的结构体方法/类方法,就是 func (p 结构体名) 函数名

相当于是吧一个函数,绑定到结构体实例里面,所以是这样子写法,func 之后马上跟上括号的结构体实例,然后才是函数名

变量和函数大写小写的问题

比如这样两段代码

package main

import (
   "encoding/json"
   "fmt"
)

type person struct {
   First       string
   Last        string
   Age         int
   notExported int
}

func main() {
   p1 := person{"James", "Bond", 20, 007}
   bs, _ := json.Marshal(p1)
   //fmt.Println(bs)
   //fmt.Printf("%T \n", bs)
   fmt.Println(string(bs))
}

这个可以正常json.dumps 但是这个

package main

import (
   "encoding/json"
   "fmt"
)

type person struct {
   first string
   last  string
   age   int
}

func main() {
   p1 := person{"James", "Bond", 20}
   //fmt.Println(p1)
   bs, _ := json.Marshal(p1)
   fmt.Println(string(bs))
}

就dumps 出来是{} 找了半天,发现区别就是 第一个的变量是大写的,只有大写的,才能导入

这是因为在go中,大写开头的变量和函数,才是能被包外的访问,才是可见,可以作为公共api 如果是小写开头,那么他们就是未被导出的,只能在【定义他们的包内部访问】,对于包外是不可见的,他们具有包的私有访问限制

同样的规则也适用于,函数,类型,接口,结构体的字段和方法可见性 go的包名(导入Import的最后一部分)也遵循这个原则,必须大写开头,以便其他的包能够导入他

package mypackage

// 导出的变量,可以被其他包访问
var ExportedVar int = 10

// 未导出的变量,仅在当前包内可见
var unexportedVar int = 20

// 导出的函数,可以被其他包调用
func ExportedFunc() {}

// 未导出的函数,仅在当前包内可见
func unexportedFunc() {}

tag

json 包定义的时候,还可以指定tag

package main

import (
    "encoding/json"
    "fmt"
)

type persion struct {
    FirstName string
    LastName  string `json:"-"`
    Age       int    `json:"wisdom score"`
    Num       int    `json:"hello,omitempty"`
    Num_str   string `json:"num_str,omitempty"`
    noImport  string
}

func main() {
    p1 := persion{
       "a",
       "b",
       18,
       0,
       "",
       "c",
    }
    bs, _ := json.Marshal(p1)
    fmt.Println(bs)
    fmt.Println(string(bs))
}

注意几个点:

  1. 结构体定义的时候,最后一个参数一定要写上,逗号,不写是错误的
  2. bs, _ := json.Marshal(p1) 这个是冒号等于 ,刚才第一下写成等于是错误的
  3. json包的tag 是 `` 然后里面包裹上json:"" 你要写的内容

这个json:"" 里面可以写新的名字,然后表示重命名 或者写上 - 代表忽略 或者写上忽略的 omitempty

注意一个特别的点:反引号 json:"num_str,omitempty" 这里面千万不能有空格,不能写成 json:"num_str, omitempty" 只要带了空格,那么就不会被识别,会被完全输出

由于Num和Num_str 都是零值,所以不会被输出,对的,甚至包括如果int等于0的话,也算零值,不会被输出

{"FirstName":"a","wisdom score":18}

其实很奇怪,为什么要弄出这么多选择,尤其是omieempty 如果为空省略的话,难道不会造成结构的不一致吗,缺少了某个字段

说省略零值,确实会导致结果的不一致,但是好处在于,向后兼容性,新增字段可能会导致向后兼容性,通过使用 omitempty 只有在有值的时候才包含他,可以避免不必要的版本升级 性能:主要可以减小序列化后的数据大小

其他的还有的选项比如

image.png

反序列化

使用 json.Unmarshar(bs, &p1) 注意bs的定义是 []byte(``) 这里面使用的是反引号

type person struct {
    First string
    Last  string
    Age   int
}

func main() {
    var p1 person
    bs := []byte(`{"First":"a", "Last":"b", "Age": 3}`)
    json.Unmarshal(bs, &p1)
    fmt.Println(bs)
    fmt.Println(string(bs))
    fmt.Println(p1.First)
}

同样的如果带了tag的话,那么就是按照带的tag 去解析


type person struct {
    FirstName string
    LastName  string `json:"-"`
    Age       int    `json:"wisdom score"`
    Num       int    `json:"hello,omitempty"`
    Num_str   string `json:"num_str,omitempty"`
    noImport  string
}

func main() {
    var p1 person

    bs := []byte(`{"FirstName":"a","LastName":"b", "wisdom score":18,"hello":0,"num_str":"","noImport": 123, "xxxx": false}
`)
    json.Unmarshal(bs, &p1)
    //fmt.Println(string(p1))
    fmt.Printf("%v\n", p1)
    fmt.Println("FirstName", p1.FirstName)
    fmt.Println("LastName", p1.LastName)
    fmt.Println("Age", p1.Age)
    fmt.Println("Num", p1.Num)
    fmt.Println("Num_str", p1.Num_str)
    fmt.Println("noImport", p1.noImport)
    fmt.Printf("%+v\n", p1)
}

之所以这么长的代码,是因为之前的那个代码错了,之前的是string(bs) 而不是string 结构式 是不能对结构体string的 另外发现 %v 输出的话,是没有带上字段值的,所以才自己写了这么多的println

后来查询发现,只要带上+就能正确输出了 fmt.Printf("%+v\n", p1) 即可正确输出

{FirstName:a LastName: Age:18 Num:0 Num_str: noImport:}

可以看出,我里面还带了结构体中不包含的其他值,但是在loads的时候也不会报错 说明如果json里面有多值只是不会解析,但是不会报错

json的Marshal 和 NewEncode

NewEncode 和 Marshal 是类似的

type person struct {
    First string
    Last  string
}

func main() {
    p1 := person{
       "a",
       "b",
    }
    fmt.Printf("%+v\n", p1)
    json.NewEncoder(os.Stdout).Encode(p1)

    var p2 person
    rdr := strings.NewReader(`
       {
          "First": "c",
          "Last": "d"
       }
    `)
    //rdr := strings.NewReader(`{"First": "c","Last": “d”}`)
    json.NewDecoder(rdr).Decode(&p2)
    fmt.Printf("%+v\n", p2)

    data, err := json.Marshal(p1)
    if err != nil {
       log.Fatal(err)
    }
    err = os.WriteFile("data.json", data, 0644)
    if err != nil {
       log.Fatal(err)
    }
}

区别就是,Marshal返回包含一个json序列的字符串切片,然后如果要写入网络流或者文件流的话,需要手动再写入一次 NewEncoder 创建一个Encode的话,能直接写入到任何一个io.Write中,例如 os.Stdout 或者 net.conn

例如Marshal要写入网络流要加上

type person struct {
    First string
    Last  string
}

func main() {
    p1 := person{
       "a",
       "b",
    }
    fmt.Printf("%+v\n", p1)
    json.NewEncoder(os.Stdout).Encode(p1)

    var p2 person
    rdr := strings.NewReader(`
       {
          "First": "c",
          "Last": "d"
       }
    `)
    //rdr := strings.NewReader(`{"First": "c","Last": “d”}`)
    json.NewDecoder(rdr).Decode(&p2)
    fmt.Printf("%+v\n", p2)

    data, err := json.Marshal(p1)
    if err != nil {
       log.Fatal(err)
    }
    err = os.WriteFile("data.json", data, 0644)
    if err != nil {
       log.Fatal(err)
    }

    f, err := os.Create("data2.json")
    if err != nil {
       log.Fatal(err)
    }
    defer f.Close()
    err = json.NewEncoder(f).Encode(p1)
    if err != nil {
       log.Fatal(err)
    }
}

注意go 1.16或者更低的版本,要使用 io.WriteFile 后面的新版本都是使用 os.WriteFile

21 interface

接口的一个典型代码如下

package main

import (
    "fmt"
    "math"
)

type circle struct {
    radius float64
}

type square struct {
    side float64
}

type shape interface {
    area() float64
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c square) area() float64 {
    return c.side * c.side
}

func info(c shape) {
    fmt.Println(c)
    fmt.Println(c.area())
}

func totalArea(shapes ...shape) (area float64) {
    for _, s := range shapes {
       area += s.area()
    }
    return
}

func main() {
    s := square{10}
    c := circle{10}
    info(s)
    info(c)
    fmt.Println("Total Area: ", totalArea(s, c))
}

注意的是,这里area的函数定义和普通的不一样,普通的是 func 函数名(参数)...

而area 由于是接口中的函数,所以是 func (实例名 结构体名) 函数名(参数)这种形式

另外现在对...的使用更深刻了,...是跟在实例名后面的,然后才是类型,不是 shapes shape... 而是 shapes... shape 代表好多好多省略到的shape 类型都是shapes

这种接口的一个典型用法就是在io.Copy里面,不管是string还是byte 类型都实现了io.Copy接口


func main() {
    msg := "Do not dwell in the past, do not dream of the future, concentrate the mind on the present."
    rdr := strings.NewReader(msg)
    io.Copy(os.Stdout, rdr)

    rdr2 := bytes.NewBuffer([]byte(msg))
    io.Copy(os.Stdout, rdr2)

    //res, _ := http.Get("http://www.mcleods.com")
    //io.Copy(os.Stdout, res.Body)
    //res.Body.Close()
}

sort的用法

集中的用法如下

package main

import (
    "fmt"
    "sort"
)

type name []string
type name2 = []string

func (n name) Len() int           { return len(n) }
func (n name) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }
func (n name) Less(i, j int) bool { return n[i] < n[j] }

//func (n name2) Len() int           { return len(n) }
//func (n name2) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }
//func (n name2) Less(i, j int) bool { return n[i] < n[j] }

func main() {
    a := []int{3, 1, 4, 5, 2}
    sort.Ints(a)
    fmt.Println(a)
    sort.Sort(sort.IntSlice(a))
    fmt.Println(a)
    sort.Sort(sort.Reverse(sort.IntSlice(a)))
    fmt.Println(a)

    b := []string{"a", "aa", "aaa", "aaaa"}
    sort.Strings(b)
    fmt.Println(b)
    sort.Sort(sort.Reverse(sort.StringSlice(b)))
    fmt.Println(b)

    fmt.Println("-------")

    c := name{"a", "aa", "aaa", "aaaa"}
    sort.Strings(c)
    fmt.Println(c)
    sort.Sort(sort.Reverse(sort.StringSlice(c)))
    fmt.Println(c)

    fmt.Println("-------")

    d := name2{"a", "aa", "aaa", "aaaa"}
    sort.Strings(d)
    fmt.Println(d)
    sort.Sort(sort.Reverse(sort.StringSlice(d)))
    fmt.Println(d)

}

这里面注意几个点

第一个 name 是创建一个全新的类型,name2 是[]string的别名,实际上还是string

所以你如果对name2 实现 len less swap 的话,会报错,因为[]string本身已经实现了这些接口 因为name 是全新类型,所以必须要求实现下面三个函数

  • Len
  • Swap 注意这里函数的参数都是int类型的,用于在数组中调换位置
  • Less

第二个,初始化name的时候,不是 []name类型初始化了,而是直接 name{...}来初始化

第三个,关于嵌套的sort

sort.Sort(sort.Reverse(sort.StringSlice(c)))

sort 本身可以sort.Ints 和sort.String 但是这个只能升序排列,你要降序的话,就得这么来操作

sort.StringSlice 是将string类型的切片,转化为 sort.Interface 接口,任何实现了sort.Interface 接口的就可以排序了 sort.Reverse 接受一个sort.Interface 接口,并对他进行反向排序 sort.Sort 接受一个sort.Interface 接口并对他进行排序

sort.Sort(sort.Reverse(sort.StringSlice(c))) 为什么不写成 sort.Reverse(sort.Sort(sort.StringSlice(c))) 呢,先排序然后倒叙呢?

因为sort.Reverse 是返回一个新的类型,这个类型在sort.Sort的时候会自动逆序排序,sort.Reverse 不是一个排序函数,而是一个对结果进行逆序操作的函数