Go 语言入门7 – 资源管理与错误处理

一、资源管理

Java 有 try-finally,可以在 finally 中进行资源的关闭;Go 可以使用 defer

  • defer 在函数结束时发生调用
  • defer 的调用是栈类型 – 先进后出
  • defer 通常用于资源关闭 Open/Close,Lock/UnLock 等

一句话总结:defer 的调用机制是 “将defer语句加入栈中,当函数结束时(包括正常执行结束 / return / panic 出错结束等),从栈中依次执行 defer”

func writeFile(filename string) {
   file, err := os.Create(filename)
   if err != nil {
      panic(err)
   }
   defer file.Close() // 将 "file.Close()" 压入 defer 栈中

   writer := bufio.NewWriter(file)
   defer writer.Flush() // 将 "writer.Flush()" 压入 defer 栈中

   fmt.Fprintln(writer, "123")
   // 当函数执行结束时,从 defer 栈中执行语句 - 后进先出,先 "writer.Flush()",再 "file.Close()"
}

func main() {
   writeFile("defer.txt")
}

二、错误简单处理

使用机制

通过被调用函数的注释查看其可能发生的错误,然后依据错误类型并进行处理;
错误处理结束后要 return

func main() {
   file, err := os.Open(filename)
   // 错误处理
   if err != nil {
      // 判断 err 是否是 *os.PathError,因为 os.Open(filename) 的注释:"If there is an error, it will be of type *PathError."
      if pathError, ok := err.(*os.PathError); ok {
         fmt.Printf("error: %s", pathError.Error())
      } else {
         fmt.Printf("notKnown error:%s", err.Error())
      }
      return // 返回
   }
}

//error 是一个接口,定义如下:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
   Error() string
}

基于该接口我们可以实现自定义的 error 实现。( errors 是 error 接口的一个实现,可以直接仿照,也可以直接使用,如 err := errors.New(“my custom error”))

三、panic & recover

  • panic
    • 停止当前程序运行
    • 一直向上返回,执行每一层的 defer
    • 如果没有遇见 recover,程序退出
  • recover(相当于对 panic 的 catch 语句)
    • 仅在 defer 调用中使用
    • 获取 panic 的值
    • 如果无法处理,可重新 panic
import (
   "fmt"
   "errors"
)

func recove() {
   defer func() {
      // func recover() interface{},表示 recover() 函数的返回类型可以是各种类型,所以要判断是否是 error
      // 使用 recover() catch panic,防止程序直接退出
      r := recover()
      if err, ok := r.(error); ok {
         fmt.Println(err) // runtime error: integer divide by zero
      } else {
         panic(errors.New("not known error"))
      }
   }()

   b := 0
   a := 5/b // panic: runtime error: integer divide by zero
   fmt.Println(a)

   //panic("123") // panic: not known error
}

func main() {
   recove()
}

四、错误统一处理

一个生产系统通常包含两种异常

  • 不可直接暴露给用户的异常:例如系统内部异常
  • 需要暴露给用户的异常:例如部分自定义异常信息用于提示用户操作

本节写一个需求:实现一个读取文件的 httpServer 处理器。
代码结构如下:

1556591347-7773-5842684-d463e5d73785616d

4.1 userError 自定义用户异常接口

package exception

type UserError interface {
   error // 内嵌类型
   Message() string
}

4.2 myCustomError 自定义用户异常实现

package exception

// 基于基本类型创建自定义类型
type MyCustomError string

func (e MyCustomError) Error() string {
   return e.Message()
}

func (e MyCustomError) Message() string {
   return string(e)
}

4.3 handler 核心业务逻辑处理器

package handler

import (
"net/http"
"os"
"io/ioutil"
"strings"
"exception"
)

const PathPrefix = "/list/"

