十分钟速读 Effective Go (二)

第一部分在这里:十分钟速读 Effective Go (一)

7. 函数

多返回值特性

在其他语言中,通常会返回一个特殊值,比如 -1 这种,标识异常,但是 Golang 中直接支持返回多个值,其中一个值通常是表示是否存在错误

Golang 复制代码
// 通常错误值是放在最后一个返回的
func (file *File) Write(b []byte) (n int, err error)
可命名返回值参数
Golang 复制代码
// 返回值 n 和 err 已提前声明,函数中可直接使用,注意声明的值均为对应类型的默认值
func (file *File) Write(b []byte) (n int, err error) {
    // return 不带返回值,则默认返回 n 和 err
    return
}
Defer(延迟执行)机制
Golang 复制代码
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err  // 如果代码在这里 return f.Close() 不会被执行,因为他还没有通过 defer 注册
    }
    defer f.Close()  // 声明一个延迟执行的代码块,他将在函数返回之前立即执行

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // 如果在这里 return f.Close() 会被执行
        }
    }
    return string(result), nil // 如果代码在这里 return f.Close() 也会被执行
}

8. 数据

利用 new / make 分配内存空间
  • new 为类型初始化了空间并返回其指针
Golang 复制代码
// 等同于  p := &SyncedBuffer{},new 为类型初始化了空间并且初值都置为了0,然后返回其指针
p := new(SyncedBuffer)  
// 这个与 new(SyncedBuffer) 不同,这个仅仅是声明一个变量,并没有为结构体初始化空间和值,而是一个 nil 指针
var p *SyncedBuffer 
  • make 函数用于分配并初始化引用类型(如slice、map和channel)等,并且支持 初始化时指定长度
Golang 复制代码
// arr 为*[]int64类型, arr 其实相当于  var arr *[]int64,在 Slice 底层并没有为 slice 的底层数组分配空间
arr := new([]int64) 
*arr = append(*arr, 1)
ar := make([]int64, 0) // arr = &ar 在 Slice 底层为 slice 的底层数字分配了长度为 0 的空间
  • 一种 Golang 独有的初始化结构体,slice,map 等的方式:Composite literal 在中文中可以翻译为"复合字面量"或者"组合字面量"
go 复制代码
// 其实就是最基本的声明并赋初值的一种简化语法操作,基础语法,入门必备,避免了使用 new,更简洁
slice := []int{1,2,3} 

实际编码中,大量在使用Composite literal or make,极少用到 new

数组

数组是 Slice 底层实现的基础,Golang 中数组不完全等同于 c 语言的数组,它有如下几个独特特点:

  • Golang 中数组可以类比为一个值类型,将数组进行赋值时实际上执行的是值拷贝;
  • 数组作为函数参数传递时,也会进行值拷贝传递,而非指针;
  • 类型[10]int[20]int是不同的,数组的大小是其类型的一部分;

当然仍然可以通过 &array 方式传递数组指针,但是这并非 Go 官方推荐的做法,如果有此需求,直接使用 Slice 更好

Slice(切片)
  • Slice 是对数组的二次封装,使其功能更强大,Slice 底层仍然是利用数组进行数据存储
  • Slice 中存储的是数组的地址,所以 Slice 复制时,两个 Slice 指向了同样的底层数组,所以当函数传递的参数是 Slice 时,实际上传递的是数组的地址,子函数对 Slice 的修改可能会反馈到父函数中,为何这里是可能而不是一定呢,请看下面代码:
Golang 复制代码
package main

import (
	"fmt"
)

func main() {
	slice := []int{1, 2}
	slice = append(slice, 3)   // slice 按照双倍扩容,所以下面的 cap 为 4
	fmt.Println("cap", cap(slice))
	passSlice(slice)      // 第一次调用添加一个元素,未触发
	fmt.Println("slice", slice)
	slice = append(slice, 4)
	fmt.Println("cap", cap(slice))
	passSlice(slice)
	fmt.Println("slice", slice)
}

