十分钟速读 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 变量,那就会出现重复定义错误了,无法完成编译。

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

相关推荐
千|寻4 分钟前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱18 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯24 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响27 分钟前
枚举在实际开发中的使用小Tips
后端
wuhunyu32 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi32 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员
苏三说技术2 小时前
给你1亿的Redis key,如何高效统计?
后端
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql
程序员清风2 小时前
阿里二面:Kafka 消费者消费消息慢(10 多分钟),会对 Kafka 有什么影响?
java·后端·面试