// 实现一个读取文件的 httpServer 处理器
// 假设访问 http://localhost:8888/list/abc.txt
func HandleFileListing(writer http.ResponseWriter, request *http.Request) error {
   // 1. 如果 urlPath 不是以 /list/ 开头的,则自定义用户错误
   if strings.Index(request.URL.Path, PathPrefix) != 0 {
      return exception.MyCustomError("url path need startWith /list/")
   }
   //fmt.Println("path", request.URL.Path)    // /list/abc.txt
   path := request.URL.Path[len(PathPrefix):] // abc.txt 字符串切割,subString

   // 2. 打开文件
   file, err := os.Open(path)
   if err != nil {
      // 遇到错误直接返回,由错误统一处理器进行处理
      return err
   }
   defer file.Close()

   // 3. 读取文件到 byte[]
   all, err := ioutil.ReadAll(file)
   if err != nil {
      return err
   }

   // 4. 将 byte[] all 写出到响应流
   writer.Write(all)
   return nil
}

4.4 errorWrapperHandler 统一错误处理器

package exception

import (
"net/http"
"log"
"os"
)

// 定义一个 function 类型的 type,返回值是 error
type appHandler func(writer http.ResponseWriter, request *http.Request) error

// 输入 appHandler 是一个函数,输出也是一个函数 - 函数式编程
func ErrWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
   return func(writer http.ResponseWriter, request *http.Request) {
      // 1. 处理业务逻辑
      err := handler(writer, request)
      if err != nil {
         log.Printf("error occured, %s", err) // 2018/11/04 10:10:12 error occured, open abc.txt1: no such file or directory

         // 2. 处理可以抛给用户的错误
         if err, ok := err.(UserError); ok {
            // 将错误写回到 http.ResponseWriter
            http.Error(writer, err.Message(), http.StatusBadRequest)
         }

         // 3. 处理不可以抛给用户的错误
         code := http.StatusOK
         switch {
         case os.IsNotExist(err):
            code = http.StatusNotFound
         default:
            code = http.StatusInternalServerError
         }
         http.Error(writer, http.StatusText(code), code) // 浏览器:Not Found
      }
   }
}

注意这样的姿势:定义一个 function 类型的 type,返回值是 error

type appHandler func(xx) error

4.5 web httpServer 服务器

package main

import (
"net/http"
"handler"
"exception"
)

func main() {
   // 1. 注册处理 handler.PathPrefix 开头的业务逻辑处理器
   http.HandleFunc(handler.PathPrefix, exception.ErrWrapper(handler.HandleFileListing))

   // 2. 启动 httpServer,监听端口
   err := http.ListenAndServe("127.0.0.1:8888", nil)

   // 3. 如果启动失败,则直接抛出错误
   if err != nil {
      panic(err)
   }
}

Go 语言入门6 – 闭包

// 该函数的返回值是一个函数:func(value int) int
func adder() func(value int) int {
    sum := 0 // 自由变量,Go 的闭包是可以含有自由变量的
    return func(value int) int {
        sum += value
        return sum
    }
}

func main() {
    add := adder() // 此处返回一个函数
    for i := 0; i < 10; i++ {
        fmt.Printf("0 + ... + %d = %d", i, add(i)) //调用函数add
        fmt.Println()
    }
}
  • adder() 函数的返回值是一个函数
  • 注意:在 Go 语言中,闭包是可以含有自由变量的

Go 语言入门5 – 接口

一、接口

1.1、定义接口

// notifier 是一个定义了通知类行为的接口
type notifier interface {
    // 接口方法
    notify()
}

1.2、实现接口

  • 使用值接收者实现接口
import "fmt"

// notify 是使用值接收者实现 notifier interface 接口的方法
// sendNotification(&u) 和 sendNotification(u) 都可
func (u user) notify() {
   fmt.Println("notify", u)
}

对于值接收者,sendNotification(&u) 和 sendNotification(u) 都可

  • 使用指针接收者实现接口
// notify 是使用指针接收者实现 notifier interface 接口的方法
// 只能使用 sendNotification(&u)
func (u *user) notify() {
    fmt.Println("notify", *u)
}

对于指针接收者,只能使用 sendNotification(&u)

非常重要的点 - 值接收者与指针接收的对比
https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver-in-golang:详细的介绍了什么时候使用值接收者,什么时候使用指针接收者
https://studygolang.com/articles/1113
https://blog.csdn.net/suiban7403/article/details/78899671
type User struct {
    Name  string
    Email string
}
// 注意这个是值接收者
func (u User) Notify() error {
    u.Name = "alimon"
    return nil
}