func passSlice1(s []int) { // s 指向的底层数组与 slice 仍然一致,容量为 4,长度为 3
	s = append(s, 4)   // 未触发扩容,在原数组上增加了一个元素,容量为 4,长度为 4
	s[0] = 100         // 修改数据,现在 s 和 slice 仍然共用一个底层数组,故该修改影响父函数的变量
	fmt.Println("sub func slice:", s, "cap", cap(s))
}
func passSlice2(s []int) { // s 指向的底层数组与 slice 仍然一致,容量为 4,长度为 4
	s = append(s, 4)   // 触发扩容,容量扩为 8 个,长度为 5,注意此时底层数组地址已经变了
	s[0] = 200         // 修改数据,现在 s 和 slice指向不同底层数组,故该修改不影响父函数的变量
	fmt.Println("sub func slice:", s, "cap", cap(s))
}
// Output 
cap 4
sub func slice: [100 2 3 4] cap 4
slice [100 2 3]     // 可以看到外层数据被修改了
cap 4
sub func slice: [200 2 3 4 4] cap 8
slice [100 2 3 4]   // 可以看到外层数据未被修改

上面的 append 函数实现是会根据 slice 的容量和长度动态扩容,扩容时会重新复制底层数组的数据

二维切片(数组)

二维切片其实就是数组的数组或者切片的切片,官方推荐了两种初始化方式,第一种针对会可能发生长度变化的二维切片

Golang 复制代码
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // 分配多少行
// Loop over the rows, allocating the slice for each row.
for i := range picture {
	picture[i] = make([]uint8, XSize) // 分配每行的元素个数
}

第二种针对完全固定的长度的,可以一次性分配内存(非极致性能要求场景个人不推荐,这种写法可能对一些人理解起来费劲,性能影响也极少)

go 复制代码
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
//  创建一个完整的大 Slice
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// 这里需要注意,将大 Slice 切成小的 Slice 时,底层并不会重新分配内存,而是指向原来的 Slice 内存
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Map

map 基础用法这里不做赘述,因为 golang 的强类型机制,即使从 map 中获取不存在的值也会返回对应类型的默认值,这时候要利用 Golang 中特有的一种 comma ok 的写法来判断:

Golang 复制代码
func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok { // ok 为 true 才表示 map 中包含 key 为 tz 的值
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

delete(timeZone, "PDT")  // Now on Standard Time

## []()

另外特别注意 map 是一种并发不安全的类型, effective go 在该章节未做特别说明

打印输出
占位符 描述
%d 整数,可以是十进制、八进制或十六进制的数字
%f 浮点数,可以指定小数点后的精度
%s 字符串
%t 布尔值
%v (value)支持任意类型,输出等同于 fmt.Println
%b 二进制数
%x 十六进制数
%o 八进制数
%c 字符
%T 打印值类型

特别支持: %+v 打印时会将结构体的字段名打印出来; %#v 会将包含结构体名称等打印出来
任何一个类型都可以实现 func (m MyString) String() string 接口,然后自定义 %s/%v 等的输出内容

Golang 复制代码
type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}
a := MyString("aaa")
fmt.Printf("%v  %+v  %#v  %T  %#+q %s\n", a, a, a, a, a, a)
fmt.Println(a)
// Output, 可以看到自定义的 String 方法影响了大部分格式化输出
MyString=aaa  MyString=aaa  "aaa"  `MyString=aaa` MyString=aaa
MyString=aaa
append 函数的补充说明

该函数原型如下,其中类型T是调用者决定的,这个函数无法通过目前的Go语法实现(不过泛型是可以实现类似能力的),所以它是个内建函数

scss 复制代码
func append(slice []T, elements ...T) []T

注意该函数的返回值,与传入的值不一定是同一个,slice有可能在添加原始时动态扩容,返回一个新的地址; 另外该函数支持任意量元素添加,当我们期望将一个slice添加到另外一个slice后面时,需要利用...展开操作符:

Golang 复制代码
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

9. 初始化