func main() {
    u := &User{"Damon", "damon@xxoo.com"}
    u.Notify()
    log.Println(u.Name)
}

注意

  • 值接收者(func (u User) Notify() error)操作的是 User 的副本(不管调用者使用值还是地址),所以不会改变其内部的值,这里输出还是 Damon
  • 指针接收者(func (u *User) Notify() error)操作的是 User 本身,所以其内部的值会发生变化,这里输出是 alimon

什么时候使用值接收者,什么时候使用指针接收者

If the receiver is a map, func or chan, don’t use a pointer to it.
If the receiver is a slice and the method doesn’t reslice or reallocate the slice, don’t use a pointer to it.
If the method needs to mutate(使 receiver 改变) the receiver, the receiver must be a pointer.
If the receiver is a struct that contains a sync.Mutex or similar synchronizing field, the receiver must be a pointer to avoid copying.
If the receiver is a large struct or array, a pointer receiver is more efficient. How large is large? Assume it’s equivalent to passing all its elements as arguments to the method. If that feels too large, it’s also too large for the receiver.
Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer.
If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating, prefer a pointer receiver, as it will make the intention more clear to the reader.
If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used
If the receiver is a small array or struct that is naturally a value type (for instance, something like the time.Time type), with no mutable fields and no pointers, or is just a simple basic type such as int or string, a value receiver makes sense.
A value receiver can reduce the amount of garbage that can be generated; if a value is passed to a value method, an on-stack copy can be used instead of allocating on the heap. (The compiler tries to be smart about avoiding this allocation, but it can’t always succeed.) Don’t choose a value receiver type for this reason without profiling first.

Finally, when in doubt, use a pointer receiver.

总结一下:

  • 如果需要改变 receiver 内部的属性值,选择指针接收者;
  • 如果 struct 中的一个方法使用了指针接收者,那么该 struct 内的全部方法都是用指针接收者 – 一致性

1.3、使用接口

// sendNotification 接受一个实现了 notifier 接口的值并发送通知
func sendNotification(n notifier) {
    n.notify()
}

func main() {
    u := user{"nana"}
    sendNotification(&u) // notify {nana}
}

二、实现多态

api.go

package api

// 定义接口
type Notifier interface {
    Notify()
}

接口名和接口方法大写表示 public,小写表示 java 的 default(即包隔离级别)

user.go

package impl

import "fmt"

// 定义实现类 user
type User struct {
    Name string
}

func (u *User) Notify() {
    fmt.Println("user", *u)
}

admin.go

package impl

import "fmt"

// 定义实现类 Admin,首字母大写表示 public
type Admin struct {
    Name string
}

func (a *Admin) Notify() {
    fmt.Println("admin", *a)
}

main.go

package main

import (
    // 相对于 GOPATH/src 下的地址
    "github.com/zhaojigang/helloworld/ooi/api"
    "github.com/zhaojigang/helloworld/ooi/impl"
)

func sendNotification(n api.Notifier) {
    n.Notify()
}

func main() {
    u := impl.User{"nana"}
    sendNotification(&u) // user {nana}

    a := impl.Admin{"zhao"}
    sendNotification(&a) // admin {zhao}
}

三、嵌入类型内部接口实现提升

基于上述程序修改 admin.go 和 main.go。

admin.go

package impl

// 定义实现类 Admin,首字母大写表示 public
type Admin struct {
    User // 嵌入类型
    Name string
}

注意:该类没有实现接口 notifier 的 notify() 方法,但是由于该类的内嵌类 User 实现了,内嵌类会提升到外部类中,所以 Admin 类也是 notifier 接口的实现类,其实现函数就是 User#Notify() ,当然,可以 Admin 可以自己实现 Notify() 来覆盖 User#Notify()。

main.go

package main

import (
    // 相对于 GOPATH/src 下的地址
    "github.com/zhaojigang/helloworld/ooi/api"
    "github.com/zhaojigang/helloworld/ooi/impl"
)

func sendNotification(n api.Notifier) {
    n.Notify()
}

func main() {
    u := impl.User{"nana"}
    sendNotification(&u) // user {nana}

    a := impl.Admin{u,"zhao"}
    sendNotification(&a) // user {nana}
}

四、组合接口

=========================== Api1 ===========================
package api