Go中初始化其实就三个部分,分别为常量、变量和函数

常量

常量也分为全局和局部,但要求均是在编译器期间就可求值;常量只能是数字、字符(符文)、字符串或布尔值; 常量创建过程中有个特殊的 iota 值,是一个编译器预定义的标识符,当在一个包中第一次声明一个常量时,iota会被设置为0。在后续的常量声明中,iota会自动递增。它主要用于创建一系列相关的常量,例如枚举类型:

Golang 复制代码
const (
	Monday    = iota // 0
	Tuesday          // 1
	Wednesday        // 2
	Thursday         // 3
	Friday           // 4
	Saturday         // 5
	Sunday           // 6
)
变量

变量其初始值也可以是在运行时才被计算,也可修改

初始化函数 init
  • 可以定义多个init函数,会顺序执行
  • init函数的执行是等该包中的所有变量声明都初始化完成后执行,所以init中很适合做参数检查和默认值
  • 各个包中的init是按照该包导入顺序执行的,且只执行一次

10. 方法

  • 可以为任何已命名的类型(除了指针或接口)定义方法; 接收者可不必为结构体;
Golang 复制代码
type ByteSlice []byte
// 值方法
func (slice ByteSlice) Append(data []byte) []byte {
	
}
// 指针方法
func (slice *ByteSlice) Append(data []byte) (n int, err error) {
	
}
  • 方法接受者可以为指针或值,区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用
Golang 复制代码
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (r Person) method() {
	fmt.Println("值方法")
}
func (r *Person) method2() {
	fmt.Println("指针方法")
}

func main() {
	var pv Person
	pv.method2()
	pv.method()
	var p *Person
	// p.method() // 这句会报错,
	p.method2()
        p = &pv
	p.method() // 这里就不会报错了,因为 p 已经指向了具体的实例,
	p.method2()
}
  • 指针方法可以修改接收者;值方法调用它们会导致方法接收到该值的副本

11. 接口

Go 中的接口为指定对象的行为提供了一种方法;换句话说,如果一个结构体实现了某个接口定义的全部方法,那么它就是这个接口的一个具体实现;

Golang 复制代码
type Sequence []int
// sort.Interface 所需的方法。任何类型只要实现了以下三个方法就支持使用 sort.Sort 进行排序
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// 用于打印输出的方法 - 在输出前对元素进行排序,再序列化。
func (s Sequence) String() string {
    sort.Sort(s)
    str := "["
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}
类型转换

上面示例中 type Sequence []int可以看出 Sequence[]int 其实是一样的,所以两者是可以互相转化的,所以上面的排序可以通过 sort.IntSlice 简化

Golang 复制代码
type Sequence []int
// 用于打印输出的方法 - 在输出前对元素进行排序,再序列化。
func (s Sequence) String() string {
	sort.IntSlice(s).Sort() // s 可以自动转化为 []int 类型
	return fmt.Sprint([]int(s))
}
接口转换与类型断言

接口类型转换主要是两个场景,一是判断接口是什么类型,二是将接口转为具体的类型的值

  • 判断接口类型,利用 switch
Golang 复制代码
type Stringer interface {
	String() string
}

var value interface{} // 调用者提供的值。
switch str := value.(type) {
case string:
	return str
case Stringer:
	return str.String()
}
  • 转换具体类型
Golang 复制代码
str, ok := value.(string)
if ok {
	fmt.Printf("字符串值为 %q\n", str)
} else {
	fmt.Printf("该值非字符串\n")
}
接口的通用性

这个跟其他语言的接口目的其实是一致;接口可以使我们更加关注其行为而非实现,并且对于实现了同样接口的类型,可以做到快速替换;

接口和方法

由于几乎任何类型都能添加方法,因此几乎任何类型都能满足一个接口。一个很直观的例子就是 http 包中定义的 Handler 接口。任何实现了 Handler 的对象都能够处理 HTTP 请求。这里不再赘述。

12. 空白描述符