type Api1 interface {
    Api1()
}
=========================== Api2 ===========================
package api

type Api2 interface {
    Api2()
} 
=========================== Api12(组合接口) ===========================
package api

type Api12 interface {
    Api1
    Api2
}
=========================== Api12Impl ===========================
package impl

import "fmt"

type Api12Impl struct {
    Name string
}

func (api12 *Api12Impl) Api1()  {
    fmt.Println("api1", api12.Name)
}

func (api12 *Api12Impl) Api2()  {
    fmt.Println("api2", api12.Name)
}
=========================== main ===========================
package main

import (
    // 相对于 GOPATH/src 下的地址
    "github.com/zhaojigang/helloworld/ooi/api"
    "github.com/zhaojigang/helloworld/ooi/impl"
)

func sendNotification(n api.Api12) {
    n.Api1() // api1 nana
    n.Api2() // api2 nana
}

func main() {
    api12 := impl.Api12Impl{"nana"}
    sendNotification(&api12)
}

Go 语言入门4 – 对象

一、自定义结构

定义结构:type 结构名称 struct

*注:go语言中是没有类这回事,但是它使用了其他方式来充当类,比如一个文件xxx.go那么就属于一个xxx类。go语言对命名方式做了一点智能化处理,如果名字是以小写开头的则只能在包范围内访问,如果是以大写字母开头的,则属于公开,类似于java、php中的public。

1.1、定义结构

type User struct {
   id   int
   name string
}

type Admin struct {
   user User
   role string
}

1.2、赋值

var user User
user.id = 1
user.name = "alan"
fmt.Println(user)

lisa := User{2, "lisa"}
lily := User{id:2,name: "lily"}
fmt.Println(lisa)
fmt.Println(lily)

二、方法

  • 方法的定义方法实际上也是函数,只是在声明时,在关键字 func 和方法名之间增加了一个参数
    • 普通的函数定义 func 方法名(入参) 返回值
    • 自定义类型的方法定义 func (接收者) 方法名 (入参)  返回值
  • 方法的值传递和指针传递
    • func (u user) notify() 拷贝一份 user
    • func (u *user) changeEmail(newEmail string) 传递指针(即地址),内部改变会影响外部
//定义一个类型
type User struct {
   id   int
   name string
}

//定义3个方法
func (u User) getName() string {
   return u.name;
}

func (u *User) setName(name string) {
   u.name = name
}
func (u User) println() {
   fmt.Println(u)
}

//测试方法
func test3() {
   var user User
   user.id = 1
   user.name = "alan"
   fmt.Println(user.getName())// alan
   user.setName("Alan2")
   user.println()// 1 Alan2
}

注意:“再强调一次,println() 操作的是一个副本,只不过这次操作的是从 user 指针指向的值的副本。”

三、嵌入类型

  • Go 语言允许用户扩展或者修改已有类型的行为。这个功能是通过嵌入类型 type embedding 完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
  • Java 通常可以通过继承或组合的方式实现嵌入类型要实现的功能。
  • “要嵌入一个类型,只需要声明这个类型的名字就可以了”,即 user 而不是 u user, u user 是声明字段
  • 内部类型的方法可以被提升到外部类型直接使用

*注:嵌入类型实际上就是go的多态,类似于java的继承。

//定义一个user基础类型
type User struct {
   id   int
   name string
}
//定义个admin基础类型
type Admin struct {
   user User
   role string
}
//定义一个admin2嵌入user类型
type Admin2 struct {
   User
   role string
}

//测试方法
func test4() {
   var admin Admin;//定义一个admin对象
   admin.user = User{id: 1, name: "alan"};
   admin.role = "Administrator"

   admin.setName("test") //错误,这里的admin并不会继承user中的方法

   admin2 := Admin2{role: "Administrator"};
   admin2.id = 1
   admin2.name = "alan"
   admin2.getName()//这里的admin2对象会自动继承user中的方法
   admin2.println()

}
//给User类型添加getName函数
func (u User) getName() string {
   return u.name;
}
//给User类型添加setName函数
func (u *User) setName(name string) {
   u.name = name
}
//给User类型添加println函数
func (u User) println() {
   fmt.Println(u)
}

Go语言入门3 – 容器