定义:空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。它有点像 Unix 中的 /dev/null 文件:它表示只写的值,在需要变量但不需要实际值的地方用作占位符,在 Golang 中使用 - 来表示。

多重赋值中的空白标识符

比如函数返回多个有效值,其中某些值并不被使用,可以使用空白描述符,占位:

Golang 复制代码
if _, err := os.Stat(path); os.IsNotExist(err) {
	fmt.Printf("%s does not exist\n", path)
}

但是永远不要丢弃 err

Golang 复制代码
// 如果路径不存在,那么这个程序会 panic
fi, _ := os.Stat(path)
if fi.IsDir() {
	fmt.Printf("%s is a directory\n", path)
}
未使用的导入或者变量(通常用不到)
Golang 复制代码
package main

import (
    "fmt"
    "io"
    "log"
    "os"
)
// 如果不加入下面的声明,fmt 包被导入就会被提示错误
var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。
var _ io.Reader    // For debugging; delete when done. // 用于调试,结束时删除。

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}
为副作用(side effect)而导入

这个翻译很让人难以理解,但是目前都是用的这个词,具体就是导入某个库后,库里可能定义了 init 函数,会自动被执行,比如 pprof 库

Golang 复制代码
import _ "net/http/pprof"
接口检查
Golang 复制代码
if _, ok := val.(json.Marshaler); ok {
	fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

13. 内嵌

Golang 中没有继承,子类父类直说,都是通过内嵌实现类似特性的,比如

接口内嵌
Golang 复制代码
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}
// ReadWriter 接口结合了 Reader 和 Writer 接口。
type ReadWriter interface {
	Reader
	Writer
}
结构体内嵌
Golang 复制代码
package main

import "fmt"

type Reader struct {
	A string
}

func (r *Reader) Read() {
	fmt.Println("==== read", r.A)
}

type Writer struct {}
func (r *Reader) Write() {
	fmt.Println("==== write")
}

type WriterReader struct {
	*Reader
	*Writer
}

func main() {
	wr := WriterReader{
                // 特别注意,如果未对指针进行初始化,那么该值相当于nil,如果调用读取内部变量的方法会引起 panic
		Reader: &Reader{
			A: "a",
		},
	}
	wr.Reader.Read()    // 如果不初始化 Reader 变量,调用就会 panic
	wr.Writer.Write()   // 这个无论是否初始化,都不会 panic
}

当然上面的 WriterReader 结构体实现并不符合ReadWriter接口的定义,如果要满足其接口定义,则需要提升 Read和Write 方法到 WriterReader 结构体

golang 复制代码
type ReadWriter struct {
	reader *Reader
	writer *Writer
}
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
	return rw.reader.Read(p)
}

注意,对于内嵌结构体,你也可以直接调用其方法:

Golang 复制代码
    wr.Read()    // 完全可以直接调用
    wr.Write() 
    
// 另外一个示例    
type Job struct {
	Command string
	*log.Logger
}

job.Log("starting now...")
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...)) // 也可以使用内嵌名这样调用,直接忽略包限定名即可
命名冲突

如果内嵌的结构体中与外层变量或者方法存在冲突,则外层的会直接覆盖内嵌结构体内的,比如 Job 自己也定义了一个 Log 方法,那么 job.Log 引用的一定是自己定义的方法,当然如果 Job 自己又定义了一个 Logger 变量,那就会出现重复定义错误了,无法完成编译。

从国庆拖到了元旦,总算是又推进了一些,还剩几部分内容比较多的,争取元旦完成~

相关推荐
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
背水2 小时前
初识Spring
java·后端·spring
晴天飛 雪2 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590452 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端
AskHarries3 小时前
Spring Cloud Gateway快速入门Demo
java·后端·spring cloud
Qi妙代码3 小时前
MyBatisPlus(Spring Boot版)的基本使用
java·spring boot·后端
宇宙超级勇猛无敌暴龙战神3 小时前
Springboot整合xxl-job
java·spring boot·后端·xxl-job·定时任务
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑3 小时前
SpringBoot(五)
java·spring boot·后端