容器

  • Array 数组
  • Slice 切片(可以看成动态的数组)
  • Map 映射

一、Array 数组

  • [10]int 和 [20]int 是不同类型,数组类型相同要长度和元素类型完全相同才可以
  • func loopArray(arr2 [3]int) 是值传递,入参会拷贝一份数组,所以如果数组很大,从内存和性能上函数传递数组值都是很大的开销,需要避免(使用指针可以实现”引用传递” func loopArray(arr2 *[3]int),调用方传入 &arr2)
  • 在 Go 中一般不直接使用数组,而是使用切片
  • 数组是定长的,不可扩展,切片相当于动态数组
import "fmt"

func defineArray() [3]int {
   // 定义数组,不赋初值(使用默认值)
   var arr1 [5]int // [0 0 0 0 0]
   // 定义数组,赋初值
   arr2 := [3]int{1, 2, 3} // [1 2 3]
   // 定义数组,由编译器来计算长度,不可写成[],不带长度或者 ... 的表示切片
   arr3 := [...]int{4, 5, 6, 7} // [4 5 6 7]
   // 创建指针数组
   arr4 := [2]*string{new(string), new(string)}
   *arr4[0] = "hello"
   *arr4[1] = "go"
   // 为指定索引位置设置值
   arr5 := [3]int{1:10} // [0,10,0]
   // 二维数组
   var grid [4][5]int // [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
   // 数组拷贝,直接复制一份 arr2 给 arr6
   arr6 := arr2

   fmt.Println(arr1, arr2, arr3, arr4, arr5, arr6, grid)// arr4 打印出来的是地址 [0xc00000e1e0 0xc00000e1f0]
   fmt.Println(*arr4[0]) // hello
   return arr2
}

// 数组是值传递,这里的入参会拷贝一份数组(使用指针可以实现"引用传递")
func loopArray(arr2 [3]int) {
   // 通用方法
   for i := 0; i < len(arr2); i++ {
      fmt.Println(arr2[i])
   }
   // 最简方法,只获取数组下标
   for i := range arr2 {
      fmt.Println(arr2[i])
   }
   // 最简方法,获取数组下标和对应的值
   for i, v := range arr2 {
      fmt.Println(i, v)
   }
   // 最简方法,只获取值,使用 _ 省略变量
   for _, v := range arr2 {
      fmt.Println(v)
   }
}

二、Slice 切片

切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。

切片有 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)

5842684-3ed9eb450701d434

从切片 slice1 创建出来的切片 slice2,slice1 和 slice2 共享底层数组,一个修改了共享部分的元素,另一个也会感知

2.1、创建切片

// 1、使用make函数创建一个字符串切片,长度和容量都是5
slice1 := make([]string, 5)

// 2、创建一个int切片,长度是3,容量是5
slice2 := make([]int, 3, 5)

// 3、使用字面量创建切片,长度是3,容量是3
slice3 := []int{1, 2, 3} // [3]int{1, 2, 3}

// 4、创建 nil 切片,长度为0,容量为0
var slice4 []int

// 5、创建空切片,长度为0,容量为0
slice5 := make([]int, 0)
slice6 := []int{}

// 6、自定义底层数组,通过该底层数组创建切片
arr := [5]int{1, 2, 3, 4, 5}

// 数组转化为切片,左闭右开 [arr[2]~arr[4])
slice7 := arr[2:4] // [3,4]
slice8 := arr[2:]  // [3,4,5]
slice9 := arr[:4]  // [1,2,3,4]
slice10 := arr[:]   // [1,2,3,4,5]

实际上还有第七种创建切片的方式:根据切片创建切片,称为 reslice

2.2、切片使用

slice1 := []int{1, 2, 3, 4, 5}

// 1、根据索引获取切片元素
fmt.Println(slice1[1]) // 2

// 2、根据索引修改切片元素
slice1[3] = 400
fmt.Println(slice1) // [1, 2, 3, 400, 5]

// 3、根据切片创建切片,和根据自定义数组创建切片方式相同,长度是2=3-1,容量是4=5-1
// 但是需要格外注意,新生成的切片 slice2 和原始切片 slice1 的指针元素指向了相同的底层数组,所以修改元素要注意
slice2 := slice1[1:3] // [2, 3]
slice2[1] = 300
fmt.Println(slice2) // [2, 300]
fmt.Println(slice1) // [1, 2, 300, 400, 5] slice1也发生了变化

// 4、拷贝 slice 中的元素
fmt.Println("copy")
slice3 := []int{0, 0, 0, 0, 0}
slice4 := []int{1, 2, 3}
copy(slice3, slice4)
fmt.Println(slice3) // [1, 2, 3, 0, 0]
fmt.Println(slice4) // [1, 2, 3]    

// 5、删除 slice 中的元素,删除slice5[2]=3
fmt.Println("delete")
slice5 := []int{1, 2, 3, 4}
slice5 = append(slice5[:2], slice5[3:]...)
fmt.Println(slice5) // [1, 2, 4]

2.3、append 增加切片长度

5842684-081821049d9e13f3

// 1、创建原始切片,长度是5,容量是5
slice := []int{10, 20, 30, 40, 50}

// 2、reslice 新切片,长度是2,容量是4
newSlice := slice[1:3] // [20, 30]

// 由于底层数组还有容量,可以直接追加元素而容量不变
newSlice = append(newSlice, 60) // [20, 30 ,60] 长度是3,容量是4
fmt.Println(newSlice)           // [20, 30 ,60]
fmt.Println(slice)              // [10, 20, 30 ,60, 50]

// 长度4,容量4
slice := []int{10, 20, 30, 40}
// 此时切片容量用完了,再追加需要扩容,此处会新加数组,长度为原数组的2倍,即 newSlice 的底层数组是新数组,新切片容量为8;
// 而 slice 的底层数组是旧数组,二者互不影响
newSlice := append(slice, 50)
fmt.Println(slice)    // [10, 20, 30, 40]
fmt.Println(newSlice) // [10, 20, 30, 40, 50]
newSlice[0] = 100
fmt.Println(slice)    // [10, 20, 30, 40]
fmt.Println(newSlice) // [100, 20, 30, 40, 50]

这里是我自己的测试代码:

b := make([]string, 3, 5)
b[0] = "A"
b[1] = "B"
b[2] = "C"
//b[3]="C1"
//b[4]="C2"
b_1 := append(b, "D")
b_1 = append(b, "E")
fmt.Println("b:", b)
fmt.Println("b_1:", b_1)

b1 := []int{1, 2, 3, 4, 5}
var b2 []int
b3 := b1[0:2]
fmt.Println(b1)

b1[0] = 100

fmt.Println(b2)
fmt.Println(b3)
fmt.Println(&b1[0])
fmt.Println(&b3[0])
fmt.Println(len(b3))

fmt.Println("b1:", b1)
b1_1 := append(b1, 6);
b1_2 := append(b1_1, 7);
b1_3 := append(b1_2, 8);
b1_3 = append(b1_3, 9);
b1_3 = append(b1_3, 10);
b1_3 = append(b1_3, 11);
b1_3 = append(b1_3, 12);
b1_3 = append(b1_3, 13);
b1_3 = append(b1_3, 14);
b1_3 = append(b1_3, 15);
b1_3 = append(b1_3, 16);
b1_3 = append(b1_3, 17);
b1_3 = append(b1_3, 18);
b1_3 = append(b1_3, 19);
b1_3 = append(b1_3, 20);
b1_3 = append(b1_3, 21);
//b1_1 = append(b1, 9);
//b1_1 = append(b1, 10);

fmt.Println("b1_1:", b1_1)
fmt.Println("b1_2:", b1_2)
fmt.Println("b1_3:", b1_3)
fmt.Println("b1_3:", len(b1_3))
fmt.Println("b1_3:", cap(b1_3))

5842684-dc4d519b7d344b50

  • 当切片容量(而非数组长度,默认切片容量等于数组长度,也可以显示指定)用完了,再追加需要扩容,此处会新建数组,长度为原数组的2倍,然后将旧数组元素拷贝到新数组,newSlice 的底层数组是新数组,newSlice 容量为8;而 slice 的底层数组是旧数组,二者互不影响。
  • slice 扩容机制:在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25% 的容量。

2.4、显示设置容量

在没有显示指定容量的情况下,切片容量就是其底层数组的长度,如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改

source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 长度为1=3-2,容量为1=3-2  source[i:j:k] 长度=j-i 容量=k-i
slice := source[2:3:3]
fmt.Println(source) // ["Apple", "Orange", "Plum", "Banana", "Grape"]
fmt.Println(slice) // ["Plum"]
// 超出切片容量3,需要新建数组
slice = append(slice, "Kiwi")
fmt.Println(source) // ["Apple", "Orange", "Plum", "Banana", "Grape"]
fmt.Println(slice) // ["Plum", "Kiwi"]

2.5、合并切片

s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1,s2...)) // [1, 2, 3, 4]

2.6、迭代切片

  slice := []int{10, 20, 30, 40}
    // 与数组迭代一样,可以使用 for range + 普通 for 循环
    for index,value := range slice {
        fmt.Println(index, value)
    }

2.7、函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低;而在函数间传递数组是需要拷贝整个数组的,所以内存和性能上都不好
调用函数,传递一个切片副本,实际上内部还是传递了对数组的指针,所以 foo 内部的操作会影响 main 中的 slice。

import "fmt"

func foo(slice []int) []int {
   slice[0] = 100
   return slice
}

func main() {
   // 1、创建一个 slice
   slice := []int{1, 2, 3, 4, 5}
   fmt.Println(slice) // [1, 2, 3, 4, 5]
   // 2、调用函数,传递一个切片副本,实际上内部还是传递了对数组的指针,
   // 所以 foo 内部的操作会影响 main 中的 slice
   slice2 := foo(slice)
   fmt.Println(slice2) // [100, 2, 3, 4, 5]
   fmt.Println(slice) // [100, 2, 3, 4, 5]
}

三、Map 映射

  • Map 是一个存储键值对的无序集合,就是说不保证顺序
  • Slice、Map、function 以及包含切片的结构类型不能作为 Map 的 key
  • map在函数间传递,不会拷贝一份map,相当于是”引用传递”,所以remove函数对传入的map的操作是会影响到main函数中的map的

3.1、基本用法

// 1、使用 make 创建 map,key为string,value为int
map1 := make(map[string]int)
// 2、使用字面量创建 map - 最常用的姿势,key为string,value为slice,初始值中的slice可以不加 []string 定义
map2 := map[string][]string{"hi": {"go", "c"}, "hello": []string{"java"}}
// 3、创建空映射
map3 := map[string]string{} // map3 := map[string]string nil映射
fmt.Println(map1, map2, map3)

// 4、向映射添加值
fmt.Println("map put")
map3["a"] = "x"
map3["b"] = "y"
fmt.Println(map3) // map[a:x b:y]

// 5、获取值并判断是否存在
value, exist := map3["c"]
if exist {
fmt.Println(value)
} else {
fmt.Println("map3[\"c\"] does not exist")
}

// 6、迭代
fmt.Println("iterat")
for key, value := range map3 {
fmt.Println(key, value)
}

// 7、从 map 中删除元素
delete(map3, "a")
fmt.Println(map3) // map[b:y]

以下是我的笔记代码:

m1 := make(map[string]string)
m1["a"] = "A"
m1["b"] = "B"
m1["c"] = "C"
m1["d"] = "D"
fmt.Println(m1)

m2 := map[int]int{}
m2[0] = 100
m2[1] = 100 * m2[0]
m2[2] = 100 * m2[0]
fmt.Println(m2)

parserS1(m1);
//delete(m1,"a")
v, m_1 := m1["a"]
if m_1 {
   fmt.Println("存在的", v)
} else {
   fmt.Println("不存在的", m_1)
}

for i, n := range m1 {
   fmt.Printf("me-> Key %s : %s \n", i, n)
}

3.2、函数间传递映射

import "fmt"

// map在函数间传递,不会拷贝一份map,相当于是"引用传递",所以remove函数对传入的map的操作是会影响到main函数中的map的
func remove(map4 map[int]int)  {
   delete(map4, 1)
}

func main() {
   map4 := map[int]int{0:0, 1:1, 2:2}
   fmt.Println(map4) // map[0:0 1:1 2:2]
   remove(map4)
   fmt.Println(map4) // map[0:0 2:2]
}