diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5ccf29a Binary files /dev/null and b/.DS_Store differ diff --git "a/Go\350\257\255\350\250\200/Go\350\257\255\350\250\200\345\255\246\344\271\240\346\214\207\345\214\227.md" "b/Go\350\257\255\350\250\200/Go\350\257\255\350\250\200\345\255\246\344\271\240\346\214\207\345\214\227.md" new file mode 100644 index 0000000..6691ce3 --- /dev/null +++ "b/Go\350\257\255\350\250\200/Go\350\257\255\350\250\200\345\255\246\344\271\240\346\214\207\345\214\227.md" @@ -0,0 +1,123 @@ +学习资料汇总 +初稿都是收集于iwiki,取之于民,用之于民。 后续会持续补充网络上找到的资料。 + +重要的不是资料,而是马上开始! + +汇总文档(太长不看,就先看这) +iwiki:go语言汇总文档 +《go语言圣经》 +知乎优秀问答:怎么学习 Golang? +知乎优秀go语言学习资料汇总:https://zhuanlan.zhihu.com/p/25493806 +https://golangbot.com/learn-golang-series/ +GO语言Web应用入门:https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/preface.md +《go入门指南》:https://github.com/unknwon/the-way-to-go_ZH_CN +go指南(不需要部署环境,可以直接写go代码,感受下go语言的基础语法):https://tour.go-zh.org/flowcontrol/6 +《go简明教程》 +给Java开发者转go的指南:https://yourbasic.org/golang/go-java-tutorial/ +go语言标准库-中文版(英文好的可以直接看原版):http://cngolib.com/ +《Go需要避免踩的50个坑》 +go比较优秀的开源三方框架:https://github.com/avelino/awesome-go +谷歌Go语言官方规范:The Go Programming Language Specification +资料明细 +在线书籍 +《go语言圣经》 +《go语言设计与实现》 +《go高级编程》 +《go web 编程》 +《go语言入门》 +《go并发》 +《go语言中文文档》 +《Go语言标准库》 +一些文章 +一些实例:https://learnku.com/docs/gobyexample/2020 +gin实践分析: https://www.jishuchi.com/read/gin-practice/3824  + +https://geektutu.com/post/hpg-concurrency-control.html + +https://geektutu.com/post/high-performance-go.html + +GO语言最佳实践 +Golang单元测试指引:http://km.oa.com/group/47322/articles/show/439416?kmref=search&from_page=1&no=1 +GO 调用模型&&内存模型: +Go 为什么这么“快”: http://km.oa.com/group/39344/articles/show/413042  +深入Golang Runtime系列之内存与内存分配: http://km.oa.com/group/19253/articles/show/371529 +tRPC-Go:http://km.oa.com/articles/show/441717?kmref=search&from_page=1&no=1 +gomod 环境配置:5分钟搞定go module开发配置#report +学习网站 +golang github官方仓库 + +go 官方文档 + +Go 语言学习资料与社区索引 + +go技术论坛 +go语言中文网 +代码规范 +腾讯go代码规范 + +腾讯go编码安全规范 + +官方go CR代码规范 + +protobuf规范:https://git.woa.com/standards/protobuf +go语言学习分享 +Go语言学习分享PPT +Go语言学习参考资料 +iwiki文章1:Go学习指南 + + +参考的iwiki文章 +参考资料1:go语言学习 + +参考资料2:学习小组 + +参考资料3:go语言学习指引 + + + +Go语言分类学习资料 +基础 +环境安装 +module +KM文章:Golang 模块版本管理机制合辑 + + +并发 +http://blog.xiayf.cn/2015/05/20/fundamentals-of-concurrent-programming/ +web开发 + + +指针 + + +垃圾回收 + + +Go个人学习笔记系列 + + +《Go入门常见错误》 + + +《Go语言入门指南》笔记 +《Go语言入门指南》基础·1694247520 + +《Go 圣经学习》笔记 + + +《Go语言核心编程》笔记 +《Go语言核心编程》 + +《Go语言核心36讲》笔记 +《Go语言核心36讲》课程学习笔记 + +《Go Web 入门》笔记 +《Go Web开发入门》 + +《Go 单测入门》笔记 +《Go 单元测试入门》 + +《Go RPC&Protobuf入门》笔记 + + +《Go 语言规范》汇总 \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go Module\344\270\200\346\226\207\346\220\236\345\256\232\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go Module\344\270\200\346\226\207\346\220\236\345\256\232\343\200\213.md" new file mode 100644 index 0000000..45421c3 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go Module\344\270\200\346\226\207\346\220\236\345\256\232\343\200\213.md" @@ -0,0 +1,204 @@ +[TOC] +# Go Module 那些道道 + +## 导入本地 module +- 可以借助 go.mod 的 replace 指示符,来解决这个问题。 + - 首先,我们需要在 module a 的 go.mod 中的 require 块中,手工加上这一条: + ```go + //这里的 v1.0.0 版本号是一个“假版本号”,目的是满足 go.mod 中 require 块的语法要求。 + require github.com/user/b v1.0.0 + ``` + - 然后,我们再在 module a 的 go.mod 中使用 replace,将上面对 module b v1.0.0 的依赖,替换为本地路径上的 module b: + ```go + replace github.com/user/b v1.0.0 => module b的本地源码路径 + ``` + + +## 拉取私有 module 的需求与参考方案 + +- 配置公共 GOPROXY 服务拉取公共 Go Module,同时再把私有仓库配置到 GOPRIVATE 环境变量,就可以了。 + +- 这样,所有私有 module 的拉取,都会直连代码托管服务器,不会走 GOPROXY 代理服务,也不会去 GOSUMDB 服务器做 Go 包的 hash 值校验。 + + +更多的公司 / 组织,可能会将私有 Go Module 放在公司 / 组织内部的 vcs(代码版本控制)服务器上。 一般有两个方案: +- 第一个方案,通过直连组织公司内部的私有 Go Module 服务器拉取。 + >十分适合内部有完备 IT 基础设施的公司。这类型的公司内部的 vcs 服务器都可以通过域名访问(比如 git.yourcompany.com/user/repo),因此,公司内部员工可以像访问公共 vcs 服务那样,访问内部 vcs 服务器上的私有 Go Module。 +- 第二种方案,将外部 Go Module 与私有 Go Module 都交给内部统一的 GOPROXY 服务去处理: + >可以将所有复杂性都交给 in-house goproxy 这个节点,开发人员可以无差别地拉取公共 module 与私有 module,心智负担降到最低。 + +## 如何升级修改版本 + +- **查看版本**: `o list -m -versions github.com/sirupsen/logrus` +- **修改版本**: + - 方法1: 以在项目的 module 根目录下,执行带有版本号的 go get 命令:`go get github.com/sirupsen/logrus@v1.7.0` + - 方法2: 修改go.mod文件,然后tidy一下: + - `go mod edit -require=github.com/sirupsen/logrus@v1.7.0` + - `go mod tidy` + + +### 如何添加主版本号大于1的依赖(在代码中import) +在 Go Module 构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在 Go 源码中导入依赖包,**不需要在包的导入路径上增加版本号**,也就是: + +```go +import github.com/user/repo/v0 等价于 import github.com/user/repo +import github.com/user/repo/v1 等价于 import github.com/user/repo +``` + +- 如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。 反过来说,如果不兼容,那剧需要采用不同的导入路径。 + + +如果引入的主版本大于1的依赖(比如v2.0.0),那么就不能直接使用`github.com/user/repo`,因为这是默认0/1,这个与2是不兼容的。 需要向下面这样导入: +```go +import github.com/user/repo/v2/xxx +``` + - 也就是在声明它的导入路径的基础上,加上版本号信息。 + - 然后要从新下载最新的:`go get github.com/go-redis/redis/v7` + +### 升级依赖版本到一个不兼容版本 + +跟上面类似,修改版本号,然后重新下载。 +```go + +import ( + _ "github.com/go-redis/redis/v8" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +// +$go get github.com/go-redis/redis/v8 +``` + +### 移除一个依赖 + +- 在业务代码中删除依赖后,直接build不会删除不用的依赖,因为如果源码满足成功构建的条件,go build 命令是不会“多管闲事”地清理 go.mod 中多余的依赖项的。 +- 运行下 `go mod tidy`就行, go mod tidy 会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。 + + +### 特殊情况:使用vendor + +**什么情况下还需要用vendor?** + + +- 在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境,比如在一些内部的持续集成或持续交付环境(CI/CD)中,使用 vendor 机制可以实现与 Go Module 等价的构建。 + + +**怎么用mod模式下用vendor?** + +- Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令: + - `go mod vendor` 项目根目录,创建vendor目录。 + - go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本 + - 并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。 +- 如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上 `-mod=vendor` 参数。 +- 在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build **默认也会优先基于 vendor 构建**,除非你给 go build 传入 -mod=mod 的参数。 + + +>go get会下载gotests下面所有的包,如果gotests是一个可执行文件的项目(带有main包main函数). go get会在下载包之后构建这个项目并把可执行文件放入$GOPATH/bin下。 + + +## 作为module维护者,你需要知道的事情 + +>从 Go Module 的作者或维护者的视角,来聊聊在规划、发布和维护 Go Module 时需要考虑和注意什么事情,包括 go 项目仓库布局、Go Module 的**发布**、**升级 module 主版本号**、作废特定版本的 module. + + +**仓库布局:是单module耗时多module** + +- 能单就单。 管理方便,导入时也方便。 + - 然后我们对仓库打 tag,这个 tag 也会成为 Go Module 的版本号,这样,对仓库的版本管理其实就是对 Go Module 的版本管理。 + + +如果组织层面要求采用单一仓库(monorepo)模式,也就是所有 Go Module 都必须放在一个 repo 下,那我们只能使用单 repo 下管理多个 Go Module 的方法了。 就是所谓的 **大仓**。 + +例如: +```go +./srmm +├── module1 +│ ├── go.mod +│ └── pkg1 +│ └── pkg1.go +└── module2 + ├── go.mod + └── pkg2 + └── pkg2.go +``` +- 这种情况下,module 的 path 也不能随意指定,必须包含子目录的名字。 +- 如果我们要发布 module1 的 v1.0.0 版本,我们不能通过给仓库打 v1.0.0 这个 tag 号来发布 module1 的 v1.0.0 版本,正确的作法应该是打 module1/v1.0.0 这个 tag 号。 + + +**发布Go Module** + + +- 发布的步骤也十分简单,就是为 repo 打上 tag 并推送到代码服务器上就好了。 + - 单module,给 repo 打的 tag 就是 module 的版本。 + - 多module,在 tag 中加上各个 module 的子目录名,这样才能起到发布某个 module 版本的作用,否则 module 的用户通过 go get xxx@latest 也无法看到新发布的 module 版本。 + + +**作废特定版本的Go Module** + + +- 修复 broken 的 module 版本并重新发布。 + - m1 的作者只需要删除掉远程的 tag: v1.0.2, + - 在本地 fix 掉问题, + - 然后重新 tag v1.0.2 并 push 发布到 bitbucket 上的仓库中就可以了。 +- 如果m1所有的消费者,都是通过m1所在代码托管服务器来获取m1的特定版本,那么只要清理掉本地缓存module cache(`go clean -modcache`),然后再重新构建就可以了. +- 但现实的情况时,现在大家都是通过 Goproxy 服务来获取 module 的。 + - 当某个消费者通过他配置的 goproxy 获取这个版本时,这个版本就会在被缓存在对应的代理服务器上。 + - 后续 m1 的消费者通过这个 goproxy 服务器获取那个版本的 m1 时,请求不会再回到 m1 所在的源代码托管服务器。 +- 如果 m1 的作者删除了 bitbucket 上的 v1.0.2 这个发布版本,各大 goproxy 服务器上的 broken v1.0.2 版本是否也会被同步删除呢? **不会**。 + + +**那怎么解决?** +>Go 社区更为常见的解决方式就是**发布 module 的新 patch 版本**. + + +现在我们废除掉 v1.0.2,在本地修正问题后,直接打 v1.0.3 标签,并发布 push 到远程代码服务器上。 + +- 重新拉取最新的会获得v1.0.3, 但是对于之前曾依赖 v1.0.2 版本的消费者 c2 来说,这个时候他们需要手工介入才能解决问题。 + + +从 Go 1.16 版本开始,Go Module 作者还可以在 go.mod 中使用新增加的retract 指示符,标识出哪些版本是作废的且不推荐使用的。retract 的语法形式如下: +```go + +// go.mod +retract v1.0.0 // 作废v1.0.0版本 +retract [v1.1.0, v1.2.0] // 作废v1.1.0和v1.2.0两个版本 +``` + + +如果要提示用某个 module 的某个大版本整个作废,我们用 Go 1.17 版本引入的 Deprecated 注释行更适合。下面是使用 Deprecated 注释行的例子: +```go +// Deprecated: use bitbucket.org/bigwhite/m1/v2 instead. +module bitbucket.org/bigwhite/m1 +``` + +### 升级 module 的 major 版本号 + +- 在同一个 repo 下,不同 major 号的 module 就是完全不同的 module,甚至同一 repo 下,不同 major 号的 module 可以相互导入。 +- 这意味着高版本的代码要与低版本的代码彻底分开维护,通常 Go 社区会采用为新的 major 版本建立新的 major 分支的方式,来将不同 major 版本的代码分离开。 + + +以将 bitbucket.org/bigwhite/m1 的 major 版本号升级到 v2 为例看看。 + +- 首先,我们要建立 v2 代码分支并切换到 v2 分支上操作 +- 然后修改 go.mod 文件中的 module path,增加 v2 后缀: +```go +//go.mod +module bitbucket.org/bigwhite/m1/v2 + +go 1.17 +``` + - 如果module内部包间有互相导入,那么在升级major号时,这些包的 import 路径上也要增加 v2。 否则就会出现高major号的module引用低module。 +- 使用者:需要在这个依赖 module 的 import 路径的后面,增加 /vN 就可以了(这里是 /v2),当然代码中也要针对不兼容的部分进行修改,然后 go 工具就会自动下载相关 module。 + + +**多module的情况下升级major版本号?** +分两种情况: + +- 第一种情况:repo 下的所有 module 统一进行版本发布。 + - 建立 vN 版本分支,在 vN 分支上对 repo 下所有 module 进行演进,统一打 tag 并发布。 + - 当然 tag 要采用带有 module 子目录名的那种方式,比如:module1/v2.0.0。 +- 第二个情况:repo 下的 module 各自独立进行版本发布。 + - 需要建立 major 分支矩阵。 + - 假设我们的一个 repo 下管理了多个 module,从 m1 到 mN,那么 major 号需要升级时,我们就需要将 major 版本号与 module 做一个组合,形成下面的分支矩阵: v2_m1/v2_m2/v3_m1 + diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go Protobuf\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go Protobuf\343\200\213.md" new file mode 100644 index 0000000..dbb3adc --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go Protobuf\343\200\213.md" @@ -0,0 +1,257 @@ + +# 简介 + +- protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。 +- protobuf **性能和效率大幅度优于** JSON、XML 等其他的结构化数据格式。 +- protobuf 是以**二进制方式存储**的,占用空间小,但也带来了**可读性差**的缺点。 + + +protobuf 在通信协议和数据存储等领域应用广泛。 +>例如著名的分布式缓存工具 Memcached 的 Go 语言版本**groupcache** 就使用了 protobuf 作为其 RPC 数据格式。 + + +Protobuf 在 .proto 定义需要处理的结构化数据,可以通过 protoc 工具,将 .proto 文件转换为 C、C++、Golang、Java、Python 等多种语言的代码,兼容性好,易于使用。 + +## 安装 +**protoc** +- 从 Protobuf Releases 下载最先版本的发布包安装。 mac选all版本 +- 解压到 /usr/local 目录下,可以解压到其他的其他,并把解压路径下的 bin 目录 加入到环境变量即可。 +>protoc --version 可以看版本号 + + +**protoc-gen-go** +在 Golang 中使用 protobuf,还需要安装 protoc-gen-go,这个工具用来将 .proto 文件转换为 Golang 代码。 + +>go get -u github.com/golang/protobuf/protoc-gen-go + +protoc-gen-go 将自动安装到 $GOPATH/bin 目录下,也需要将这个目录加入到环境变量中。 + + +## 定义消息类型 +student.proto +```go +// protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = "proto3" 标明版本。 +syntax = "proto3"; +// 包名声明符是可选的,用来防止不同的消息类型有命名冲突。 +package main; + +// 消息类型 使用 message 关键字定义,Student 是类型名 +message Student{ + //name, male, scores 是该类型的 3 个字段,类型分别为 string, bool 和 []int32。 + //字段可以是标量类型,也可以是合成类型。 + string name = 1; + //每个字符 =后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。 + //标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] + bool male = 2; + // 每个字段的修饰符默认是 singular,一般省略不写,repeated 表示字段可重复,即用来表示 Go 语言中的数组类型。 + repeated int32 scores = 3; +} +``` +- 一个 .proto 文件中可以写多个消息类型,即对应多个结构体(struct)。 + + +在proto文件所在目录执行生成命令: +>protoc --go_out=. *.proto + +>有报错:`unable to determine Go import path for "student.proto"`,大概意思是找不到该文件编译出来的golang文件的导出目录。我们在.proto文件中加上如下一行: +>option go_package="." # 意思是输出到当前目录 + + +会生成一个 Go 文件 `student.pb.go`。这个文件内部定义了一个结构体 Student: +```go +package main + +type Student struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Male bool `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"` + Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"` +} +``` + + +接下来,就可以在项目代码中直接使用了,以下是一个非常简单的例子,即证明被序列化的和反序列化后的实例,包含相同的数据。 + +```go +// 在同一个包里,就可以直接引用 +test := &Student{ + Name: "txbobo", + Male: true, + Scores: []int32{98, 86, 67}, + } + data, err := proto.Marshal(test) + if err != nil { + log.Fatal("marshaling error: ", err) + } + newTest := &Student{} + err = proto.Unmarshal(data, newTest) + if err != nil { + log.Fatal("unmarshaling error: ", err) + + } + // GetName是自动生成的方法 + if test.GetName() != newTest.GetName() { + + log.Fatal("data mismatch %q != %q", test.GetName(), newTest.GetName()) + } +``` + + +**保留字段(Reserved Field)** +更新消息类型时,可能会将某些字段/标识符删除。 +这些被删掉的字段/标识符可能被重新使用,如果加载老版本的数据时,可能会造成数据冲突,在升级时,可以将这些字段/标识符保留(reserved),这样就不会被重新使用了,protoc 会检查。 + + +## 字段类型 + +### 标量类型(Scalar) +| proto类型 | go类型 | 备注 | proto类型 | go类型 | 备注 | +|----------|---------|-------------------|----------|---------|--------------------| +| double | float64 | | float | float32 | | +| int32 | int32 | | int64 | int64 | | +| uint32 | uint32 | | uint64 | uint64 | | +| sint32 | int32 | 适合负数 | sint64 | int64 | 适合负数 | +| fixed32 | uint32 | 固长编码,适合大于2^28的值 | fixed64 | uint64 | 固长编码,适合大于2^56的值 | +| sfixed32 | int32 | 固长编码 | sfixed64 | int64 | 固长编码 | +| bool | bool | | string | string | UTF8 编码,长度不超过 2^32 | +| bytes | []byte | 任意字节序列,长度不超过 2^32 | + + +标量类型如果没有被赋值,则不会被序列化,解析时,会赋予默认值。 +* strings:空字符串 +* bytes:空序列 +* bools:false + + +### 枚举(Enumerations) +枚举类型适用于提供一组预定义的值,选择其中一个。例如我们将性别定义为枚举类型。 +```go +message Student { + string name = 1; + enum Gender { + FEMALE = 0; + MALE = 1; + } + Gender gender = 2; + repeated int32 scores = 3; +} +``` +- 枚举类型的**第一个**选项的**标识符必须是0**,这也是枚举类型的**默认值**。 +- 别名(Alias),允许为不同的枚举值赋予相同的标识符,称之为别名,需要打开allow_alias选项。 作用是啥? +```go + message EnumAllowAlias { + enum Status { + option allow_alias = true; + UNKOWN = 0; + STARTED = 1; + RUNNING = 1; + } +} + +``` + + +### 使用其他消息类型 +`Result`是另一个消息类型,在 SearchReponse 作**为一个消息字段类型使用**。 +```go +message SearchResponse { + repeated Result results = 1; +} + +message Result { + string url = 1; + string title = 2; + repeated string snippets = 3; +} +``` + +- 可以嵌套写: +``` +message SearchResponse { + message Result { + string url = 1; + string title = 2; + repeated string snippets = 3; + } + repeated Result results = 1; +} +``` + + +- 如果定义在其他文件中,可以导入其他消息类型来使用: + >import "myproject/other_protos.proto"; + + +### 任意类型(Any) +Any 可以表示不在 .proto 中定义任意的内置类型。 啥用处? +```go +import "google/protobuf/any.proto"; + +message ErrorStatus { + string message = 1; + repeated google.protobuf.Any details = 2; +} +``` + +- oneof: +```go +message SampleMessage { + oneof test_oneof { + string name = 4; + SubMessage sub_message = 9; + } +} +``` + +- map +```go +message MapRequest { + map points = 1; +} + +``` + + + + +## 定义服务(Services) +如果消息类型是用来远程通信的(Remote Procedure Call, RPC),可以在 .proto 文件中定义 RPC 服务接口。 + + +例如我们定义了一个名为 SearchService 的 RPC 服务,提供了 Search 接口,入参是 SearchRequest 类型,返回类型是 SearchResponse。 +``` +ervice SearchService { + rpc Search (SearchRequest) returns (SearchResponse); +} +``` + + +## protooc其他参数 +命令行使用方法 +>protoc --proto_path=IMPORT_PATH --_out=DST_DIR path/to/file.proto + +- --proto_path=IMPORT_PATH:可以在 .proto 文件中 **import 其他的 .proto 文件**,proto_path 即用来指定其他 .proto 文件的查找目录。 + >如果没有引入其他的 .proto 文件,该参数可以省略。 + +- --_out=DST_DIR:指定生成代码的目标文件夹。 + >例如 –go_out=. 即**生成 GO 代码在当前文件夹**, + >另外支持 cpp/java/python/ruby/objc/csharp/php 等语言 + + +## 推荐风格 +- 文件(Files) + * 文件名使用小写下划线的命名风格,例如 lower_snake_case.proto + * 每行不超过 80 字符 + * 使用 2 个空格缩进 + * 字符串最好用双引号 +* 包 + * 包名应该和目录结构对应,比如文件在hello/protobuf/目录下,包名应为 hello.protobuf +* 消息和字段 + * 消息名使用首字母大写驼峰风格 + * 字段名使用小写下划线风格 + * 枚举类型使用首字母大写驼峰风格。 枚举值使用全大写下划线隔开风格 + * 对重复字段使用复数名称:repeated string keys = 1; +* 服务 + * RPC服务名和方法名,均使用首字母大写驼峰。 + + +## \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go RPC\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go RPC\343\200\213.md" new file mode 100644 index 0000000..6acc780 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go RPC\343\200\213.md" @@ -0,0 +1,280 @@ + +# Go RPC & TLS 鉴权简明教程 +Go 语言远程过程调用(Remote Procedure Call, RPC)的使用方式,示例基于 Golang 标准库 net/rpc,同时介绍了如何基于 TLS/SSL 实现服务器端和客户端的单向鉴权、双向鉴权。 + +## RPC简介 +>远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。 +- 该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而**程序员就像调用本地程序一样,无需额外地为这个交互作用编程**(无需关注细节)。 +- RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。 +- RPC 协议假定某种传输协议(TCP, UDP)存在,为通信程序之间携带信息数据。 + + +- 相比于调用本地的接口,RPC 还需要知道的是服务器端的地址信息。 + + +## 一个简单的Demo +计算二次方的程序: +- Cal 结构体,提供了 Square 方法,用于计算传入参数 num 的 二次方。 + + +**本地调用版本**: + + +```go +// main.go +package main + +import "log" + +type Result struct { + Num, Ans int +} + +type Cal int + +func (cal *Cal) Square(num int) *Result { + return &Result{ + Num: num, + Ans: num * num, + } +} + +func main() { + cal := new(Cal) + result := cal.Square(12) + log.Printf("%d^2 = %d", result.Num, result.Ans) +} +``` + + +**RPC需要满足什么条件?** +- 有一定的约束和规范,Golang标准库中的`net/rpc`的方法需要长下面这样: + +```go +func (t *T) MethodName(argType T1, replyType *T2) error +``` + +即需要满足以下 5 个条件: +1. 方法类型(T)是导出的(首字母大写) +2. 方法名(MethodName)是导出的 +3. 方法有2个参数(argType T1, replyType *T2),均为导出/内置类型 + 1. net/rpc 对参数个数的限制比较严格,仅能有2个, + 2. 第一个参数是调用者提供的请求参数, + 3. 第二个参数是返回给调用者的响应参数 +4. 方法的第2个参数一个指针(replyType *T2) +5. 方法的返回值类型是 error + + +**满足RPC条件的版本** + + +```go +func (cal *Cal) Square(num int, result *Result) error { + result.Num = num + result.Ans = num * num + return nil +} + +func main() { + cal := new(Cal) + var result Result + cal.Square(15, &result) + log.Printf("%d^2 = %d", result.Num, result.Ans) +} +``` +* Cal 和 Square 均为导出类型,满足条件 1) 和 2) +* 2 个参数,num int 为内置类型,result *Result 为导出类型,满足条件 3) +* 第2个参数 result *Result 是一个指针,满足条件 4) +>方法 Cal.Square 满足了 RPC 调用的5个条件。可以用rpc进行改造了。 + + +## RPC服务与调用 + + +### 基于HTTP,启动RPC服务 +RPC是典型的CS架构:需要将 Cal.Square 方法放在服务端。 +- 服务端需要提供一个套接字服务,处理客户端发送的请求。通常可以基于 HTTP 协议,监听一个端口,等待 HTTP 请求。 + + +**服务端Server** +```go +type Result struct { + Num, Ans int +} + +type Cal int + +func (cal *Cal) Square(num int, result *Result) error { + result.Num = num + result.Ans = num * num + return nil +} +func main() { + // 发布 Cal 中满足 RPC 注册条件的方法(Cal.Square) + rpc.Register(new(Cal)) + // 注册用于处理 RPC 消息的 HTTP Handler + rpc.HandleHTTP() + + // 监听 1234 端口,等待 RPC 请求。 + log.Printf("Serving RPC server on port %d", 1234) + if err := http.ListenAndServe(":1234", nil); err != nil { + log.Fatal("Error serving: ", err) + } +} +``` +- RPC 服务启动,等待客户端的调用。 + + + +**客户端Client** + + +```go +type Result struct { + Num, Ans int +} + +func main() { + // 创建了 HTTP 客户端 client,并且创建了与 localhost:1234 的链接,1234 恰好是 RPC 服务监听的端口 + client, _ := rpc.DialHTTP("tcp", "localhost:1234") + var result Result + // 调用远程方法,第1个参数是方法名 Cal.Square,后两个参数与 Cal.Square 的定义的参数相对应。 + if err := client.Call("Cal.Square", 12, &result); err != nil { + log.Fatal("Failed to call Cal.Square.", err) + } + log.Printf("%d^2 = %d", result.Num, result.Ans) +} +``` +- rpc.Call 调用远程方法,**第1个参数是方法名** Cal.Square,后两个参数与 Cal.Square 的定义的参数相对应。 + + +## 异步调用 +- `client.Call` 是同步调用的方式,会阻塞当前的程序,直到结果返回。 +- `client.Go`: 是异步调用,客户端调用后,程序可以继续往下走。 +```go + // 创建了 HTTP 客户端 client,并且创建了与 localhost:1234 的链接,1234 恰好是 RPC 服务监听的端口 + client, _ := rpc.DialHTTP("tcp", "localhost:1234") + var result Result + // 异步调用 + asyncCall := client.Go("Cal.Square", 12, &result, nil) + + log.Printf("%d^2 = %d", result.Num, result.Ans) // 2020/01/13 21:34:26 0^2 = 0 + + // 阻塞当前程序直到 RPC 调用结束 + <-asyncCall.Done + log.Printf("%d^2 = %d", result.Num, result.Ans) // 2020/01/13 21:34:26 12^2 = 144 +} +``` + + +## 证书鉴权(TLS/SSL) + +### 客户端对服务器端鉴权 +- HTTP 协议默认是不加密的,我们可以使用证书来保证通信过程的安全。 + +- 生成私钥和自签名的证书,并将 server.key 权限设置为只读,保证私钥的安全。 + + +**生成私钥和自签名的证书** +```s +# 生成私钥 +openssl genrsa -out server.key 2048 +# 生成证书 +openssl req -new -x509 -key server.key -out server.crt -days 3650 +# 只读权限 +chmod 400 server.key +``` + + +**服务端可以使用证书启动TLS端口监听** +```go + // 发布 Cal 中满足 RPC 注册条件的方法(Cal.Square) + rpc.Register(new(Cal)) + cert, _ := tls.LoadX509KeyPair("server.pem", "server.key") + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + // 服务器端可以使用生成的 server.crt 和 server.key 文件启动 TLS 的端口监听。 + listener, _ := tls.Listen("tcp", ":1234", config) + log.Printf("Serving RPC server on port %d", 1234) + + /* + listener.Accept() 阻塞等待客户端与服务端建立连接,建立连接后交给 rpc.ServeConn 异步处理。 + 因为可能有多个客户端建立连接,所以需要无限循环, + 每建立一个链接,就异步处理,然后继续等待下一个连接建立。 + */ + for { + conn, _ := listener.Accept() + defer conn.Close() + go rpc.ServeConn(conn) + } +``` + + +**客户端使用tsl.Dial,并将服务端的证书添加到信任证书池中** +```go + certPool := x509.NewCertPool() + certBytes, err := ioutil.ReadFile("/Users/bobo-mac/Documents/code/study/go/src/helloworld/rpc/tls/server/server.pem") + if err != nil { + log.Fatal("Failed to read server.pem") + } + certPool.AppendCertsFromPEM(certBytes) + + config := &tls.Config{ + // 将服务端的证书添加到信任证书池中 + RootCAs: certPool, + } + + conn, _ := tls.Dial("tcp", "localhost:1234", config) + defer conn.Close() + client := rpc.NewClient(conn) + + var result Result + if err := client.Call("Cal.Square", 12, &result); err != nil { + log.Fatal("Failed to call Cal.Square. ", err) + } + + log.Printf("%d^2 = %d", result.Num, result.Ans) +``` + + +### 服务端对客户端的鉴权 +与上面的类似,重点是tls.Config的配置: +- 把对方的证书添加到自己的信任证书池 RootCAs(客户端配置),ClientCAs(服务器端配置) 中。 +- 创建链接时,配置自己的证书 Certificates + + +**客户端** +```go +/ client/main.go + +cert, _ := tls.LoadX509KeyPair("client.crt", "client.key") +certPool := x509.NewCertPool() +certBytes, _ := ioutil.ReadFile("../server/server.crt") +certPool.AppendCertsFromPEM(certBytes) +config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: certPool, +} +``` + + +**服务端** +```go +// server/main.go + +cert, _ := tls.LoadX509KeyPair("server.crt", "server.key") +certPool := x509.NewCertPool() +certBytes, _ := ioutil.ReadFile("../client/client.crt") +certPool.AppendCertsFromPEM(certBytes) +config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, +} + +``` + + +# 参考或者说照搬 +- 极客兔兔:https://geektutu.com/post/quick-go-rpc.html \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go Web\345\205\245\351\227\250\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go Web\345\205\245\351\227\250\343\200\213.md" new file mode 100644 index 0000000..4615520 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go Web\345\205\245\351\227\250\343\200\213.md" @@ -0,0 +1,260 @@ +# 《Go Web开发入门》 + +# Web开发基础 + + +# Web开发框架 + +## 为什么不直接用标准库,必须使用框架? +net/http提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。 + +一些Web开发中简单的需求并**不支持**,需要手工实现。 +* 动态路由:例如hello/:name,hello/*这类的规则。 +* 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。 +* 模板:没有统一简化的HTML机制。 + +框架的核心能力: +* 路由(Routing):将请求映射到函数,支持动态路由。例如'/hello/:name。 +* 模板(Templates):使用内置模板引擎提供模板渲染机制。 +* 工具集(Utilites):提供对 cookies,headers 等处理机制。 +* 插件(Plugin):Bottle本身功能有限,但提供了插件机制。可以选择安装到全局,也可以只针对某几个路由生效。 + +## Go Gin 简明教程 +Gin 是使用 Go/golang 语言实现的 HTTP Web 框架。**接口简洁,性能极高**。截止 1.4.0 版本,包含测试代码,仅14K,其中测试代码 9K 左右,也就是说框架源码仅 5K 左右。 + +### Gin特性 +* **快速**:路由不使用反射,基于Radix树,内存占用少。 + +* **中间件**:HTTP请求,可先经过一系列中间件处理,例如:Logger,Authorization,GZIP等。这个特性和 NodeJs 的 Koa 框架很像。中间件机制也极大地提高了框架的可扩展性。 + +* **异常处理**:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理HTTP请求过程中发生的错误。 + +* **JSON**:Gin可以解析并验证请求的JSON。这个特性对Restful API的开发尤其有用。 + +* **路由分组**:例如将需要授权和不需要授权的API分组,不同版本的API分组。而且分组可嵌套,且性能不受影响。 + +* **渲染内置**:原生支持JSON,XML和HTML的渲染。 + + +### 安装Gin +``` +go get -u -v github.com/gin-gonic/gin +``` +-v:打印出被构建的代码包的名字 +-u:已存在相关的代码包,强行更新代码包及其依赖包 + + +### Hello Gin +```go +package main + +import "github.com/gin-gonic/gin" + +func main() { + //生成了一个实例,这个实例即 WSGI 应用程序 + r := gin.Default() + //声明了一个路由,告诉 Gin 什么样的URL 能触发传入的函数 + r.GET("/", func(c *gin.Context) { + c.String(200, "Hello, Gin") + }) + //让应用运行在本地服务器上,默认监听端口是 _8080_,可以传入参数设置端口(注意要带冒号) + r.Run(":8089") // listen and serve on 0.0.0.0:8080 +} +``` + +### 路由 +路由方法有 GET, POST, PUT, PATCH, DELETE 和 OPTIONS,还有Any,可匹配以上任意类型的请求。 + +- 无参数 +- 解析路径参数 +- 获取Query参数 +- 获取Post参数 +- GET 和 POST 混合 +- Map字典参数 +- 重定向 + +```go +//1. 无参数 + //生成了一个实例,这个实例即 WSGI 应用程序 + r := gin.Default() + //声明了一个路由,告诉 Gin 什么样的URL 能触发传入的函数 + r.GET("/", func(c *gin.Context) { + c.String(200, "Hello, Gin") + }) + + //2. 解析路径参数 + // 匹配 /user/geektutu + //http://localhost:9999/user/geektutu + r.GET("/user/:name", func(c *gin.Context) { + name := c.Param("name") + c.String(http.StatusOK, "Hello %s", name) + }) + // 3. 获取Query参数 + // 匹配users?name=xxx&role=xxx,role可选 + //http://localhost:9999/users?name=Tom&role=student + r.GET("/users", func(c *gin.Context) { + name := c.Query("name") + role := c.DefaultQuery("role", "teacher") + c.String(http.StatusOK, "%s is an %s", name, role) + }) + + // 4. 获取POST参数 + //$ curl http://localhost:9999/form -X POST -d 'username=geektutu&password=1234' + //{"password":"1234","username":"geektutu"} + r.POST("/form", func(c *gin.Context) { + username := c.PostForm("username") + password := c.DefaultPostForm("password", "000000") // 可设置默认值 + + c.JSON(http.StatusOK, gin.H{ + "username": username, + "password": password, + }) + }) + + // 5. GET 和 POST 混合 + //curl "http://localhost:9999/posts?id=9876&page=7" -X POST -d 'username=geektutu&password=1234' + //{"id":"9876","page":"7","password":"1234","username":"geektutu"} + r.POST("/posts", func(c *gin.Context) { + id := c.Query("id") + page := c.DefaultQuery("page", "0") + username := c.PostForm("username") + password := c.DefaultPostForm("password", "000000") // 可设置默认值 + + c.JSON(http.StatusOK, gin.H{ + "id": id, + "page": page, + "username": username, + "password": password, + }) + }) + + //6. Map字典参数 + //curl -g "http://localhost:9999/post?ids[Jack]=001&ids[Tom]=002" -X POST -d 'names[a]=Sam&names[b]=David' + //{"ids":{"Jack":"001","Tom":"002"},"names":{"a":"Sam","b":"David"}} + r.POST("/post", func(c *gin.Context) { + ids := c.QueryMap("ids") + names := c.PostFormMap("names") + + c.JSON(http.StatusOK, gin.H{ + "ids": ids, + "names": names, + }) + }) + + //7. 重定向(Redirect) + // curl -i http://localhost:9999/redirect + // curl "http://localhost:9999/goindex" + r.GET("/redirect", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/index") + }) + + r.GET("/goindex", func(c *gin.Context) { + c.Request.URL.Path = "/" + r.HandleContext(c) + }) + + r.Run(":9999") // listen and serve on 0.0.0.0:8080 +``` + +### 分组路由 +类似Controller层类上面加个统一的前缀 /cpc/ ,后面方法/addCpc、/deleteCpc等。 +- 利用分组路由还可以更好地实现权限控制,例如将需要登录鉴权的路由放到同一分组中去,简化权限控制。 +```go +defaultHandler := func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "path": c.FullPath(), + }) + } + + //group: v1 + v1 := r.Group("/v1") + + { + v1.GET("/posts", defaultHandler) + v1.GET("/series", defaultHandler) + } + + //group: v2 + v2 := r.Group("/v2") + + { + v2.GET("/posts", defaultHandler) + v2.GET("/series", defaultHandler) + } +``` + +### 上传文件 +```go + r := gin.Default() + + // 单个文件 + r.POST("/upload1", func(ctx *gin.Context) { + file, _ := ctx.FormFile("file") + // ctx.SaveUploadedFile(file, dst) + ctx.String(http.StatusOK, "%s uploaded!", file.Filename) + }) + + // 多个文件 + r.POST("/upload2", func(ctx *gin.Context) { + // Muiltpart form + form, _ := ctx.MultipartForm() + files := form.File["upload[]"] + + for _, file := range files { + log.Panicln(file.Filename) + // ctx.SaveUploadedFile(file, dst) + } + ctx.String(http.StatusOK, "%s files up;oaded!", len(files)) + }) + + r.Run(":9999") // listen and serve on 0.0.0.0:8080 +``` + + +**debug的方法**:dlv +使用: +1、dlv debug xxx.go 指定需要debug的文件 +2、进入dlv交互式窗口后,b : 指定断点 +3、r arg 指定运行参数 +4、n 执行一行 +5、c 运行至断点或程序结束 + +### HTML模板(Template) +```go +r.LoadHTMLGlob("templates/*") + + stu1 := &student{Name: "Bob", Age: 12} + stu2 := &student{Name: "Lili", Age: 13} + r.GET("/arr", func(ctx *gin.Context) { + ctx.HTML(http.StatusOK, "arr.tmpl", gin.H{ + "title": "Gin", + "stuArr": [2]*student{stu1, stu2}, + }) + }) +``` +还需要搭配写个arr.tmpl文件: +```html + + + +

hello, {{.title}}

+ {{range $index, $ele := .stuArr }} +

{{ $index }}: {{ $ele.Name }} is {{ $ele.Age }} years old

+ {{ end }} + + +``` + +- Gin默认使用模板Go语言标准库的模板text/template和html/template,语法与标准库一致,支持各种复杂场景的渲染。 +- 参考官方文档[text/template](https://golang.org/pkg/text/template/),[html/template](https://golang.org/pkg/html/template/) + + +### 中间件 + + +### 热加载调试 Hot Reload +fresh: go get -v -u github.com/pilu/fresh + +**使用:** fresh run main.go + +# 开发实践 \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go \345\205\245\351\227\250\345\270\270\350\247\201\351\224\231\350\257\257\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go \345\205\245\351\227\250\345\270\270\350\247\201\351\224\231\350\257\257\343\200\213.md" new file mode 100644 index 0000000..29f121f --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go \345\205\245\351\227\250\345\270\270\350\247\201\351\224\231\350\257\257\343\200\213.md" @@ -0,0 +1,157 @@ +[TOC] +# 基础 + +## 基础类型 + +### 切片 + +**长度和容量的区别** +- 切片的长度就是它所包含的元素个数。 +- 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。 + + +**用make创建切片** +- 第二个参数指定长度;指定它的容量,需向 make 传入第三个参数 +```go +a := make([]int, 5) // len(a)=5 +b := make([]int, 0, 5) // len(b)=0, cap(b)=5 + +b = b[:cap(b)] // len(b)=5, cap(b)=5 +b = b[1:] // len(b)=4, cap(b)=4 +``` + + + + + +#### range遍历 +- 只有一个值时,**默认取的是index而不是value**。 建议使用` _, v := range arr` +```go +for i := range arrs { + fmt.Printf("i:%d\n", i) + v += i +} +``` + +## 声明和定义 + +### make 和 new的区别 +- **相同点**:都在堆上分配内存,但它们行为不同,适用于不同类型。 + +**不同点**: +- new() + - 为值类型分配内存 + - new(T)为每个新的类型T分配一片内存,初始化为0并且返回类型为*T的内存地址 • 适用于值类型如数组、结构体 +- make() + * 为引用类型分配内存并初始化,返回的是类型本身,因为其就是引用类型 + * make(T)返回一个类型为T的初始值 + * 只适用于3种内建的引用类型:切片、map 、 channel + + + +### 常见的坑 +- 不能用简短声明方式来单独为一个变量重复声明, := 左侧至少有一个新变量,才允许多变量的重复声明: + + +**不小心覆盖了变量** +- 对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。 +- 如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作: + +```go +func main() { + x := 1 + println(x) // 1 + { + println(x) // 1 + x := 2 + println(x) // 2 // 新的 x 变量的作用域只在代码块内部 + } + println(x) // 1 +} +``` + +- 复制代码这是 Go 开发者常犯的错,而且不易被发现。 +- 可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用: +> go tool vet -shadow main.go +> main.go:9: declaration of "x" shadows declaration at main.go:5 +- 注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测: +> $GOPATH/bin/go-nyet main.go +> main.go:10:3:Shadowing variable `x` + + + + +## 打印语句Printf的格式转换 +- 不同类型打印值 + - `%s` 字符串 + - `%d` 十进制整数 + - `%x, %o, %b` 十六进制,八进制,二进制整数。 + - `%f, %g, %e` 浮点数: 3.141593 3.141592653589793 3.141593e+00 + * `%t` 布尔:true或false + * `%c` 字符(rune) (Unicode码点) + * `%s` 字符串 +* 常用特别 + * `%v` 变量的自然形式(natural format) + * `%T` 变量的类型 + * `#` + * 带`+` 输出字段名 +* 其他不常用 + * `%q` 带双引号的字符串"abc"或带单引号的字符'c' + * `%%` 字面上的百分号标志(无操作数) + * `%*s` 其中的*会在字符串之前填充**一些空格**。`fmt.Printf("%*s\n", depth*2, "", n.Data)` + + +**其他说明** +- 后缀f指format,ln指line。 + - 以字母`f`结尾的格式化函数: 如`log.Printf`和`fmt.Errorf`,都采用fmt.Printf的格式化准则。 + - 以`ln`结尾的格式化函数: 则遵循Println的方式,以跟`%v`差不多的方式格式化参数,并在最后添加一个换行符 + + + + +# 函数、方法 + +## 错误处理 +error类型是一个接口类型,它的定义为: `type error interface { Error() string }` + + +Ø 定义错误 +• errors.New函数接收错误信息创建 + err := errors.New(“error message”) • fmt 创建错误对象 + fmt.Error(“error message”) + +Øpanic:用于主动抛出错误,在调试程序时, 通过panic 来打印堆栈,方便定位错误 +Ø recover:用来捕获 panic 抛出的错误, 阻 止panic继续向上传递 + +### defer +- 函数中使用defer,如果在defer声明之前抛出异常, defer不会执行,因为defer都没有压入到栈空间中; +- 如果在defer声明之后抛出异常, defer会被执行. +- 如果有多个defer按照栈的规则:**先入后出** + + +# 接口、结构体 + +## 接口 + + +### 接口类型转换 +go 的在 interface 类型转换的时候, 不是使用类型的转换, 而是使用 +``` +t,ok := i.(T) +//具体例子 +func getName(params ...interface{}) string { + var stringSlice []string + for _, param := range params { + stringSlice = append(stringSlice, param.(string)) + } + return strings.Join(stringSlice, "_") +} + +func main() { + fmt.Println(getName("redis", "slave", "master")) +} +``` + + + +# 多线程 \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go \345\215\225\345\205\203\346\265\213\350\257\225\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go \345\215\225\345\205\203\346\265\213\350\257\225\343\200\213.md" new file mode 100644 index 0000000..7b17a4e --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go \345\215\225\345\205\203\346\265\213\350\257\225\343\200\213.md" @@ -0,0 +1,384 @@ + +[TOC] + +# 标准库 testing + +## 测试规范 + +### 约定俗成 +- Go 语言推荐测试文件和源代码文件放在一块,测试文件以 `_test.go` 结尾。 +- 测试用例名称一般命名为 Test 加上待测试的方法名。 +- 测试用的参数有且只有一个,在这里是 t *`testing.T`。 +- 基准测试(benchmark)的参数是 `*testing.B`,TestMain 的参数是 `*testing.M` 类型。 +- 运行 `go test`,该 package 下所有的测试用例都会被执行。 + - `-v` 参数会显示每个用例的测试结果,另外 `-cover `参数可以查看覆盖率。 +- 如果只想运行**其中的一个用例**,例如 TestAdd,可以用 `-run` 参数指定,该参数支持通配符 `*`,和部分正则表达式,例如 `^`、`$` + - **go test -run TestAdd -v** +- `t.Error/t.Errorf` 遇错不停,还会继续执行其他的测试用例 +- `t.Fatal/t.Fatalf` 遇错即停。 + + +## 子测试(Subtests) +子测试是 Go 语言内置支持的,可以在某个测试用例中,根据测试场景使用 t.Run创建不同的子测试用例: +```go +// calc_test.go + +func TestMul(t *testing.T) { + t.Run("pos", func(t *testing.T) { + if Mul(2, 3) != 6 { + t.Fatal("fail") + } + + }) + t.Run("neg", func(t *testing.T) { + if Mul(2, -3) != -6 { + t.Fatal("fail") + } + }) +} +``` + + +对于子测试场景,更推荐(tabl-driven tests)的写法: +- 所有用例的数据组织在切片 cases 中,看起来就像一张表,借助循环创建子测试。 +- 好处 + - 新增用例非常简单,只需给 cases 新增一条测试数据即可。 + - 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。 + - 用例失败时,报错信息的格式比较统一,测试报告易于阅读。 +```go +// calc_test.go +func TestMul(t *testing.T) { + cases := []struct { + Name string + A, B, Expected int + }{ + {"pos", 2, 3, 6}, + {"neg", 2, -3, -6}, + {"zero", 2, 0, 0}, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + if ans := Mul(c.A, c.B); ans != c.Expected { + t.Fatalf("%d * %d expected %d, but %d got", + c.A, c.B, c.Expected, ans) + } + }) + } +} +``` + +## 帮助函数(helpers) +对一些重复的逻辑,抽取出来作为公共的帮助函数(helpers),可以增加测试代码的可读性和可维护性。 相当于抽出一些公共私有方法。 + + +例如把上面的子测试的逻辑抽出来: +```go +// calc_test.go +package main + +import "testing" + +type calcCase struct{ A, B, Expected int } + +func createMulTestCase(t *testing.T, c *calcCase) { + t.Helper() + if ans := Mul(c.A, c.B); ans != c.Expected { + t.Fatalf("%d * %d expected %d, but %d got", + c.A, c.B, c.Expected, ans) + } + +} + +func TestMul(t *testing.T) { + createMulTestCase(t, &calcCase{2, 3, 6}) + createMulTestCase(t, &calcCase{2, -3, -6}) + createMulTestCase(t, &calcCase{2, 0, 1}) // wrong case +} +``` +报错: +``` +--- FAIL: TestMul3 (0.00s) + xxx/calc_test.go:44: 2 * 0 expected 1, but 0 got +``` + +- 如果报错出现在帮助函数里面,只打印帮助函数的信息,第一时间很难确定是由哪个用例出现错误的。 所以, Go 语言在 1.9 版本中引入了 `t.Helper()`,用于标注该函数是帮助函数,**报错时将输出帮助函数调用者的信息**,而不是帮助函数的内部信息。 +新的报错: +``` +--- FAIL: TestMul3 (0.00s) + xxx/calc_test.go:53: 2 * 0 expected 1, but 0 got +``` + + +**两条建议** +- 不要返回错误, 帮助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。 +- 调用 t.Helper() 让报错信息更准确,有助于定位。 + + +## setup 和 teardown +如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,一般会写在 setup 和 teardown 函数中。 +```go +func setup() { + fmt.Println("Before all tests") +} + +func teardown() { + fmt.Println("After all tests") +} + +func Test1(t *testing.T) { + fmt.Println("I'm test1") +} + +func Test2(t *testing.T) { + fmt.Println("I'm test2") +} + +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} +``` + +* 在这个测试文件中,包含有2个测试用例,Test1 和 Test2。 +* 如果测试文件中包含函数 TestMain,那么生成的测试将调用 TestMain(m),而不是直接运行测试。 +* 调用 m.Run() 触发所有测试用例的执行,并使用 os.Exit() 处理返回的状态码,如果不为0,说明有用例失败。 +* 因此可以在调用 m.Run() 前后做一些额外的准备(setup)和回收(teardown)工作。 + + +## 网络测试 + +### TCP/HTTP +假设需要测试某个 API 接口的 handler 能够正常工作,例如 helloHandler +```go +func helloHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello world")) +} +``` + +**测试**: +```go +func handlerError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal("failed", err) + } +} + +func TestConn(t *testing.T) { + // 监听一个未被占用的端口,并返回 Listener。 + ln, err := net.Listen("tcp", "127.0.0.1:0") + handlerError(t, err) + defer ln.Close() + + http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { + var b []byte = []byte("hello " + r.URL.RawQuery) + w.Write(b) + }) + go http.Serve(ln, nil) + + // 尽量不对 http 和 net 库使用 mock,这样可以覆盖较为真实的场景 + resp, err := http.Get("http://" + ln.Addr().String() + "/hello?world") + handlerError(t, err) + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + handlerError(t, err) + + if string(body) != "hello world" { + t.Fatal("expect hello world, but got", string(body)) + } +} +``` + +### httptest +针对 http 开发的场景,使用标准库 net/http/httptest 进行测试更为高效。 +```go +func TestConn(t *testing.T) { + req := httptest.NewRequest("GET", "http://example.com/foo", nil) + w := httptest.NewRecorder() + helloHandler(w, req) + bytes, _ := ioutil.ReadAll(w.Result().Body) + + if string(bytes) != "hello world" { + t.Fatal("expected hello world, but got", string(bytes)) + } +} +``` +使用 httptest 模拟请求对象(req)和响应对象(w),达到了相同的目的。 + + +### Benchmark基准测试 +* 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名 +* 参数为 b *testing.B。 +* 执行基准测试时,需要添加 -bench 参数。 +- 如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer() 先重置定时器。 类似sleep +```go +func BenchmarkHello(b *testing.B) { + ... // 耗时操作 + b.ResetTimer() + for i := 0; i < b.N; i++ { + fmt.Sprintf("hello") + } +} +``` + +基准测试报告,每一列值对应的含义: +``` + go test -benchmem -bench . +... + int 迭代次数 time.Duration基准测试花费的时间 一次迭代处理的字节数 总的分配内存的次数 总的分配内存的字节数 +BenchmarkHello-16 15991854 71.6 ns/op 5 B/op 1 allocs/op +... +``` + +- 使用 RunParallel 测试并发性能 + +```go +func BenchmarkParallel(b *testing.B) { + templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) + b.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + for pb.Next() { + // 所有 goroutine 一起,循环一共执行 b.N 次 + buf.Reset() + templ.Execute(&buf, "World") + } + }) +} +``` + +# go mock + +## 简介 + +当待测试的函数/对象的依赖关系很复杂,并且有些依赖不能直接创建,例如数据库连接、文件I/O等。这种场景就非常适合使用 `mock/stub` 测试。 + + +[gomock](https://github.com/golang/mock) 是官方提供的 mock 框架,同时还提供了 `mockgen` 工具用来辅助生成测试代码。 + + +## 一个简单的Demo +```go +type DB interface { + Get(key string) (int, error) +} + +func GetFromDB(db DB, key string) int { + if value, err := db.Get(key); err == nil { + return value + } + return -1 +} +``` + +- 假设 DB 是代码中负责与数据库交互的部分,测试用例不能创建真实的数据库连接,如果我们需要测试 GetFromDB 这个函数内部的逻辑,就需要 mock 接口 DB。 + - 第一步:使用 mockgen 生成 db_mock.go。 一般传递三个参数。包含需要被mock的接口得到源文件source,生成的目标文件destination,包名package. + > mockgen -source=db.go -destination=db_mock.go -package=main + - 第二步:新建 db_test.go,写测试用例。 +```go +func TestGetFromDB(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用 + + m := NewMockDB(ctrl) + m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist")) + + if v := GetFromDB(m, "Tom"); v != -1 { + t.Fatal("expected -1, but got", v) + } +} +``` +- 这个测试用例有2个目的: + - 一是 使用 `ctrl.Finish()` 断言 `DB.Get()`被是否被调用,如果没有被调用,后续的 mock 就失去了意义; + - 二是 测试方法 GetFromDB() 的逻辑是否正确(如果 DB.Get() 返回 error,那么 GetFromDB() 返回 -1)。 + - NewMockDB() 的定义在 db_mock.go 中,由 mockgen 自动生成。 + + +## 打桩(stubs) +在上面的例子中,**当** Get() 的**参数为** Tom,**则返回** error,这**称之为打桩(stub)**,有明确的参数和返回值是最简单打桩方式。 + +### 比较参数(Eq、Any、Not、Nil) + +```go +m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist")) +m.EXPECT().Get(gomock.Any()).Return(630, nil) +m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil) +m.EXPECT().Get(gomock.Nil()).Return(0, errors.New("nil")) +``` + +* Eq(value) 表示与 value 等价的值。 +* Any() 可以用来表示任意的入参。 +* Not(value) 用来表示非 value 以外的值。 +* Nil() 表示 None 值。 + + +### 返回值(Return, DoAndReturn) + +```go +m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil) +m.EXPECT().Get(gomock.Any()).Do(func(key string) { + t.Log(key) +}) +m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string) (int, error) { + if key == "Sam" { + return 630, nil + } + return 0, errors.New("not exist") +}) +``` +* Return 返回确定的值 +* Do Mock 方法被调用时,要执行的操作吗,忽略返回值。 +* DoAndReturn 可以动态地控制返回值。 + + +### 调用次数(Times) +```go +func TestGetFromDB(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + m := NewMockDB(ctrl) + m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil).Times(2) + GetFromDB(m, "ABC") + GetFromDB(m, "DEF") +} +``` +- Times() 断言 Mock 方法被调用的次数。 +- MaxTimes() 最大次数。 +* MinTimes() 最小次数。 +* AnyTimes() 任意次数(包括 0 次)。 + + +### 调用顺序(InOrder) +```go +func TestGetFromDB(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用 + + m := NewMockDB(ctrl) + o1 := m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist")) + o2 := m.EXPECT().Get(gomock.Eq("Sam")).Return(630, nil) + gomock.InOrder(o1, o2) + GetFromDB(m, "Tom") + GetFromDB(m, "Sam") +} +``` + + +### 如何编写可 mock 的代码 +* `mock`作用的是接口,因此将依赖抽象为接口,而不是直接依赖具体的类。 +* 不直接依赖的实例,而是**使用依赖注入**降低耦合性。比如下面这种情况,对 DB 接口的 mock 并不能作用于 GetFromDB() 内部,这样写是没办法进行测试的。 +``` +func GetFromDB(key string) int { + //这个依赖不是注入进来的,而是自己实例化的,就没法mock + db := NewDB() + if value, err := db.Get(key); err == nil { + return value + } + + return -1 +} +``` \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go \345\271\266\345\217\221\347\274\226\347\250\213\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go \345\271\266\345\217\221\347\274\226\347\250\213\343\200\213.md" new file mode 100644 index 0000000..35f7adc --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go \345\271\266\345\217\221\347\274\226\347\250\213\343\200\213.md" @@ -0,0 +1,281 @@ + +## Go的MPG线程模型 + +### 协程 +**进程**是一个具有**独立功能的程序**关于某个数据集合的一次**动态执行过程**,是操作系统进行**资源分配和调度的基本单位**,是应用程序运行的载体。 + +而**线程**则是程序执行过程中一个**单一的顺序控制流程**,是 **CPU 调度和分派的基本单位**。 + +线程是比进程更小的独立运行基本单位,一个进程中可以拥有一个或者以上的线程,这些线程共享进程所持有的资源,在 CPU 中被调度执行,共同完成进程的执行任务。 + + + +**用户态和内核态,内核空间和用户空间** + +根据**资源访问权限**的不同,操作系统会把内存空间分为内核空间和用户空间: +- 内核空间的代码能够直接访问计算机的底层资源,如 CPU 资源、I/O 资源等,为用户空间的代码提供计算机底层资源访问能力; +- 用户空间为上层应用程序的活动空间,无法直接访问计算机底层资源,需要借助“系统调用”“库函数”等方式调用内核空间提供的资源。 + + +线程也可以分为内核线程和用户线程。 + +- **内核线程**由操作系统管理和调度,是内核调度实体,它能够直接操作计算机底层资源,可以充分利用 CPU 多核并行计算的优势,但是线程切换时需要 CPU 切换到内核态,存在一定的开销,可创建的线程数量也受到操作系统的限制。 +- **用户线程**由用户空间的代码创建、管理和调度,无法被操作系统感知。用户线程的数据保存在用户空间中,切换时无须切换到内核态,切换开销小且高效,可创建的线程数量理论上只与内存大小相关。 + + +**协程是一种用户线程**,属于**轻量级**线程。 + +优势: +- 协程的调度,完全由用户空间的代码控制; +- 协程拥有自己的寄存器上下文和栈,并存储在用户空间; +- 协程切换时无须切换到内核态访问内核空间,切换速度极快。 + +缺点: +- 但这也给开发人员带来较大的技术挑战:开发人员需要在用户空间处理协程切换时上下文信息的保存和恢复、栈空间大小的管理等问题。 + +Go 是为数不多**在语言层次实现协程并发**的语言,它采用了一种特殊的两级线程模型:MPG 线程模型 + +### MPG线程模型 + +- M,即 machine,相当于内核线程在 Go 进程中的映射,它与内核线程一一对应,代表真正执行计算的资源。在 M 的生命周期内,它只会与一个内核线程关联。 + +- P,即 processor,代表 Go 代码片段执行所需的上下文环境。M 和 P 的结合能够为 G 提供有效的运行环境,它们之间的结合关系不是固定的。P 的最大数量决定了 Go 程序的并发规模,由 runtime.GOMAXPROCS 变量决定。 + +- G,即 goroutine,是一种轻量级的用户线程,是对代码片段的封装,拥有执行时的栈、状态和代码片段等信息。 + +# Context + +## 什么是Context +Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系。 + + +该接口定义了四个需要实现的方法,其中包括: +```go +type Context interface { + Deadline() (deadline time.Time, ok bool) + Done() <-chan struct{} + Err() error + Value(key interface{}) interface{} +} +``` +1. Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期; +2. Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel; +3. Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值; + 1. 如果 context.Context 被取消,会返回 Canceled 错误; + 2. 如果 context.Context 超时,会返回 DeadlineExceeded 错误; +4. Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据; + + +context 包中提供的 context.Background、context.TODO、context.WithDeadline 和 context.WithValue 函数会返回实现该接口的私有结构体。 + +## 为什么需要Context +- 在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。 + + + +**背景知识**:waitgroup和channel + +WaitGroup 和信道(channel)是常见的 2 种并发控制的方式。 + +如果并发启动了多个子协程,需要等待所有的子协程完成任务,WaitGroup 非常适合于这类场景,例如下面的例子: + +```go +var wg sync.WaitGroup + +func doTask(n int) { + time.Sleep(time.Duration(n)) + fmt.Printf("Task %d Done\n", n) + wg.Done() +} + +func main() { + for i := 0; i < 3; i++ { + wg.Add(1) + go doTask(i + 1) + } + wg.Wait() + fmt.Println("All task Done") +} + +``` +- wg.Wait() 会等待所有的子协程任务全部完成,所有子协程结束后,才会执行 wg.Wait() 后面的代码. +- WaitGroup并**不能主动通知子协程退出**。 + + + 假如开启了一个定时轮询的子协程,**有没有什么办法,通知该子协程退出呢**? +>select + chan 的机制 +```go + +var stop chan bool + +func reTask(name string) { + for { + select { + case <-stop: + fmt.Println("stop", name) + return + default: + fmt.Println(name, "send request") + time.Sleep(1 * time.Second) + } + } +} + +func main() { + stop = make(chan bool) + go reTask("worker1") + time.Sleep(3 * time.Second) + stop <- true + time.Sleep(3 * time.Second) +} +``` +>子协程使用 for 循环定时轮询,如果 stop 信道有值,则退出,否则继续轮询。 + + +更复杂的场景如何做并发控制呢? +比如子协程中开启了新的子协程,或者需要同时控制多个子协程。这种场景下,select+chan的方式就显得力不从心了。 + + +Go 语言提供了 Context 标准库可以解决这类场景的问题,Context 的作用和它的名字很像,上下文,即子协程的下上文。Context 有两个主要的功能: + +* 通知子协程退出(正常退出,超时退出等); +* 传递必要的参数。 + +## contex.WithCancel +`context.WithCancel()`创建**可取消的Context对象**,即可以主动通知子协程退出。 + +### 控制单个协程 + +使用Context改写上面的例子,效果与select+chan相同。 +```go + +func reTask(ctx context.Context, name string) { + for { + select { + // 在子协程中,使用 select 调用 <-ctx.Done() 判断是否需要退出。 + case <-ctx.Done(): + fmt.Println("stop", name) + return + default: + fmt.Println(name, "send request") + time.Sleep(1 * time.Second) + } + } +} + +func main() { + // context.Backgroud() 创建根 Context,通常在 main 函数、初始化和测试代码中创建,作为顶层 Context。 + // context.WithCancel(parent) 创建可取消的子 Context,同时返回函数 cancel + ctx, cancel := context.WithCancel(context.Background()) + go reTask(ctx, "worker1") + time.Sleep(3 * time.Second) + // 主协程中,调用 cancel() 函数通知子协程退出。 + cancel() + time.Sleep(3 * time.Second) +} +``` + +### 控制多个协程 + +调用 `cancel()` 函数后该 `Context` 控制的所有子协程都会退出。 + +```go + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + go reTask(ctx, "worker1") + go reTask(ctx, "worker2") + + time.Sleep(3 * time.Second) + // 为每个子协程传递相同的上下文 ctx 即可,调用 cancel() 函数后该 Context 控制的所有子协程都会退出。 + cancel() + time.Sleep(3 * time.Second) +} +``` + + +### context.WithValue +如果需要**往子协程中传递参数**,可以使用 `context.WithValue()`。 +```go +type Options struct { + Interval time.Duration +} + +func reqTask(ctx context.Context, name string) { + for { + select { + case <-ctx.Done(): + fmt.Println("stop", name) + return + default: + fmt.Println(name, "send request") + op := ctx.Value("options").(*Options) + time.Sleep(op.Interval * time.Second) + } + + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + vCtx := context.WithValue(ctx, "options", &Options{1}) + + go reqTask(vCtx, "worker1") + go reqTask(vCtx, "worker2") + + time.Sleep(3 * time.Second) + cancel() + time.Sleep(3 * time.Second) +} +``` + +- `context.WithValue()` 创建了一个基于 ctx 的子 Context,并携带了值 options。 +- 在子协程中,使用 `ctx.Value("options"`) 获取到传递的值,读取/修改该值。 + + +### context.WithTimeout +如果需要控制子协程的执行时间,可以使用 `context.WithTimeout` 创建具有**超时通知机制**的 `Context` 对象。 +```go +ctx, cancel := context.WithCancel(context.Background()) +``` +- WithTimeout()的使用与 WithCancel() 类似,多了一个参数,用于设置超时时间。 +- 因为超时时间设置为 2s,但是 main 函数中,3s 后才会调用 cancel(),因此,在调用 cancel() 函数前,子协程因为超时已经退出了。 + + + +### context.WithDeadline +- 超时退出可以控制子协程的最长执行时间,那 `context.WithDeadline()` 则可以控制子协程的**最迟退出时间**。 + +```go + +func reqTask(ctx context.Context, name string) { + for { + select { + case <-ctx.Done(): + fmt.Println("stop", name) + return + default: + fmt.Println(name, "send request") + + time.Sleep(1 * time.Second) + } + + } +} + +func main() { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) + + go reqTask(ctx, "worker1") + go reqTask(ctx, "worker2") + + time.Sleep(3 * time.Second) + fmt.Println("before cancel") + cancel() + time.Sleep(3 * time.Second) +} +``` + +- WithDeadline 用于设置截止时间。在这个例子中,将截止时间设置为1s后,cancel() 函数在 3s 后调用,因此子协程将在调用 cancel() 函数前结束。 +- 在子协程中,可以通过 ctx.Err() 获取到子协程退出的错误原因。 \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go \347\226\221\351\232\276\346\235\202\347\227\207\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go \347\226\221\351\232\276\346\235\202\347\227\207\343\200\213.md" new file mode 100644 index 0000000..00ac9eb --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go \347\226\221\351\232\276\346\235\202\347\227\207\343\200\213.md" @@ -0,0 +1,205 @@ +[TOC] + + + +## 经常疑惑的点 + +### GOPATH + +- 下载的第三方包源代码文件放在$GOPATH/src目录下, +- 产生的二进制可执行文件放在 $GOPATH/bin目录下, +- 生成的中间缓存文件会被保存在 $GOPATH/pkg 下 + + +- 配置变量GOPATH会跟安装默认的冲突不? + >是配置到~/.bashrc里。然后source一下. + + +- `go install xxx`文件后,提示安装到了最初go安装默认的位置,但是已经指定GOPATH,为啥还会安装到那? 提示:`open /usr/local/go/bin/mathapp: permission denied`。 难道还要指定GOROOT? 我希望安装到的是自定义的GOPATH下的src/bin目录下 +>**解决办法**: +>(a) 如果你没有设置你的GOBIN env变量,你可以在GOROOT/bin中获得Go编译器二进制文件,而你的二进制文件将在GOPATH/bin中.(我个人喜欢这种二进制分离.) +>(b) 如果你将GOBIN设置为任何东西,那么Go二进制文件和你的二进制文件都将转到GOBIN. + + + +### 包 package +- go 里面一个目录为一个package, 一个package级别的func, type, 变量, 常量, 这个package下的所有文件里的代码都可以随意访问, 不需要首字母大写 +- **同目录**下的两个文件如hello.go和hello2.go中的package 定义的名字要是同一个,不同的话,是会报错的 ==> 所以main方法要单独放一个文件 + + +### init函数和main函数 + +**init函数**: 用于包(package)的初始化 + + + 1. init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等 + 2. 每个包可以拥有多个init函数 + 3. 包的每个源文件也可以拥有多个init函数 + 4. 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明) + 5. 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序 + 6. init函数**不能被其他函数调用**,而是在main函数执行之前,自动被调用。 手动显示调用init会收到编译错误:`undefined: init` + + +**main函数**:Go语言程序的默认入口函数(主函数) + + + 1. 可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错。 + 2. 在启动了多个 Goroutine的 Go 应用中,main.main 函数将在 Go 应用的主 Goroutine 中执行。如果住goroutine结束、返回,则其他子goroutine都会结束。 + + +**异同**: +- 相同点: + - 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。 +- 不同点: + - init可以应用于任意包中,且可以重复定义多个。 + - main函数只能用于main包中,且只能定义一个。 + + +**执行顺序** +![go包初始化顺序](../../Computer-StudyNotes/img/《Go%20疑难杂症》/Go包初始化顺序.jpg) + +- 如果 main 包依赖的包中定义了 init 函数,或者是 main 包自身定义了 init 函数,那么 Go 程序在这个包初始化的时候,就会自动调用它的 init 函数,因此这些 **init 函数的执行就都会发生在 main 函数之前**。 + +* 对同一个go文件的多个init()调用顺序是**从上到下的**。 + +* 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。 + +* 对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),**最后调用main函数**。 + +* 如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用 + + + +# 看go语言圣经的疑惑 +## defer修饰的方法到底什么时候执行? + +### 看了下csdn有小伙伴遇到一样的疑问 + +https://blog.csdn.net/lj779323436/article/details/109696343 + +但最近看《go程序设计语言》一书关于defer这一块的介绍时,书中写了一个demo,用defer实现了进入函数的打印以及出函数的打印和函数花费的时间,现把代码贴出来: +```go +//gopl.io/ch5/trace + +func bigSlowOperation() { + defer trace("bigSlowOperation")() // don't forget the extra parentheses + // ...lots of work… + time.Sleep(10 * time.Second) // simulate slow operation by sleeping +} +func trace(msg string) func() { + start := time.Now() + log.Printf("enter %s", msg) + return func() { + log.Printf("exit %s (%s)", msg,time.Since(start)) + } +} + +func main(){ + bigSlowOperation() +} + +``` + +仔细一看,发现defer后面是一个函数(方法),而且这个函数的返回值也是函数,按照常规的思维方式,defer修饰的代码应该在函数(方法)结束的时候才执行,也就是time.Sleep(10 * time.Second)这句代码执行之后再才轮到defer修饰的trace方法执行,这样trace方法中的log.Printf("enter %s", msg)岂不是在程序执行10s后才执行吗?这如何实现杠进入bigSlowOperation就打印呢?下面我们跑一下 +``` +//打印 +2015/11/18 09:53:26 enter bigSlowOperation +2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s) + +``` + +**如果defer不带返回值呢?** +不带return的defer后面的函数确实是在原函数结束的时候才执行 + + +所以该题主得初步出结论:**defer后面的函数里面只要有return语句,则只有这个return的语句才会在原函数结束时执行;** + + +我又试了下,想看看返回不是函数,而是其他值,比如int是不是还会如此, 看下面评论。 + +### 我的评论 + +看这块的时候我也遇到这个坑了,不过你的结论没有准确覆盖。 +你有没有发现书上的例子return的是一个函数?你发现了。 但是你仔细看defer声明背后带了个圆括号?: +defer trace("bigSlowOperation")() + +如果我把这个函数改一下,改成返回int,那带圆括号是会报错的,可能是返回func()和圆括号是语法匹配?这个坑我还没找到解释。 但是, 我可以把圆括号去掉改成下面这个: +```go +func bigSlowOperation() { + defer trace("xxx") // don't forget the extra parentheses + // ...lots of work... + time.Sleep(10 * time.Second) // simulate slow operation by sleeping + log.Println("ready end...") +} + +func trace(msg string) int { + v := 2 + if msg == "xxx" { + v++ + log.Printf("v:%d", v) + return v + } + log.Printf("enter %s, v:%d", msg, v) + return v +} + +// !-main + +func main() { + bigSlowOperation() +} +``` + +结果发现: +``` +2022/05/14 01:43:42 ready end... +2022/05/14 01:43:42 v:3 +``` +初步结论:defer后面函数虽然带了return语句,return前的代码也不会运行,而是一起等到原函数结束后才运行。 目前只有返回func的defer函数(注意不要漏掉圆括号)的场景,才可以实现return前的代码经过defer时就运行,return后的func是原函数结束后执行。 + + + +而书中两个提到两句话可能是解开问题的关键(虽然我还是没弄懂,刚学习一周多): + +1. 你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行。 +- 执行到该条语句--应该就是经过defer时, +- 函数和参数得到计算---这是核心点,这里的函数是指啥? 参数又是指啥,我感觉函数应该就是最开始的例子,参数的例子可能我还得试一下。 +- defer后的函数才会被执行---这一句的函数跟上面的应该不是一个东西?(我严重怀疑是翻译的问题,决定是看看英文) + +2. 需要注意一点:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。 + + + +### 于是我就去看了英文版本 +下面2个网址可以在线阅读: + +https://www.doc88.com/p-6189947807970.html?r=1 + +https://www.shuzhiduo.com/A/Vx5Mvo47dN/ + +Syntactically, a defer statement is an ordinary function or method call prefixed by the keyword defer. +**The function and argument expressions are evaluated** when **the statement is executed**, but **the actual call is deferred** until **the function** that contains the defer statement has **finished**, whether normally, by executing a return statement or falling off the end, or abnormally, by panicking. + Any number of calls may be deferred; they are executed in the reverse of the order in which they were deferred. + + +The defer statement can also be used to pair "on entry" and "on exit" actions when debugging a complex function. + +The bigSlowOperation function below calls trace immediately, which does the "on entry* action then returns a fuction value that, when called, does the corresponding "on exit" action. + +By deferring a call to the returned function in this way, we can instrument the entry point and all exit points of a function in a single statement and even pass values, like the start time, between the two actions. + +But don't forget the final parentheses in the defer statement, or the "on entry" action will happen on exit and the on-exit action won't happen at all! + + + +## 圣经翻译疑惑:给命名类型指定方法有啥限制? +`Distance() float64` + + + + +# 疑惑 + +## 如何知道类型实现了哪些接口? + +有个疑问,go里面一个类型实现了接口所有的方法,才算该接口类型,但并没有语法显式 说明这个类型实现了哪个接口(例如java中有implements), 这样看别人代码的时候,碰到一 个类型,无法知道这个类型是不是实现了一个接口,除非类型和接口写在一个文件,然后 还要自己一个一个方法去对比。有比较快的方法可以知道当前类型实现了哪些接口么? diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go \350\257\255\350\250\200\350\247\204\350\214\203\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go \350\257\255\350\250\200\350\247\204\350\214\203\343\200\213.md" new file mode 100644 index 0000000..16e5d97 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go \350\257\255\350\250\200\350\247\204\350\214\203\343\200\213.md" @@ -0,0 +1,96 @@ +[TOC] +# 如何写出优雅的Go代码 + +## Go语言规范 + +- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) +- [《Effective Go》](https://go.dev/doc/effective_go) + + +### 腾讯规约 + +#### 不符合个人习惯的点 +**特别不符合** + + +- xxx + + + +**一般不符合** + + +- 作为**输入参数**或者数组下标时,运算符和运算数之间不需要空格,紧凑展示。 + +#### import规范 +* 对包进行分组管理,通过空行隔开,默认分为本地包(标准库、内部包)、第三方包。 +* 标准包永远位于最上面的第一组。 +* 内部包是指不能被外部 import 的包,如 GoPath 模式下的包名或者非域名开头的当前项目的 GoModules 包名。 +* **带域名**的包名都属于第三方包,如 git.code.oa.com/xxx/xxx,github.com/xxx/xxx,**不用区分是否是当前项目内部**的包。 +* goimports 默认最少分成本地包和第三方包两大类,这两类包必须分开不能放在一起。本地包或者第三方包内部可以继续按实际情况细分不同子类。 + +- 不要使用相对路径引入包 +- 匿名包的引用建议使用一个新的分组引入,并在匿名包上写上注释说明。 + +#### error处理 +- error 必须是最后一个参数 +- 错误描述不需要标点结尾。 +- 判断错误不要if-else,直接用卫语句一个if搞定。 而且需要单独处理,不与其他变量组合逻辑判断。 +```go +// 不要采用这种方式: +x, y, err := f() +if err != nil || y == nil { + return err // 当y与err都为空时,函数的调用者会出现错误的调用逻辑 +} +``` +* 【推荐】对于不需要格式化的错误,生成方式为:errors.New("xxxx")。 +* 【推荐】建议go1.13 以上,error 生成方式为:fmt.Errorf("module xxx: %w", err)。 + + +### 最佳实践 + +#### 包 +>如何设计包,包括包的名称,命名类型以及编写方法和函数的技巧 + +- 在项目中,每个包名称应该是唯一的。包的名称应该描述其目的的建议很容易理解 - 如果你发现有两个包需要用相同名称,它可能是: + - 包的名称太通用了。 + - 该包与另一个类似名称的包重叠了。在这种情况下,您应该检查你的设计,或考虑合并包。 + + +- 避免使用类似 base, common或 util的包名称 + - 一些帮助程序和工具类的包,由于这些包包含各种不相关的功能,因此很难根据包提供的内容来描述它们。 + - 像 utils或 helper这样的包名称通常出现在较大的项目中,这些项目已经开发了深层次包的结构,并且希望在不遇到导入循环的情况下共享 helper函数。 + - 如果可能的话,将相关的函数移动到调用者的包中。即使这涉及复制一些 helper程序代码,这也比在两个程序包之间引入导入依赖项更好。 + > [一点点] 重复比错误的抽象的性价比高很多。 + + +#### 项目结构 +- 除 cmd/和 internal/之外的每个包都应包含一些源代码。 +- 尽量选择更少、更大的包 + - 如果您来自 Java或 C#,请考虑这一经验法则 + - Java包相当于单个 .go源文件。 + - Go 语言包相当于整个 Maven模块或 .NET程序集。 + +- 什么时候应该将 .go文件拆分成多个文件?什么时候应该考虑整合 .go文件? + + * 开始时使用一个 .go文件。为该文件指定与文件夹名称相同的名称。例如: package http应放在名为 http的目录中名为 http.go的文件中。 + + * 随着包的增长,您可能决定将各种职责任务拆分为不同的文件。例如, messages.go包含 Request和 Response类型, client.go包含 Client类型, server.go包含 Server类型。 + + * 如果你的文件中 import的声明类似,请考虑将它们组合起来。或者确定 import集之间的差异并移动它们。 + + * 不同的文件应该负责包的不同区域。 messages.go可能负责网络的 HTTP请求和响应, http.go可能包含底层网络处理逻辑, client.go和 server.go实现 HTTP业务逻辑请求的实现或路由等等。 + + +#### API设计 + + + +--- + +## 腾讯 Protobuf 代码规范 +Protobuf作为服务数据接口的重要组成部分,某种程度上对其正确性和稳定性的**要求比对功能代码本身还高**。 + + +--- +## Go Mod 开发&发布公约 diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go \351\235\242\350\257\225\351\242\230\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go \351\235\242\350\257\225\351\242\230\343\200\213.md" new file mode 100644 index 0000000..84bac65 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go \351\235\242\350\257\225\351\242\230\343\200\213.md" @@ -0,0 +1,220 @@ + +# 基础语法 + +## 指针 +- 指针用来保存变量的地址。 +```go +var x = 5 +var p *int = &x +fmt.Printf("x = %d", *p) // x 可以用 *p 访问 + +type Result struct { + Num, Ans int +} + +type Cal int + +func (cal *Cal) Square(num int) *Result { + return &Result{ + Num: num, + Ans: num * num, + } +} + +func main() { + cal := new(Cal) + result := cal.Square(6) + // cal type: *main.Cal + // result type: *main.Result + +} +``` +* * 运算符,也称为解引用运算符,用于访问地址中的值。 +* &运算符,也称为地址运算符,用于返回变量的地址。 +- &Result 等于 new(Result) 结果是对象的指针 +- *Result 表示一个Result对象的指针 + + +## 类型 + +1. **如何高效拼接字符串** +>使用 strings.Builder,最小化内存拷贝次数 + + +2. 什么是 rune 类型 + >Unicode是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。 + +Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。例如下面的例子中 语 和 言 使用 UTF-8 编码后各占 3 个 byte,因此 len("Go语言") 等于 8. +```go +fmt.Println(len("Go语言")) // 8 +//将字符串转换为 rune 序列 +fmt.Println(len([]rune("Go语言"))) // 4 +``` + + +## defer +- 多个 defer 语句,遵从后进先出(Last In First Out,LIFO)的原则,最后声明的 defer 语句,最先得到执行。 + + +- defer 在 return 语句之后执行,但在函数退出之前,defer 可以修改返回值(有名返回值)。 + - 如果是未命名返回值:执行 return 语句后,Go 会创建一个临时变量保存返回值,因此,defer 语句修改了局部变量 i,并没有修改返回值。 + - 有名返回值: 对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改会对返回值产生了影响。 + + +## tag的作用 +- tag 可以理解为 struct 字段的**注解**,可以用来**定义字段的一个或多个属性**。 +- 框架/工具可以**通过反射获取到某个字段定义的属性**,采取相应的处理方式。 +- tag 丰富了代码的语义,增强了灵活性。 +```go +package main + +import "fmt" +import "encoding/json" + +type Stu struct { + Name string `json:"stu_name"` + ID string `json:"stu_id"` + Age int `json:"-"` +} + +func main() { + buf, _ := json.Marshal(Stu{"Tom", "t001", 18}) + fmt.Printf("%s\n", buf) +} +``` +这个例子使用 tag 定义了结构体字段与 json 字段的转换关系,Name -> stu_name, ID -> stu_id,忽略 Age 字段。很方便地实现了 Go 结构体与不同规范的 json 文本之间的转换。**输出**: +```json +{"stu_name":"Tom","stu_id":"t001"} + +``` + + +## 字符串 + +**字符串打印时,%v 和 %+v 的区别,** +>%v 和 %+v 都可以用来打印 struct 的值,区别在于 %v 仅打印各个字段的值,%+v 还会打印各个字段的名称。 + + +## 用常量表示枚举值 +```go +type StuType int32 + +const ( + Type1 StuType = iota + Type2 + Type3 + Type4 +) + +func main() { + fmt.Println(Type1, Type2, Type3, Type4) // 0, 1, 2, 3 +} +``` + +## 常量赋值 +```go +func main() { + const N = 100 + //无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功 + var x int = N + fmt.Println(x) + //有类型的常量 ,赋值给其他变量时,需要类型匹配才能成功 + const M int32 = 100 + var y int = M + fmt.Println(y) +} +``` + + +## 空 struct{} 的用途 +- 使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。 +```go +fmt.Println(unsafe.Sizeof(struct{}{})) // 0 +``` + +- 使用 map 表示集合时,**只关注 key**,value 可以使用 struct{} 作为占位符。 +```go + type Set map[string]struct{} +``` + +- 使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替 +```go +func main() { + ch := make(chan struct{}, 1) + go func() { + <-ch + // do something + }() + ch <- struct{}{} + // ... +} +``` + +- 声明只包含方法的结构体。 +```go +type Lamp struct{} + +func (l Lamp) On() { + println("On") + +} +func (l Lamp) Off() { + println("Off") +} +``` + +# 并发编程 + +## 什么是协程泄露(Goroutine Leak)? +协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。常见的导致协程泄露的场景有以下几种: + +- 缺少接收器,导致发送阻塞 +- 缺少发送器,导致接收阻塞 +- 两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。 + +# 垃圾回收(GC) + +Go 语言采用的是**标记清除**算法。并在此基础上使用了**三色标记法**和**写屏障技术**,提高了效率。 + + +标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段: + +* 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象; +* 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表。 + + +标记清除算法的一大问题是**在标记期间,需要暂停程序**(Stop the world,STW),标记结束之后,用户程序才可以继续执行。 + +**为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法**。三色标记算法将程序中的对象分成白色、黑色和灰色三类。 + +* 白色:不确定对象。 +* 灰色:存活对象,子对象待处理。 +* 黑色:存活对象。 + + +1. 标记开始时,所有对象加入白色集合(这一步需 STW )。 +2. 首先将根对象标记为灰色,加入灰色集合。 +3. 垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。 +4. 重复这个过程,直到灰色集合为空为止,标记阶段结束。 +5. 那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。 + +三色标记法**因为多了一个白色的状态来存放不确定对象**,**所以后续的标记阶段可以并发地执行**。**为啥?** + + +当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。 +所以三色标记法是一个 false negative(假阴性)的算法。 + +三色标记法并发执行仍存在一个问题,即在 GC 过程中,对象指针发生了改变。 +>A (黑) -> B (灰) -> C (白) -> D (白) +从B到C时,C删除了D的引用, 而B又引用了D,这个时候B已经标记为黑色了,不会再扫描其指向的对象。 D怎么办? + +为了解决这个问题,Go**使用了内存屏障技术**,它是**在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码**,类似于一个钩子。 +垃圾收集器使用了写屏障(Write Barrier)技术,**当对象新增或更新时,会将其着色为灰色**。(将B置为灰色还是将D置为灰色?或者一起?) + + +一次完整的 GC 分为四个阶段: + +1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier) +2)使用三色标记法标记(Marking, 并发) +3)标记结束(Mark Termination,需 STW),关闭写屏障。 +4)清理(Sweeping, 并发) \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\345\205\245\351\227\250\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\345\205\245\351\227\250\343\200\213.md" new file mode 100644 index 0000000..cd7e4b1 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\345\205\245\351\227\250\343\200\213.md" @@ -0,0 +1,1354 @@ +[TOC] + +# 开始学习Go + +## go的独特之处 +**它的主要目标是“兼具Python 等动态语言的开发速度和C/C++等编译型语言的性能与安全性”** + + +Go 语言的**设计哲学**:简单、显式、组合、并发和面向工程。 + +### 为并发而生 +Go 放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程,Go 将之称为 goroutine。 + + +- Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。 +- Go 语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。 +- 开启一个goroutine的消耗非常小(大约2KB的内存),goroutine 调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此你可以轻松创建数百万个goroutine。 + +- goroutine的特点: + 1. goroutine具有可增长的分段堆栈。这意味着它们只在需要时才会使用更多内存。 + 2. goroutine的启动时间比线程快。 + 3. goroutine原生支持利用channel安全地进行通信。 + 4. goroutine共享数据结构时无需使用互斥锁。 + + +- Go 还在语言层面**内置了辅助并发设计的原语**:`channel` 和 `select`。 + - 开发者可以通过语言内置的 channel 传递消息或实现同步 + - 并通过 select 实现多路 channel 的并发控制 + +### 设计理念 + +#### 简单易学 + +- **语法简洁** + - 其语法在C语言的基础上进行了大幅的简化,去掉了不需要的表达式括号,循环也只有 for 一种表示方法,就可以实现数值、键值等各种遍历。 + - 仅有25个关键字 +- **代码风格统一** + - gofmt +- **开发效率高** + - Go语言实现了开发效率与执行效率的完美结合,让你像写Python代码(效率)一样编写C代码(性能)。 +- 首字母大写决定可见性 +- 变量初始为类型零值,避免以随机值作为初值的问题; +- 内置数组边界检查,极大减少越界访问带来的安全隐患; + + +- **自带GC** + + +- **内置并发** + + +**简单的思想,没有继承,多态,类等** +- 内置接口类型,为组合的设计哲学奠定基础; +- 原生提供完善的工具链,开箱即用; + +#### 显示 + +显示转型: +```go +c = int64(a) + int64(b) +fmt.Printf("%d\n", c) +``` +在 Go 语言中,不同类型变量是不能在一起进行混合计算的,这是因为 Go 希望开发人员明确知道自己在做什么。 + + +Go 语言采用了显式的**基于值比较的错误处理方案**,函数 / 方法中的错误都会通过 return 语句显式地返回,并且通常调用者不能忽略对返回的错误的处理。 + + +#### 组合 + +在 Go 语言设计层面,Go 设计者为开发者们提供了正交的语法元素,以供后续组合使用,包括:- Go 语言无类型层次体系,各类型之间是相互独立的,没有子类型的概念; +- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的; +- 实现某个接口时,无需像 Java 那样采用特定关键字修饰; +- 包之间是相对独立的,没有子包的概念。 + + +**类型嵌入** + + +Go 语言为支撑组合的设计提供了类型嵌入(Type Embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典面向对象语言中的“继承”机制。 + +- 被嵌入的类型和新类型两者之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典面向对象语言中的那种父类、子类的关系,以及向上、向下转型(Type Casting)。 +- 通过新类型实例调用方法时,方法的匹配主要取决于方法名字,而不是类型。 + + +**垂直组合** + + +即通过类型嵌入,快速让一个新类型“复用”其他类型已经实现的能力,实现功能的垂直扩展。 +>**垂直组合本质上是一种“能力继承”,采用嵌入方式定义的新类型继承了嵌入类型的能力。** + + +**水平组合** + + +水平组合是一种能力委托(Delegate),我们通常使用**接口**类型来实现水平组合。 +- 通过接受接口类型参数的普通函数进行组合,如以下代码段所示: +```go +// $GOROOT/src/io/ioutil/ioutil.go +func ReadAll(r io.Reader)([]byte, error) + +// $GOROOT/src/io/io.go +func Copy(dst Writer, src Reader)(written int64, err error) +``` + - 上面代码实现了:从任意实现 io.Reader 的数据源读取所有数据的目的。 + - 类似的水平组合“模式”还有点缀器、中间件等 +- 通过 goroutine+channel 的组合,可以实现类似 Unix Pipe 的能力。 + + +#### 面向工程 + +Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案,这些问题包括:程序构建慢、依赖管理失控、代码难于理解、跨语言构建难等。 + + +在 Go 语言最初设计阶段就将解决工程问题作为 Go 的设计原则之一去考虑 Go 语法、工具链与标准库的设计,这也是 Go 与其他偏学院派、偏研究型的编程语言在设计思路上的一个重大差异。 + + +在面向工程设计哲学的驱使下,Go 在语法设计细节上做了精心的打磨。比如: +- 重新设计编译单元和目标文件格式,实现 Go 源码快速构建,让大工程的构建时间缩短到类似动态语言的交互式解释的编译速度; +- 如果源文件导入它不使用的包,则程序将无法编译。这可以充分保证任何 Go 程序的依赖树是精确的。这也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间; +- 去除包的循环依赖,循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建; +- 包路径是唯一的,而包名不必唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者如何引用其内容的约定。“包名称不必是唯一的”这个约定,大大降低了开发人员给包起唯一名字的心智负担; +- 故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制,向函数添加过多的参数以弥补函数 API 的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性; +- 增加类型别名(type alias),支持大规模代码库的重构。 + + +Go 在标准库中提供了各类高质量且性能优良的**功能包**,其中的net/http、crypto、encoding等包充分迎合了云原生时代的关于 API/RPC Web 服务的构建需求,Go 开发者可以直接基于标准库提供的这些包实现一个满足生产要求的 API 服务,从而**减少对外部第三方包或库的依赖**,降低工程代码依赖管理的复杂性,也降低了开发人员学习第三方库的心理负担。 + + +**工具链**丰富:工具链涵盖了编译构建、代码格式化、包依赖管理、静态代码检查、测试、文档生成与查看、性能剖析、语言服务器、运行时程序跟踪等方方面面。 + + +### go适合做什么 +* 服务端开发 +* 分布式系统,微服务 +* 网络编程 +* 区块链开发 +* 内存KV数据库,例如boltDB、levelDB +* 云平台 + + +广泛用于人工智能、云计算开发、容器虚拟化、⼤数据开发、数据分析及科学计算、运维开发、爬虫开发、游戏开发等领域。 + + +### 主要特征 +1.自动立即回收。 +2.更丰富的内置类型。 +3.函数多返回值。 +4.错误处理。 +5.匿名函数和闭包。 +6.类型和接口。 +7.并发编程。 +8.反射。 +9.语言交互性。 +10. **静态编译** + + +>接口、并发、包、测试和反射等语言特性。 + +Go语言的**面向对象机制与一般语言不同**。 +- 它没有类层次结构,甚至可以说没有类; +- 仅仅通过组合(而不是继承)简单的对象来构建复杂的对象。 +- 方法不仅可以定义在结构体上,而且,可以定义在任何用户自定义的类型上; +- 并且,具体类型和抽象类型(接口)之间的关系是隐式的, + +>所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。 + + + + +# 《build-web-application-with-golang》 +>https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/01.3.md + +## Go命令 + +```go +The commands are: + + bug start a bug report + build compile packages and dependencies + clean remove object files and cached files + doc show documentation for package or symbol + env print Go environment information + fix update packages to use new APIs + fmt gofmt (reformat) package sources + generate generate Go files by processing source + get add dependencies to current module and install them + install compile and install packages and dependencies + list list packages or modules + mod module maintenance + work workspace maintenance + run compile and run Go program + test test packages + tool run specified go tool + version print Go version + vet report likely mistakes in packages + +Use "go help " for more information about a command. + +Additional help topics: + + buildconstraint build constraints + buildmode build modes + c calling between Go and C + cache build and test caching + environment environment variables + filetype file types + go.mod the go.mod file + gopath GOPATH environment variable + gopath-get legacy GOPATH go get + goproxy module proxy protocol + importpath import path syntax + modules modules, module versions, and more + module-get module-aware go get + module-auth module authentication using go.sum + packages package lists and patterns + private configuration for downloading non-public code + testflag testing flags + testfunc testing functions + vcs controlling version control with GOVCS + +Use "go help " for more information about that topic. +``` + +### go build +目的:用于编译代码。在包的编译过程中,若有必要,会同时编译与之相关联的包。 + +**命令介绍** + +* 如果是普通包,就像我们在1.2节中编写的mymath包那样,当你执行go build之后,它不会产生任何文件。如果你需要在$GOPATH/pkg下生成相应的文件,那就得执行go install。 + +* 如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件。如果你需要在$GOPATH/bin下生成相应的文件,需要执行go install,或者使用go build -o 路径/a.exe。 + +* 如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在go build之后加上文件名,例如go build a.go;go build命令默认会编译当前目录下的所有go文件。 + +* 你也可以指定编译输出的文件名。例如1.2节中的mathapp应用,我们可以指定go build -o astaxie.exe, + * 默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。 + * 默认生成的可执行文件名是文件夹名。) +* go build会忽略目录下以“_”或“.”开头的go文件。 +* go build的时候会选择性地编译以系统名结尾的文件(Linux、Darwin、Windows、Freebsd)。例如Linux系统下面编译只会选择array_linux.go文件,其它系统命名后缀文件全部忽略,例如array_windows.go。 + + +**参数的介绍** +-o 指定输出的文件名,可以带上路径,例如 go build -o a/b/c +-i 安装相应的包,编译+go install +-a 更新全部已经是最新的包的,但是对标准包不适用 +-n 把需要执行的编译命令打印出来,**但是不执行**,这样就可以很容易的知道底层是如何运行的 +-p n 指定可以并行可运行的编译数目,默认是CPU数目 +-race 开启编译的时候自动检测数据竞争的情况,目前只支持64位的机器 +-v 打印出来我们正在编译的包名 +-work 打印出来编译时候的临时文件夹名称,并且如果已经存在的话就不要删除 +-x 打印出来执行的命令,其实就是和-n的结果类似,只是这个会执行 +-ccflags 'arg list' 传递参数给5c, 6c, 8c 调用 +-compiler name 指定相应的编译器,gccgo还是gc +-gccgoflags 'arg list' 传递参数给gccgo编译连接调用 +-gcflags 'arg list' 传递参数给5g, 6g, 8g 调用 +-installsuffix suffix 为了和默认的安装包区别开来,采用这个前缀来重新安装那些依赖的包,-race的时候默认已经是-installsuffix race,大家可以通过-n命令来验证 +-ldflags 'flag list' 传递参数给5l, 6l, 8l 调用 +-tags 'tag list' 设置在编译的时候可以适配的那些tag + +### go clean +目的:**移除当前源码包和关联源码包里面编译生成的文件** + + +这些文件包括: +``` +_obj/ 旧的object目录,由Makefiles遗留 +_test/ 旧的test目录,由Makefiles遗留 +_testmain.go 旧的gotest文件,由Makefiles遗留 +test.out 旧的test记录,由Makefiles遗留 +build.out 旧的test记录,由Makefiles遗留 +*.[568ao] object文件,由Makefiles遗留 + +DIR(.exe) 由go build产生 +DIR.test(.exe) 由go test -c产生 +MAINFILE(.exe) 由go build MAINFILE. +``` + + +常用于清除编译文件,然后GitHub递交源码,在本机测试的时候这些编译文件都是和系统相关的,但是对于源码管理来说没必要。 + + +`go clean -i -n` + +**参数介绍** + +-i 清除关联的安装的包和可运行文件,也就是通过go install安装的文件 +-n 把需要执行的清除命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的 +-r 循环的清除在import中引入的包 +-x 打印出来执行的详细命令,其实就是-n打印的执行版本 + + +### go fmt <文件名>.go +- **格式化代码文档**,不符合标准格式编译不通过。 +- 一般编译器都支持保存时自动格式化,背后就是调用go fmt,而go fmt就是调用的gofmt +- 需要参数-w,否则格式化结果不会写入文件 +- gofmt -w -l src,可以格式化整个项目。 + + +gofmt的**参数介绍** + +-l 显示那些需要格式化的文件 +-w 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。 +-r 添加形如“a[b:len(a)] -> a[b:]”的重写规则,方便我们做批量替换 +-s 简化文件中的代码 +-d 显示格式化前后的diff而不是写入文件,默认是false +-e 打印所有的语法错误到标准输出。如果不使用此标记,则只会打印不同行的前10个错误。 +-cpuprofile 支持调试模式,写入相应的cpufile到指定的文件 + + +### go get +目的:**动态获取远程代码包** + + +- 目前支持的有BitBucket、GitHub、Google Code和Launchpad,需要安装对应的源码管理工具,且加入环境变量,比如安装了Git。 +- 这个命令在内部实际上分成了两步操作: + - 第一步是下载源码包 + - 第二步是执行go install +- 其实go get支持自定义域名的功能,具体参见go help remote。 + + +**参数介绍**: + +-d 只下载不安装 +-f 只有在你包含了-u参数的时候才有效,不让-u去验证import中的每一个都已经获取了,这对于本地fork的包特别有用 +-fix 在获取源码之后先运行fix,然后再去做其他的事情 +-t 同时也下载需要为运行测试所需要的包 +-u 强制使用网络去更新包和它的依赖包 +-v 显示执行的命令 + + +### go install +实际执行了2步: +- 第一步是生成结果文件(可执行文件或者.a包),一般就是build,所以支持build的编译参数。 +- 第二步会把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin。 + + +### go test +目的:自动读取源码目录下面名为`*_test.go`的文件,生成并运行测试用的可执行文件. 默认不需要任何参数。 + + +常用的**参数**: +-bench regexp 执行相应的benchmarks,例如 -bench=. +-cover 开启测试覆盖率 +-run regexp 只运行regexp匹配的函数,例如 -run=Array 那么就执行包含有Array开头的函数 +-v 显示测试的详细命令 + + +### go tool +go tool下面下载聚集了很多命令,常用fix和vet + +- go tool fix . 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1,例如API的变化 +- go tool vet directory|files 用来分析当前目录的代码是否都是正确的代码 + >例如是不是调用fmt.Printf里面的参数不正确,例如函数里面提前return了然后出现了无用代码之类的。 + + +### go generate +用于在编译前自动化生成某类代码。 + +- go generate和go build是完全不一样的命令,通过分析源码中特殊的注释,然后执行相应的命令。 +- 这些命令都是很明确的,**没有任何的依赖**在里面。 +- 而且大家在用这个之前心里面一定要有一个理念,**这个go generate是给你用的,不是给使用你这个包的人用的,是方便你来生成一些代码的**。 + + +举个例子: +我们经常会使用`yacc`来生成代码,那么我们常用这样的命令: + +>go tool yacc -o gopher.go -p parser gopher.y + +-o 指定了输出的文件名, -p指定了package的名称。这是一个单独的命令,如果我们想让go generate来触发这个命令,那么就可以在**当前目录的任意一个xxx.go文件**里面的**任意位置**增加一行如下的注释: + +>//go:generate go tool yacc -o gopher.go -p parser gopher.y + +这里我们注意了,`//go:generate`是**没有任何空格**的,固定格式,在扫描源码文件的时候就是根据这个来判断的。 + +所以我们可以通过如下的命令来生成,编译,测试。如果gopher.y文件有修改,那么就重新执行go generate重新生成文件就好。 +``` +$ go generate +$ go build +$ go test +``` + +### godoc +目的:打印附于Go语言程序实体上的文档 +- go doc已经废弃,现在是godoc。 + +- go get golang.org/x/tools/cmd/godoc + + +通过命令在命令行执行 godoc -http=:端口号 比如godoc -http=:8080。然后在浏览器中打开127.0.0.1:8080,你将会看到一个golang.org的本地copy版本,通过它你可以查询pkg文档等其它内容。如果你设置了GOPATH,在pkg分类下,不但会列出标准包的文档,还会列出你本地GOPATH中所有项目的相关文档,这对于经常被墙的用户来说是一个不错的选择。 + + + +### 其他命令 +go version 查看go当前的版本 +go env 查看当前go的环境变量 +go list 列出当前全部安装的package +go run 编译并运行Go程序 +go fix 把Go语言源码文件中的旧版本代码修正为新版本的代码 +go vet 是一个用于检查Go语言源码中静态错误的简单工具 + + +# Go语言基础语法 + +## hello world + +### 关键字 25个 +**关键字不能用于自定义名字,只能在特定语法结构中使用** + + +break default **func** interface **select** +case **defer** go **map** **struct** +**chan** else **goto** package switch +const fallthrough if **range** type +continue for import return var + +### 保留字 37个 +这些内部预先定义的名字**并不是关键字**,你**可以在定义中重新使用它们**。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。 + + +- Constants: true false iota nil + +- Types: +``` +int int8 int16 int32 int64 +uint uint8 uint16 uint32 uint64 uintptr +float32 float64 complex128 complex64 +bool byte rune string error +``` + +- Functions: +``` +make len cap new append copy close delete +complex real imag +panic recover +``` + +### 写个hello world +- 每一个可独立运行的Go程序,必定包含一个package main,在这个main包中必定包含一个入口函数main,而这个函数既没有参数,也没有返回值。 +- Go使用package(和Python的模块类似)来组织代码。 +- main.main()函数(这个函数位于主包)是每一个独立的可运行程序的入口点。 +- Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者之一),所以它天生支持多语言。 + + +## 基础 + +### 变量 +>**Go把变量类型放在变量名后面** + +- 使用**var**关键字是Go最基本的定义变量方式: `var variableName type` + - 定义多个变量: `var vname1, vname2, vname3 type` + - 定义变量并初始化值: `var variableName type = value` + - 同时初始化多个变量: `var vname1, vname2, vname3 type= v1, v2, v3` +- 简化定义——忽略类型:`var vname1, vname2, vname3 = v1, v2, v3` +- 再简化:`vname1, vname2, vname3 := v1, v2, v3` + - `:=这`个符号直接取代了var和type,这种形式叫做简短声明. + - 只能用在函数内部; + - 在函数外部使用则会无法编译通过,所以一般用**var**方式来定义全局变量。 + + +- `_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。 **有什么用呢?后面解释**。 例如:`_, b := 34, 35`,就是丢弃34. +- Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i但未使用。 + + +### 常量 + **定义语法**: +```go +const constantName = value +//如果需要,也可以明确指定常量的类型: +const Pi float32 = 3.1415926 +``` + + +**常见例子**: +```go +const Pi = 3.1415926 +const i = 10000 +const MaxThread = 10 +const prefix = "astaxie_" +``` +- Go 语言中没有枚举(enum)的概念,一般可以用常量的方式来模拟枚举。 + + +>**特别之处**: 可以指定相当多的小数位数(例如200位), 若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit + +### 内置基础类型 + +#### Boolean + +布尔值的类型为`bool`,值是true或false,**默认**为`false`。 +```go +//示例代码 +var isActive bool // 全局变量声明 +var enabled, disabled = true, false // 忽略类型的声明 +func test() { + var available bool // 一般声明 + valid := false // 简短声明 + available = true // 赋值操作 +} +``` + + +#### 数值类型 + +- 整数分为有符号和无符号两种: + - 有符号:int,int8, int16, int32, int64,rune(=int32) + - 无符号:uint, uint8, uint16, uint32, uint64, byte(=uint8) + - +>这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 **没法强转嘛?** + +```go +var a int8 + +var b int32 + +c:=a + b +``` + +- 浮点数的类型有float32和float64两种(没有float类型),默认是float64。 +- 复数: complex64,complex128(64位实数+64位虚数),默认类型128。 +```go +// 复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位 +var c complex64 = 5+5i +//output: (5+5i) +fmt.Printf("Value is: %v", c) +``` + + +#### 字符串 +字符串是用一对双引号(`""`)或反引号(``)括起来定义,它的类型是string。 + +- 字符串是不可变的,下面代码会报错:cannot assign to s[0] +```go +var s string = "hello" +s[0] = 'c' +``` +- 真想修改可以迂回: +```go +s := "hello" +c := []byte(s) // 将字符串 s 转换为 []byte 类型 +c[0] = 'c' +s2 := string(c) // 再转换回 string 类型 +fmt.Printf("%s\n", s2) +``` +- 字符串可以做切片,用作修改 +```go +s := "hello" +s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 +fmt.Printf("%s\n", s) +``` +- ` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。下面变量输出的话也是分行的。 +```go +m := `hello + world` +``` + + +#### array 定长数组 +定义: +```go +var arr [n]type +``` + +读取和赋值 +```go +var arr [10]int // 声明了一个int类型的数组 +arr[0] = 42 // 数组下标是从0开始的 +arr[1] = 13 // 赋值操作 +fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 +fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0 +``` + + +- 长度是数组类型的一部分,长度不同那就是不一样的类型。 长度不可改变。 +- **数组之间的赋值**是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。 + - 是否可以把一个数组直接赋值给另一个数组? 长度不一样可以不? 一样可以不? + + +#### slice 动态数组 +- 要创建一个长度不为 0 的空 slice,需要使用内建函数 make。 + +```go +s := make([]string, 3) +``` + +- slice 支持内建函数 append, 该函数会返回一个包含了一个或者多个新值的 slice。 注意由于 append 可能返回一个新的 slice,我们需要接收其返回值。 + +#### map + + + +#### 错误类型 +Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误: +```go +err := errors.New("emit macho dwarf: elf header corrupted") +if err != nil { + fmt.Print(err) +} +``` + +#### 内置接口error +```go + type error interface { //只要实现了Error()函数,返回值为String的都实现了err接口 + + Error() String + + } + +``` + +#### 分组声明 用括号() +同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明 +```go +import "fmt" +import "os" + +const i = 100 +const pi = 3.1415 +const prefix = "Go_" + +var i int +var pi float32 +var prefix string +``` +分组规整为: +```go +import( + "fmt" + "os" +) + +const( + i = 100 + pi = 3.1415 + prefix = "Go_" +) + +var( + i int + pi float32 + prefix string +) +``` + +#### iota枚举 +这个关键字用来声明enum的时候采用,它默认开始值是0,const中每增加一行加1 + + +目前感觉没啥卵用,先不介绍了。 + +### 运算符 + +**算数运算符** + + 、 - 、 * 、 / 、% 没啥区别 + +- 注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。 + + + +**关系运算符** + == 、 != 、 > 、>= 、 < 、 <= ,也没啥区别 + + +**逻辑运算符** +&&、 ll 、 ! ,好像也没啥区别,会阻断,没有单个的?单个的是位运算符 + + +**位运算符** +位运算符对整数在内存中的二进制位进行操作, 很少用 +运算符 | 描述 +---------|---------- +& | 参与运算的两数各对应的二进位相与。(两位均为1才为1) +l | 参与运算的两数各对应的二进位相或。(两位有一个为1就为1) +^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1) +<< | 左移n位就是乘以2的n次方。“a<> | 右移n位就是除以2的n次方。“a>>b”是把a的各二进位全部右移b位。 + + +**赋值运算符** +没啥区别:+= 、&= + +### 类型 type + +#### 类型判断 +```go +value, ok := interface{}(container).([]string) +``` + +### 控制流 + +#### if +- 在条件语句之前可以有一个声明语句;在这里声明的变量可以在这个语句所有的条件分支中使用。 +```go + if num := 9; num < 0 { + fmt.Println(num, "is negative") + } else if num < 10 { + fmt.Println(num, "has 1 digit") + } else { + fmt.Println(num, "has multiple digits") + } +``` +- Go 没有三目运算符 + + +#### for + +- for循环,唯一的循环,多种形式。 +```go +for initialization; condition; post { + // zero or more statements +} +``` + - **initialization语句是可选的**,在循环开始前执行。initalization如果存在,必须是一条简单语句(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。 + - condition是一个布尔表达式(boolean expression),其值在**每次循环迭代开始时计算**。如果为true则执行循环体语句。 + - post语句在**循环体执行结束后执行**,**之后再次**对condition求值。 +- for循环的这三个部分每个都可以省略,如果省略initialization和post,分号也可以省略(相当于 while): +```go +// a traditional "while" loop +for condition { + // ... +} +``` +- 省略掉condition,变成for{} ,无限循环,可以用 break,return终止。 +- for循环的另一种形式,**在某种数据类型的区间(range)上遍历**,如字符串或切片。 + +```go +// Echo2 prints its command-line arguments. +package main + +import ( + "fmt" + "os" +) + +func main() { + s, sep := "", "" + for _, arg := range os.Args[1:] { + s += sep + arg + sep = " " + } + fmt.Println(s) +} +``` + - 每次循环迭代,range产生**一对值**;**索引以及在该索引处的元素值**。 + - 这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引 + - 一种思路是把索引赋值给一个临时变量(如temp)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables) + - go语言提供了一种解决方案:`空标识符(blank identifier)`,即`_`(也就是下划线)。 + - 空标识符可用于在任何语法**需要变量名但程序逻辑不需要**的时候 +```go +// Echo2 prints its command-line arguments. +package main + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + s, sep := "", "" + for _, arg := range os.Args[1:] { + s += sep + arg + sep = " " + } + fmt.Println(s) + fmt.Println(strings.Join(os.Args[1:], " ")) + fmt.Println("methodName:" + os.Args[0]) + + for i, arg := range os.Args[1:] { + fmt.Println(strconv.FormatInt(int64(i), 10) + ":" + arg) + } + +} +``` + + + +#### switch 多路选择 +```go +switch coinflip() { +case "heads": + heads++ +case "tails": + tails++ +default: + fmt.Println("landed on edge!") +} +``` +- Go语言并不需要显式地在每一个case后写break,语言**默认**执行完case后的逻辑语句会**自动退出**。 +- 如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。很少用。 +- 像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用(译注:比其它语言丰富) +- **continue**会跳过内层的循环,如果我们想**跳过的是更外层**的循环的话,我们可以在**相应的位置加上label** +>类似goto,但这种行为更多地被用到机器生成的代码中 + +- Go语言里的switch还可以**不带操作对象**(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较)。不带表达式的 switch 是实现 if/else 逻辑的另一种方式。 +```go + func Signum(x int) int { + switch { + case x > 0: + return +1 + default: + return 0 + case x < 0: + return -1 + } +} +``` +- 在同一个 case 语句中,你可以使用逗号来分隔多个表达式。 +```go +switch time.Now().Weekday() { + case time.Saturday, time.Sunday: + fmt.Println("It's the weekend") + default: + fmt.Println("It's a weekday") + } +``` + +- 类型开关 (type switch) 比较类型而非值。可以用来发现一个接口值的类型。 在这个例子中,变量 t 在每个分支中会有相应的类型。 + +```go + whatAmI := func(i interface{}) { + switch t := i.(type) { + case bool: + fmt.Println("I'm a bool") + case int: + fmt.Println("I'm an int") + default: + fmt.Printf("Don't know type %T\n", t) + } + } + whatAmI(true) + whatAmI(1) + whatAmI("hey") +``` + + +#### 标签和跳转 +Go语言使用标签(Lable)来标识一个语句的位置,用于goto、break、continue语句的跳转,语法如下: +```go +Lable:Statemaent +``` +- goto语句用于函数内部的跳转,需要配合标签一起使用: +```go + //跳转到标签后的语句执行 + goto Lable +``` +- goto只能在函数内跳转 +- goto语句不能跳过内部变量声明语句,这些变量在goto语句的标签语句处又是可见的。 +- goto语句只能跳到同级作用于或者上层作用域,不能跳到内部作用域。 + + +- break + - 用于跳出for、switch、select语句的执行,单独使用是跳出当前break所在的语句内层循环。 + - 和标签一起使用,跳出标签所表示的for、switch、select语句的执行。 标签和break必须在同一个函数内。 + + +### 函数 +函数在go语言中是一等公民,既起到”胶水“的作用,也是为其他语言特性起到底层支撑作用。 +>命名类型的方法本质上是一个函数,类型方法是go语言面向对象的实现基础。 接口底层同样是同样是通过指针和函数将接口和接口实例连接在一起。 +- 支持闭包,支持可变参数, **返回值也支持有多个**。 + - 如果多值有**错误类型,一般作为最后一个返回值**。 +- 函数是一种类型,函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如: +``` +假设 f1 需要 3 个参数 f1(a, b, c int),同时 f2 返回 3 个参数 f2(a, b int) (int, int, int),就可以这样调用 f1:f1(f2(a, b))。 +``` +- 在 Go 里面函数**重载是不被允许**的.报错`funcName redeclared in this book, previous declaration at lineno`. + - Go 语言不支持这项特性的主要原因是函数重载**需要进行多余的类型匹配影响性能**;没有重载意味着只是一个简单的函数调度。 +- 函数**值**(functions value)之间**可以相互比较**:如果它们引用相同的函数或者都是 nil 的话,则认为它们是相同的函数。 +- 函数不能在其它函数里面声明(**不能嵌套**),不过我们可以通过使用**匿名函数**来破除这个限制。 +```go + func add(a, b int) (sum int){ + anonymous := func(x, y int) int { + return x + y + } + return anonymous(a, b) + } + +``` +- 支持有名的返回值,参数名相当于函数体内最外层的局部变量,命名返回值会被初始化为类型零值,最后return可以不带参数直接返回。 + - 如果在函数里对命名返回值的变量重新定义 `sum := a + b`,那return的时候就需要带上sum。 +- 不支持默认值参数? +- 目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。 + - 不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch)与 / 或者通过使用反射(reflection)来实现相似的功能。 + - 但这回影响性能,让代码变得复杂,最好是为每一个类型单独创建一个函数。 + + +#### 按值传递 VS 按引用传递 +**按值传递(call by value)、按引用传递(call by reference)** + + +- Go **默认使用按值传递**来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量。 +- 希望可以直接修改参数的值,需要将**参数的地址**(**变量名前面添加 & 符号**,比如 &variable)传递给函数,这就是所谓的按引用传递,比如 Function(&arg1)。 + - 此时传递给函数的是一个指针。 + - 如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制; + - 我们可以通过这个指针的值来修改这个值所指向的地址上的值。 + - 指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,**按引用传递也是按值传递**。 +- 在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针) +- 传递指针(一个 32 位或者 64 位的值)的消耗都比传递副本来得少。 + - 如果一个函数需要返回四到五个值,我们可以传递: + - 一个切片给函数(如果返回值具有相同类型) + - 或者是传递一个结构体(如果返回值具有不同的类型) + + +#### 命名的返回值 +- 当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)。单个可以不用括号。 +- 命名返回值作为结果形参(result parameters)被初始化为相应类型的零值。当需要返回的时候,我们**只需要一条简单的不带参数的 return 语句**。 +>需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。 +>return 或 return var 都是可以的。不过 return var = expression(表达式) 会引发一个编译错误 + + +**尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。** + +传递指针进入函数,不再需要使用return返回。 + +#### 传递变长参数 +- 长度可以为0,形如...type +```go +func myFunc(a, b, arg ...int) {} +``` +- 类似**某个类型**的 slice 的参数,可以通过 slice... 的形式来传递参数调用变参函数。也可以用访问切片的方式访问变长参数。 +```go + package main + +import "fmt" + +func main() { + x := min(1, 3, 2, 0) + fmt.Printf("The minimum is: %d\n", x) + slice := []int{7,9,3,5,1} + //注意传递的参数形式 + x = min(slice...) + fmt.Printf("The minimum in the slice is: %d", x) +} + +func min(s ...int) int { + if len(s)==0 { + return 0 + } + min := s[0] + for _, v := range s { + if v < min { + min = v + } + } + return min +} + +``` +- 不定参数类型必须是相同的。 +- 不定参数必须是函数最后一个参数。 +- 如果多个参数的**类型并不是都相同**的呢?使用 5 个参数来进行传递并不是很明智的选择,有 2 种方案可以解决这个问题: + 1. 使用结构,类似定义对象。 + 2. 使用空接口,这样就可以接受任何类型的参数。 + - 该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。 + - 一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断: +```go +func typecheck(..,..,values … interface{}) { + for _, value := range values { + switch v := value.(type) { + case int: … + case float: … + case string: … + case bool: … + default: … + } + } +} +``` + +#### defer和追踪,recover +- 关键字`defer`允许我们推迟到函数返回之前(或任意位置执行return语句之后)一刻才执行某个语句或函数。 +- 类似`finally`语句块,它一般用于释放某些已分配的资源。 + - 关闭文件流:`defer file.Close()` + - 解锁一个加锁的资源: `defer mu.Unlock()` + - 打印最终报告:`defer printFooter()` + - 关闭数据库链接:`defer disconnectFromDB()` +- 使用 defer 的语句同样可以接受参数,下面这个例子就会在执行 defer 语句时打印 0: +```go + i := 0 + defer fmt.Println(i) + i++ + return +} +``` +- 当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出): +```go +func f() { + for i := 0; i < 5; i++ { + defer fmt.Printf("%d ", i) + } +} +//上面的代码将会输出:4 3 2 1 0 +``` +- 使用 defer 语句实现代码追踪。一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息 +- 使用 defer 语句来记录函数的参数与返回值. + + +**类似try...catch: defer 和 recover** +```go +func get(index int) (ret int) { + defer func() { + if r := recover(); r != nil { + fmt.Println("Some error happened!", r) + ret = -1 + } + }() + arr := [3]int{2, 3, 4} + return arr[index] +} + +func main() { + fmt.Println(get(5)) + fmt.Println("finished") +} +``` +能正常运行到结束。 +``` +Some error happened! runtime error: index out of range [5] with length 3 +-1 +finished +``` +- 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。 +- 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。 + + + +#### 错误处理 +- 函数实现过程中如果出现不能处理的错误,可以返回跟调用者处理。 + - 比如我们调用标准库函数os.Open读取文件,os.Open 有2个返回值,第一个是 *File,第二个是 error。 + - 如果调用成功,error 的值是 nil。 + - 如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息 + - 可以通过 errorw.New 返回自定义的错误 + +```go +import ( + "errors" + "fmt" +) + +func hello(name string) error { + if len(name) == 0 { + return errors.New("error: name is null") + } + fmt.Println("Hello,", name) + return nil +} + +func main() { + if err := hello(""); err != nil { + fmt.Println(err) + } +} +// error: name is null +``` + +- error 往往是能预知的错误,但是也可能出现一些**不可预知的错误**,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 **panic**。 + +#### 内置函数 +Go 语言拥有一些**不需要进行导入**操作就可以使用的内置函数。 +- 它们有时可以针对不同的类型进行操作,例如:len、cap 和 append +- 或必须用于系统级的操作,例如:panic。 +- 因此,它们需要直接获得编译器的支持。 + +```go +append -- 用来追加元素到数组、slice中,返回修改后的数组、slice +close -- 主要用来关闭channel +delete -- 从map中删除key对应的value +panic -- 停止常规的goroutine (panic和recover:用来做错误处理) +recover -- 允许程序定义goroutine的panic动作 +real -- 返回complex的实部 (complex、real imag:用于创建和操作复数) +imag -- 返回complex的虚部 +make -- 用来分配内存,返回Type本身(只能应用于slice, map, channel), make (type) +new -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针 +cap -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) +copy -- 用于复制和连接slice,返回复制的数目 +len -- 来求长度,比如string、array、slice、map、channel ,返回长度 +print、println -- 底层打印函数,在部署环境中建议使用 fmt 包 +``` +- new 和 make 均是用于分配内存: + - new 用于值类型和用户定义的类型,如自定义结构, + - make 用于内置引用类型(切片、map 和管道)。 +- 它们的用法就像是函数,但是将类型作为参数:new (type)、make (type)。 + - new (T) 分配类型 T 的**零值并返回其地址**,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。 + - make (T) 返回类型 T 的**初始化之后的值**,因此它比 new 进行更多的工作. + + +#### 递归函数 +当一个函数在其函数体内调用自身,则称之为递归。最经典的例子便是计算斐波那契数列,即前两个数为 1,从第三个数开始每个数均为前两个数之和。 +```go +func fibonacci(n int) (res int) { + if n <= 1 { + res = 1 + } else { + res = fibonacci(n-1) + fibonacci(n-2) + } + return +} + +``` +一般出现大量递归调用会导致程序栈内存分配耗尽,这个问题可以通过 **懒惰求值**的技术解决。 而Go语言中,可以使用管道和goroutine来实现。 + + +#### 将函数作为参数 +函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。 +```go +func main() { + callback(1, Add) +} + +func Add(a, b int) { + fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b) +} + +func callback(y int, f func(int, int)) { + f(y, 2) // this becomes Add(1, 2) +} + +``` + +#### 闭包(匿名函数?) +当我们不希望给函数起名字的时候,可以使用匿名函数,例如:`func(x, y int) int { return x + y }` + + +**defer 语句和匿名函数** +关键字 defer (详见第 6.4 节)经常配合匿名函数使用,它可以用于**改变函数的命名返回值**。 +- 下面例子中最后打印的是2 +```go +func f() (ret int) { //命名返回值 ret,所以defer中做的改变会生效,影响最终的返回值。 + defer func() { + ret++ + }() + return 1 +} +func main() { + fmt.Println(f()) //2 +} +``` +匿名函数还可以配合 go 关键字来作为 goroutine 使用(详见第 14 章和第 16.9 节)。 + +匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。 + + + +## 结构体、方法和接口 + +### 结构体 +结构体**类似于其他语言中的 class**,可以在结构体中定义多个字段,为结构体实现方法,实例化等。 +- 字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。 +- func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了 +- 调用方法通过 实例名.方法名(参数) 的方式。 +```go +type Student struct { + name string + age int +} +func (stu *Student) hello(person string) string { + return fmt.Sprintf("hello %s, I am %s", person, stu.name) +} + +func main() { + stu := &Student{ + name: "Tom", + } + msg := stu.hello("Jack") + fmt.Println(msg) // hello Jack, I am Tom +} +``` +- 还可以使用 new 实例化。 +``` +func main() { + stu2 := new(Student) + fmt.Println(stu2.hello("Alice")) // hello Alice, I am , name 被赋予默认值"" +} + +``` + +### 方法 + + + +### 接口 +- 接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。 +- Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可,必须全部实现,否则会报错。 + + +**如何确保某个类型实现了某个接口的所有方法呢?** + +一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。 +```go +var _ Person = (*Student)(nil) +var _ Person = (*Worker)(nil) +``` +将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。 + + +- 实例可以强制类型转换为接口,接口也可以强制类型转换为实例。 +```go +func main() { + var p Person = &Student{ + name: "Tom", + age: 18, + } + + stu := p.(*Student) // 接口转为实例 + fmt.Println(stu.getAge()) +} +``` + +**空接口**: +如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。**有点像泛型**。 +```go +func main() { + m := make(map[string]interface{}) + m["name"] = "Tom" + m["age"] = 18 + m["scores"] = [3]int{98, 99, 85} + fmt.Println(m) // map[age:18 name:Tom scores:[98 99 85]] +} + +``` + + +--- + +# Go并发 +**并发**在微观层面,任务不是同时运行。**并行**是多个任务同时运行。 + +**线程**也叫轻量级进程,通常一个进程包含若干个线程。 +- 线程可以利用进程所拥有的资源。 +- 在引入线程的操作系统中,通常都是把**进程作为分配资源的基本单位**,而把**线程作为独立运行和独立调度的基本单位** + >比如音乐进程,可以一边查看排行榜一边听音乐,互不影响。 + +## 并发编程(Goroutine) +Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。 + +### sync +例如我们希望并发下载 N 个资源,多个并发协程之间**不需要通信**,那么就可以使用 `sync.WaitGroup`,等待所有并发协程执行结束。 +```go +import ( + "fmt" + "sync" + "time" +) + +var wg sync.WaitGroup + +func download(url string) { + fmt.Println("start to download", url) + time.Sleep(time.Second) // 模拟耗时操作 + //减去一个计数。 + wg.Done() +} + +func main() { + for i := 0; i < 3; i++ { + //为 wg 添加一个计数 + wg.Add(1) + //启动新的协程并发执行 download 函数 + go download("a.com/" + string(i+'0')) + } + //等待所有的协程执行结束。 + wg.Wait() + fmt.Println("Done!") +} +``` + + +### channel +```go +var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道 + +func download(url string) { + fmt.Println("start to download", url) + time.Sleep(time.Second) + ch <- url // 将 url 发送给信道 +} + +func main() { + for i := 0; i < 3; i++ { + go download("a.com/" + string(i+'0')) + } + for i := 0; i < 3; i++ { + msg := <-ch // 等待信道返回消息。 + fmt.Println("finish", msg) + } + fmt.Println("Done!") +} +``` +- 使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息。 + + +--- + +# 包和模块 + +## 包 +一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。 + +如果calc.go和main.go平级,分别定义add和main方法,运行 go run main.go,会报错,add 未定义: +```go +./main.go:6:14: undefined: add +``` +因为 go run main.go 仅编译 main.go 一个文件,所以命令需要换成:`go run main.go calc.go` + + +## 模块 +- Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。 + Go Modules 可以算作是较为完善的包管理工具。 + 同时支持代理,国内也能享受高速的第三方包镜像服务。 + + +**go mod 的使用** +- 环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。 + + +--- + +# 网络编程 + + + + +--- +# 单元测试 +假设我们希望测试 package main 下 `calc.go` 中的函数,要只需要新建 `calc_test.go` 文件,在calc_test.go中**新建测试用例**即可。 + +- 运行 go test,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v参数。 + + +# GC +Go的GC只会清理被分配到堆上的、不再有任何引用的对象。 + + +# 项目实战 + +## 适合企业开发者的目录结构 +![](../img/go入门/go入门_2022-04-26-19-17.png) + +``` + +``` + + + +# 参考资料 +- 《Go入门指南》转自链接:https://learnku.com/docs/the-way-to-go/introduce/3599 +- 《简明教程》:https://geektutu.com/post/quick-go-gin.html +- 《跟着煎鱼学Go》:https://eddycjy.gitbook.io/golang/di-10-ke-pa-chong/go2018 +- 《Go by Example中文版》:https://gobyexample-cn.github.io/ +- 《Go 编程基础》--无闻:https://github.com/unknwon/go-fundamental-programming +- https://books.studygolang.com/gopl-zh/ch3/ch3-01.html +- web框架 + - 《Echo文档》:https://www.bookstack.cn/read/go-echo/README.md \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\345\255\246\344\271\240\350\265\204\346\226\231\346\261\207\346\200\273\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\345\255\246\344\271\240\350\265\204\346\226\231\346\261\207\346\200\273\343\200\213.md" new file mode 100644 index 0000000..f3dd7da --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\345\255\246\344\271\240\350\265\204\346\226\231\346\261\207\346\200\273\343\200\213.md" @@ -0,0 +1,240 @@ +# Go 成神之路 + +# 那些优质且权威的Go语言学习资料 (Tony Bai) + + +## 书籍 + +1. No.5 [《The Way To Go》](https://github.com/Unknwon/the-way-to-go_ZH_CN)- Ivo Balbaert Go 语言百科全书 + - 为什么学习 Go 以及 Go 环境安装入门; + - Go 语言核心语法; + - Go 高级用法(I/O 读写、错误处理、单元测试、并发编程、socket 与 web 编程等); + - Go 应用(常见陷阱、语言应用模式、从性能考量的代码编写建议、现实中的 Go 应用等)。 +2. No.4 [《Go 101》](https://github.com/golang101/golang101)- Go 语言参考手册 + - Go 语法基础; + - Go 类型系统与运行时实现; + - 以专题(topic)形式阐述的 Go 特性、技巧与实践模式。 +3. No.3 《Go 语言学习笔记》- Go 源码剖析与实现原理探索 。 这本书整体上分为两大部分: + - **Go 语言详解**:以短平快、“堆干货”的风格对 Go 语言语法做了说明,能用示例说明的,绝不用文字做过多修饰; + - **Go 源码剖析**:这是这本书的精华,也是最受 Gopher 们关注的部分。这部分对 Go 运行时神秘的内存分配、垃圾回收、并发调度、channel 和 defer 的实现原理、sync.Pool 的实现原理都做了细致的源码剖析与原理总结。 +4. No.2 《Go 语言实战》- 实战系列经典之作,紧扣 Go 语言的精华。 这本书的结构框架: + - 入门:快速上手搭建、编写、运行一个 Go 程序; + - 语法:数组(作为一个类型而存在)、切片和 map; + - Go 类型系统的与众不同:方法、接口、嵌入类型; + - Go 的拿手好戏:并发及并发模式; + - 标准库常用包:log、marshal/unmarshal、io(Reader 和 Writer); + - 原生支持的测试。 +5. No.1 《Go 程序设计语言》- 人手一本的 Go 语言“圣经” + + +## 文档 + +- Go官方文档——最权威的Go语言资料: https://go.dev/doc/ + + +每个 Gopher **必看的内容**: +Go 官方文档中的[Go 语言规范](https://go.dev/ref/spec)、[Go module 参考文档](https://go.dev/ref/mod)、[Go 命令参考手册](https://go.dev/doc/cmd)、[Effective Go](https://go.dev/doc/effective_go)、[Go 标准库包参考手册](https://pkg.go.dev/std)以及[Go 常见问答](https://go.dev/doc/faq)等都是。 + +>我强烈建议你一定要抽出时间来仔细阅读这些文档。 + +## 博客、文章、日/周报、邮件列表 + +**博客** + + +- [Go语言官方博客](https://go.dev/blog/) +- [Go 核心团队技术负责人 Russ Cox 的个人博客](https://research.swtch.com/) +- [Go 核心开发者 Josh Bleecher Snyder 的个人博客](https://commaok.xyz/); +- [Go 核心团队前成员 Jaana Dogan 的个人博客](https://rakyll.org/); +- [Go 鼓吹者 Dave Cheney 的个人博客](https://dave.cheney.net/); +- [Go 语言培训机构 Ardan Labs 的博客](https://www.ardanlabs.com/blog); +- [GoCN 社区](https://gocn.vip/); +- [Go 语言百科全书:由欧长坤维护的 Go 语言百科全书网站](https://golang.design/)。 + + +**Go日报/周刊邮件列表** + + +- [Go 语言爱好者周刊](https://studygolang.com/go/weekly),由 Go 语言中文网维护; +- [Gopher 日报](https://github.com/bigwhite/gopherdaily),由我本人维护的 Gopher 日报项目,创立于 2019 年 9 月。 + + +## 开源项目 + + +## 技术演讲、大会、PPT +>关于 Go 技术演讲,我个人建议以各大洲举办的 GopherCon 技术大会为主,这些已经基本可以涵盖每年 Go 语言领域的最新发展。 + +- [Go 官方的技术演讲归档](https://go.dev/talks/),这个文档我强烈建议你按时间顺序看一下,通过这些 Go 核心团队的演讲资料,我们可以清晰地了解 Go 的演化历程; +- [GopherCon 技术大会](https://www.youtube.com/c/GopherAcademy/playlists),这是 Go 语言领域规模最大的技术盛会,也是 Go 官方技术大会; +- [GopherCon Europe](https://www.youtube.com/c/GopherConEurope/playlists) 技术大会-欧洲分会; +- [GopherConUK 技术大会](https://www.youtube.com/c/GopherConUK/playlists); +- [GoLab 技术大会](https://www.youtube.com/channel/UCMEvzoHTIdZI7IM8LoRbLsQ/playlists); +- [Go Devroom@FOSDEM](https://www.youtube.com/user/fosdemtalks/playlists); +- [GopherChina 技术大会](https://space.bilibili.com/436361287),这是中国大陆地区规模最大的 Go 语言技术大会,由 GoCN 社区主办。 + + +## 其他 + + +### 高级冷门 + +- [Go 语言项目的官方 issue 列表](https://github.com/golang/go/issues) + - 通过这个 issue 列表,我们可以实时看到 Go 项目演进状态,及时看到 Go 社区提交的各种 bug。 + - 同时,我们通过挖掘该列表,还可以了解某个 Go 特性的来龙去脉,这对深入理解 Go 特性很有帮助。 +- [Go 项目的代码 review 站点](https://go-review.googlesource.com/q/status:open+-is:wip) + - 通过阅读 Go 核心团队 review 代码的过程与评审意见,我们可以看到 Go 核心团队是如何使用 Go 进行编码的,能够学习到很多 Go 编码原则以及地道的 Go 语言惯用法,对更深入地理解 Go 语言设计哲学,形成 Go 语言编程思维有很大帮助。 + + +# 从小白到“老鸟”,我的Go语言进阶之路(孔令飞) +>个人在 Go 语言进阶过程中的一些经验、心得的分享。希望通过这些分享,能帮助到渴望在 Go 研发之路上走的更远的你。 + +## Go 语言能力级别划分 + +- **初级**:已经学习完 Go 基础语法课程,能够编写一些简单 Go 代码段,或者借助于 Google/Baidu 能够编写相对复杂的 Go 代码段;这个阶段的你基本具备阅读 Go 项目代码的能力; + +- **中级**:能够独立编写完整的 Go 程序,例如功能简单的 Go 工具等等,或者借助于 Google/Baidu 能够开发一个完整、简单的 Go 项目。此外,对于项目中涉及到的其他组件,我们也要知道怎么使用 Go 语言进行交互。在这个阶段,开发者也能够二次开发一个相对复杂的 Go 项目; + +- **高级**:不仅能够熟练掌握 Go 基础语法,还能使用 Go 语言高级特性,例如 channel、interface、并发编程等,也能使用面向对象的编程思想去开发一个相对复杂的 Go 项目; + +- **资深**:熟练掌握 Go 语言编程技能与编程哲学,能够独立编写符合 Go 编程哲学的复杂项目。同时,你需要对 Go 语言生态也有比较好的掌握,具备较好的软件架构能力; + +- **专家**:精通 Go 语言及其生态,能够独立开发大型、高质量的 Go 项目,编程过程中较少依赖 Google/ 百度等搜索工具,且对 Go 语言编程有自己的理解和方法论。除此之外,还要具有优秀的软件架构能力,能够设计、并部署一套高可用、可伸缩的 Go 应用。这个级别的开发者应该是团队的技术领军人物,能够把控技术方向、攻克技术难点,解决各种疑难杂症。 + +>初级、中级、高级 Go 语言工程师的关注点主要还是使用 Go 语言开发一个实现某种业务场景的应用,但是资深和专家级别的 Go 语言工程师,除了要具有优秀的 Go 语言编程能力之外,还需要具备一些**架构能力**. + + +## 进阶之路 + +### 开发者阶段 + +从初级入门 ---> 高级 + +#### 初级 + +1. **学习基础语法** + 1. 一般学习一门编程语言,都会快速阅读两本经典的、讲基础语法的书。《Go 程序设计语言》、《Go 语言编程》。 + 2. 还有余力的话,再看两本关于场景化编程的书籍:《Go 并发编程实战》(第 2 版)和《Go Web 编程》。 +2. **实战**,主要是通过编码实战加深对 Go 语法知识的理解和掌握。 + 1. 那么具体应该**如何实战呢?**, 核心就是**抄和改**。下面重点说一下: + 2. **找需求**:可以研究优秀的(开源)项目 + 1. 以需求为驱动,找到一个合理的需求,然后实现它。 + 2. 需求来源于工作。这些需求可以是产品经理交给你的某一个具体的产品需求,也可以是能够帮助团队 / 自己提高工作效率的工具。 + 3. 总之,如果有明确的工作需求最好,如果没有明确的需求,我们就要创造需求。 + 3. **如何找优秀开源项目?**。 (以要开发一个版本发布系统为例) + 1. 在 GitHub 上找到一个优秀的版本发布系统,并基于这个系统进行二次开发。通过这种方式,我不仅学习到了一个优秀开源项目的设计和实现,还以最快的速度完成了版本发布系统的开发。 + 2. 搜索框: `language:go 版本发布`,按照star排序 + 3. 看描述,开code + >研究完 GitHub 上的开源项目,这时候我还建议你通过[libs.garden](https://libs.garden/go),再查找一些开源项目。libs.garden 的主要功能是库(在 Go 中叫包)和应用的评分网站,是按不同维度去评分的. 再就是到 GitHub 上的 [awesome-go](https://github.com/avelino/awesome-go) 项目也根据分类记录了很多包和工具。 + 3. **如何进行二次开发?** + 1. 手动编译、部署这个开源项目。 + 2. 阅读项目的 README 文档,跟着 README 文档使用这个开源项目,至少运行一遍核心功能。 + 3. 阅读核心逻辑源码,在不清楚的地方,可以添加一些 fmt.Printf 函数,来协助你理解代码。 + 4. 在你理解了项目的核心逻辑或者架构之后,就可以尝试添加 / 修改一些匹配自己项目需求的功能,添加后编译、部署,并调试。 + 5. 二次开发完之后,你还需要思考下后续要不要同步社区的代码,如果需要,如何同步代码。 + >在你通过“抄”和“改”完成需求之后,记得还要编写文档,并找个合适的时机在团队中分享你的收获和产出。这点很重要,可以将你的学习输入变成工作产出。 + + +**这样调研很多项目,花费这么多时间的好处是什么?** +1. 最优解:你可以很有底气地跟老板说,这个方案在这个类别就是业界 No.1(开源的)。 +2. 高效:基于已有项目进行二次开发,可以提高开发和学习效率。 +3. 产出:在学习的过程中,也有工作产出。个人成长、工作贡献可以一起获得。 +4. 知识积累:为今后的开发生涯积累项目库和代码库。 + +#### 中级/高级工程师阶段 +中级 / 高级工程师阶段,其实就是不断地利用所学的 Go 基础知识,去编程实践。 + + +这个阶段提升 Go 研发能力的思路也跟前面是一样的: +工作中发现需求 -> 调研优秀的开源项目 -> 二次开发 -> 团队内分享。 + +>这个阶段,可以刻意地减少对 Google/Baidu 的依赖,尝试自己编码解决问题、实现需求。 + +在需要的时候,我也会使用 Go 的高级语法 channel、interface 等,结合面向对象编程的思想去开发项目或者改造开源项目。 + + +**案例一**:开发了 HTTP 文件服务器 +- 需求来源:因为经常需要将同一个二进制文件部署到不同的机器上,为了便于分发文件,我开发了一个[HTTP 服务器](https://github.com/alex8866/grapehttp); +- 调研项目:使用了开源的[gohttpserver](https://github.com/codeskyblue/gohttpserver); +- 效果:开发完成后,在团队中推广,有不少同事使用,显著提高了文件的分发效率。 + + +**案例二**:命令行模板 +- 需求来源:因为经常需要编写一些命令行工具,所以我每次都要重复开发一些命令行工具的基础功能,例如命令行参数解析、子命令等。为了避免重复开发这些基础功能,提高工具开发效率和易用度,我开发了一个命令行框架,[cmdctl](https://github.com/lexfei/cmdctl); +- 调研项目:参考了 Kubernetes 的[kubectl命令行工具](https://github.com/kubernetes/kubernetes/tree/master/cmd/kubectl)的实现; +- 效果:在工作中很多需要自动化的工作,都以命令行工具的形式,添加在了 cmdctl 命令框架中,大大提高了我的开发效率。 + + +**研究过的其他有趣的开源项目** +- [elvish](https://github.com/elves/elvish):Go 语言编写的 Linux Shell; +- [machinery](https://github.com/RichardKnop/machinery):Go 语言编写的分布式异步作业系统; +- [gopub](https://github.com/lisijie/gopub):Go 语言编写的的版本发布系统; +- [crawlab](https://github.com/crawlab-team/crawlab):Go 语言编写的分布式爬虫管理平台; + + +### 架构师阶段 + + +### 资深工程师 +架构师有很多方向,在云原生技术时代,转型为**云原生架构师**对学 Go 语言的我们来说是一个不错的选择。 + +要成为云原生架构师,首先要学习云原生技术。云原生技术有很多,我推荐的学习路线如下图所示: +![云原生架构师技术路线图](../../Computer-StudyNotes/img/《Go语言项目开发实战》/cloudTec.jpg) + +>如果你还有精力,还可以再学习下 TKEStask、Consul、Cilium、OpenShift 这些项目。 + +[参考资料](https://github.com/marmotedu/awesome-books): + +- 微服务:《微服务设计》 [英] Sam Newman +- Docker:《Docker 技术入门与实战》(第 3 版)杨保华 / 戴王剑 / 曹亚仑、《Docker ——容器与容器云》(第 2 版)浙江大学SEL实验室 +- Kubernetes : 《Kubernetes 权威指南:从 Docker 到 Kubernetes 实践全接触》(第 4 版)龚正 / 吴治辉 / 崔秀龙 / 闫健勇、《基于 Kubernetes 的容器云平台实战》 +- Knative:[Knative Documentation](https://knative.dev/docs/) +- Prometheus:[Prometheus Documentation](https://prometheus.io/docs/introduction/overview/) +- Jaeger :Jaeger Documentation +- KVM:《KVM 虚拟化技术 : 实战与原理解析》 +- Istio:《云原生服务网格 Istio:原理、实践、架构与源码解析》张超盟,章鑫,徐中虎,徐飞 +- Kafka:《Apache Kafka 实战》胡夕、《Apache Kafka 源码剖析》徐郡明 +- Etcd:etcd 实战课 +- Tyk:Tyk Open Source +- TKEStask:TKEStack Documentation +- Consul:Consul Documentation +- Cilium:Cilium Documentation +- OpenShift :《开源容器云 OpenShift:构建基于 Kubernetes 的企业应用云平台》 + + + +### 专家工程师 + +增强自己架构能力,而不是深入具体细节: + +- 调研竞品,了解竞品的架构设计和实现方式。 +- 参加技术峰会,学习其他企业的优秀架构设计,例如 ArchSummit 全球架构师峰会、QCon 等。 +- 参加公司内外组织的技术分享,了解最前沿的技术、架构和解决方案。 +- 关注一些优秀的技术公众号,学习其中高质量的技术文章。 +- 作为一名创造者,通过积极思考,设计出符合当前业务的优秀架构。 + + +在架构师阶段你仍然是一名技术开发者,一定不能脱离代码。你可以通过下面这几个方法,让自己保持 Code 能力:- 以 Coder 的身份参与一些核心代码的研发。 +以 Reviewer 的身份 Review 成员提交的 PR。 +工作之余,阅读项目其他成员开发的源码。 +关注一些优秀的开源项目,调研、部署并试用。 + + +- 研发层面和架构层面 走得更远。 + +- 能够兼具一个 Creator 的角色,能够从 0 到 1,构建满足业务需求的优秀软件系统,甚至能够独立开发一款备受欢迎的开源项目。 + +## 进阶之路的心得分享 + +1. 第一点:尽快打怪升级。 +2. 第二点:找对方法很重要。 + 1. 工作中发现需求 -> 调研优秀的开源项目 -> 二次开发 -> 团队内分享。 + 2. 以工作需求为驱动,一方面可以让你有较强的学习动力、学习目标, + 3. 另一方面可以使你在学习的过程中,也能在工作中有所产出,工作产出和学习两不误。 +3. 第三点:学架构,先学习当前业务的架构,再学习云原生架构。 + + +毕业 3~5 年的程序员可能是性价比最高的,要时间有时间,要经验有经验,并且当前所积累的研发技能,已经能或者通过后期的学习能够满足公司业务开发需求了。 + + +如何判断一个程序员的性价比呢?就是你的能力要跑赢你当前的年龄和薪资。想跑赢当前的年龄和薪资,需要你尽快地打怪练级,提升自己。 \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\345\234\243\347\273\217\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\345\234\243\347\273\217\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..fe69b32 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\345\234\243\347\273\217\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,3170 @@ + + +# 基础部分 + +## 第一章 总览Go语言能做啥 +通过十几个程序介绍了用Go语言如何实现类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。 + +### 入门 + + +- 缺少了必要的包或者导入了不需要的包,程序都无法编译通过。Go语言编译过程没有警告信息,争议特性之一 +- 不需要添加分号,除非一行有多填语句。编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析。 + - 行末出现:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字break、continue、fallthrough或return中的一个、运算符和分隔符++、--、)、]或}中的一个),会自动插入分号分割符。 + - 以+结尾的话不会被插入分号分隔符,所以 a + b, 可以在 + 号后面换行,不能在加号前面,因为 a后面换行会被插入分号,那编译就报错了。 + + +#### 命令行参数 +- **输入源:**自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。 +- **包导入顺序**并不重要;gofmt工具格式化时**按照字母顺序对包名排序**。 +- i--给i减1。它们是语句,而不像C系的其它语言那样是表达式。所以j = i++非法,而且++和--都**只能放在变量名后面**,因此--i也非法。 + + +**命令行参数** + + +- 程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。 +- os.Args变量是一个字符串(string)的切片(slice) +- os.Args的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数 + + +- for循环,唯一的循环,多种形式。 +```go +for initialization; condition; post { + // zero or more statements +} +``` + - initialization语句是可选的,在循环开始前执行。initalization如果存在,必须是一条简单语句(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。 + - condition是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为true则执行循环体语句。 + - post语句在循环体执行结束后执行,**之后再次**对condition求值。 +- for循环的这三个部分每个都可以省略,如果省略initialization和post,分号也可以省略(相当于 while): +```go +// a traditional "while" loop +for condition { + // ... +} +``` +- 省略掉condition,变成for{} ,无限循环,可以用 break,return终止。 +- for循环的另一种形式,**在某种数据类型的区间(range)上遍历**,如字符串或切片。 + +```go +// Echo2 prints its command-line arguments. +package main + +import ( + "fmt" + "os" +) + +func main() { + s, sep := "", "" + for _, arg := range os.Args[1:] { + s += sep + arg + sep = " " + } + fmt.Println(s) +} +``` + - 每次循环迭代,range产生一对值;索引以及在该索引处的元素值。 + - 这个例子不需要索引,但range的语法要求,要处理元素,必须处理索引 + - 一种思路是把索引赋值给一个临时变量(如temp)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables) + - go语言提供了一种解决方案:`空标识符(blank identifier)`,即`_`(也就是下划线)。 + - 空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候 +```go +// Echo2 prints its command-line arguments. +package main + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + s, sep := "", "" + for _, arg := range os.Args[1:] { + s += sep + arg + sep = " " + } + fmt.Println(s) + fmt.Println(strings.Join(os.Args[1:], " ")) + fmt.Println("methodName:" + os.Args[0]) + + for i, arg := range os.Args[1:] { + fmt.Println(strconv.FormatInt(int64(i), 10) + ":" + arg) + } + +} +``` + +#### 查找重复的行 +**dup 版本一****: + +打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio包 + +```go +// Dup1 prints the text of each line that appears more than +// once in the standard input, preceded by its count. +package main + +import ( + "bufio" + "fmt" + "os" +) + +func main() { + counts := make(map[string]int) + //Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。 + input := bufio.NewScanner(os.Stdin) + //每次调用input.Scan(),即读入下一行,并移除行末的换行符; + //读取的内容可以调用input.Text()得到。 + //Scan函数在读到一行时返回true,不再有输入时返回false。 + for input.Scan() { + //下面语句等价于: line := input.Text(); counts[line] = counts[line] + 1 + counts[input.Text()]++ + //map中不含某个键时不用担心,首次读到新行时,等号右边的表达式counts[line]的值将被计算为其类型的零值,对于int即0 + } + // NOTE: ignoring potential errors from input.Err() + for line, n := range counts { + if n > 1 { + //%d表示以十进制形式打印一个整型操作数 + fmt.Printf("%d\t%s\n", n, line) + } + } +} +``` + +**Printf的格式转换**: +``` +%d 十进制整数 +%x, %o, %b 十六进制,八进制,二进制整数。 +%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00 +%t 布尔:true或false +%c 字符(rune) (Unicode码点) +%s 字符串 +%q 带双引号的字符串"abc"或带单引号的字符'c' +%v 变量的自然形式(natural format) +%T 变量的类型 +%% 字面上的百分号标志(无操作数) +``` +>后缀f指format,ln指line +- 以字母`f`结尾的格式化函数: 如`log.Printf`和`fmt.Errorf`,都采用fmt.Printf的格式化准则。 +- 以`ln`结尾的格式化函数: 则遵循Println的方式,以跟`%v`差不多的方式格式化参数,并在最后添加一个换行符 +- 带# +- 带+,输出字段名 + + + +**dup版本二** + +读取标准输入或是使用os.Open打开各个具名文件,并操作它们。 + +```go +// Dup2 prints the count and text of lines that appear more than once +// in the input. It reads from stdin or from a list of named files. +package main + +import ( + "bufio" + "fmt" + "os" +) + +func main() { + counts := make(map[string]int) + files := os.Args[1:] + if len(files) == 0 { + countLines(os.Stdin, counts) + } else { + for _, arg := range files { + //第一个值是被打开的文件(*os.File) + f, err := os.Open(arg) + //如果err等于内置值nil(译注:相当于其它语言里的NULL),那么文件被成功打开 + if err != nil { + fmt.Fprintf(os.Stderr, "dup2: %v\n", err) + continue + } + countLines(f, counts) + f.Close() + } + } + for line, n := range counts { + if n > 1 { + fmt.Printf("%d\t%s\n", n, line) + } + } +} + +func countLines(f *os.File, counts map[string]int) { + input := bufio.NewScanner(f) + for input.Scan() { + counts[input.Text()]++ + } + // NOTE: ignoring potential errors from input.Err() +} + +``` +**说明:** +- map是一个由make函数创建的数据结构的引用。 +- map作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。 +- 在我们的例子中,countLines函数向counts插入的值,也会被main函数看到。 +>(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存) + + +**dup版本三** +一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。 + + +引入了ReadFile函数(来自于io/ioutil包),其读取指定文件的全部内容,strings.Split函数把字符串分割成子串的切片。 + +```go +package main + +import ( + "fmt" + "io/ioutil" + "os" + "strings" +) + +func main() { + counts := make(map[string]int) + for _, filename := range os.Args[1:] { + data, err := ioutil.ReadFile(filename) + if err != nil { + fmt.Fprintf(os.Stderr, "dup3: %v\n", err) + continue + } + //ReadFile函数返回一个字节切片(byte slice),必须把它转换为string,才能用strings.Split分割。 + for _, line := range strings.Split(string(data), "\n") { + counts[line]++ + } + } + for line, n := range counts { + if n > 1 { + fmt.Printf("%d\t%s\n", n, line) + } + } +} +``` + + +### GIF动画 +生成的图形名字叫利萨如图形(Lissajous figures)。 + +下面代码引入新的结构,包括const声明,struct结构体类型,复合声明。 +```go +// Lissajous generates GIF animations of random Lissajous figures. +package main + +import ( + "image" + "image/color" + "image/gif" + "io" + "math" + "math/rand" + "os" + "time" +) +//引入包带过多单词时,通常我们只需要用最后那个单词表示这个包就可以 +//[]color.Color{...} 复合声明,slice切片 +var palette = []color.Color{color.White, color.Black} + +const ( + //整个包都可用,常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。 + whiteIndex = 0 // first color in palette + blackIndex = 1 // next color in palette +) + +func main() { + // The sequence of images is deterministic unless we seed + // the pseudo-random number generator using the current time. + // Thanks to Randall McPherson for pointing out the omission. + + //调用lissajous函数,用它来向标准输出流打印信息 + rand.Seed(time.Now().UTC().UnixNano()) + lissajous(os.Stdout) +} + +func lissajous(out io.Writer) { + //把常量声明定义在函数体内部,那么这种常量就只能在函数体内用 + const ( + cycles = 5 // number of complete x oscillator revolutions + res = 0.001 // angular resolution + size = 100 // image canvas covers [-size..+size] + nframes = 64 // number of animation frames + delay = 8 // delay between frames in 10ms units + ) + + freq := rand.Float64() * 3.0 // relative frequency of y oscillator + //复合声明,生成的是struct结构体,其内部变量LoopCount字段会被设置为nframes;而其它的字段会被设置为各自类型默认的零值 + anim := gif.GIF{LoopCount: nframes} + phase := 0.0 // phase difference + //外层循环会循环64次,每一次都会生成一个单独的动画帧。 + for i := 0; i < nframes; i++ { + //它生成了一个包含两种颜色的201*201大小的图片,白色和黑色。 + rect := image.Rect(0, 0, 2*size+1, 2*size+1) + img := image.NewPaletted(rect, palette) + for t := 0.0; t < cycles*2*math.Pi; t += res { + //内层循环设置两个偏振值。x轴偏振使用sin函数。 + x := math.Sin(t) + //y轴偏振也是正弦波,但其相对x轴的偏振是一个0-3的随机值,初始偏振值是一个零值,随着动画的每一帧逐渐增加。 + y := math.Sin(t*freq + phase) + //循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。 + img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), + blackIndex) + } + phase += 0.1 + //将结果append到anim中的帧列表末尾,并设置一个默认的80ms的延迟值 + anim.Delay = append(anim.Delay, delay) + anim.Image = append(anim.Image, img) + } + //循环结束后所有的延迟值被编码进了GIF图片中,并将结果写入到输出流 + gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors +} + +``` + + +### 并发访问url +先来个非并发版本的: +类似curl的最基本功能,fetch访问指定的url,并将响应的body打印出来 +```go +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" +) + +func main() { + for _, url := range os.Args[1:] { + if !strings.HasPrefix(url, "http://") { + url = "http://" + url + } + resp, err := http.Get(url) + if err != nil { + fmt.Fprintf(os.Stderr, "fetch: %v\n", err) + os.Exit(1) + } + status := resp.Status + fmt.Println(status) + //函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写入到dst中,使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout + out, err := io.Copy(os.Stdout, resp.Body) + // b, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) + os.Exit(1) + } + fmt.Printf("%s", out) + } +} +``` + + +**并发**: +fetchall的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务。 + +```go +// Fetchall fetches URLs in parallel and reports their times and sizes. +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "time" +) + +func main() { + start := time.Now() + ch := make(chan string) + for _, url := range os.Args[1:] { + //goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。 + //main函数本身也运行在一个goroutine中 + go fetch(url, ch) // start a goroutine + } + for range os.Args[1:] { + fmt.Println(<-ch) // receive from channel ch + } + fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds()) +} + +/** +* - 当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处, +* 直到另一个goroutine从这个channel里接收或者写入值,这样两个goroutine才会继续执行channel操作之后的逻辑。 +* - 每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。 +* - 这个程序中我们用main函数来接收所有fetch函数传回的字符串,可以避免在goroutine异步执行还没有完成时main函数提前退出。 +*/ +func fetch(url string, ch chan<- string) { + start := time.Now() + resp, err := http.Get(url) + if err != nil { + ch <- fmt.Sprint(err) // send to channel ch + return + } + //ioutil.Discard输出流可以把这个变量看作一个垃圾桶,可以向里面写一些不需要的数据 + nbytes, err := io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() // don't leak resources + if err != nil { + ch <- fmt.Sprintf("while reading %s: %v", url, err) + return + } + secs := time.Since(start).Seconds() + ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) +} +``` + + +### Web服务 +```go +// Server2 is a minimal "echo" and counter server. +package main + +import ( + "fmt" + "log" + "net/http" + "sync" +) + +var mu sync.Mutex +var count int + +func main() { + http.HandleFunc("/", handler) + http.HandleFunc("/showCount", showCounter) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} + +// handler echoes the Path component of the requested URL. +func handler(w http.ResponseWriter, r *http.Request) { + mu.Lock() + count++ + mu.Unlock() + + fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto) + for k, v := range r.Header { + fmt.Fprintf(w, "Header[%q] = %q\n", k, v) + } + fmt.Fprintf(w, "Host = %q\n", r.Host) + fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr) + if err := r.ParseForm(); err != nil { + log.Print(err) + } + for k, v := range r.Form { + fmt.Fprintf(w, "Form[%q] = %q\n", k, v) + } +} + +// counter echoes the number of calls so far. +func showCounter(w http.ResponseWriter, r *http.Request) { + mu.Lock() + fmt.Fprintf(w, "Count %d\n", count) + mu.Unlock() +} +``` + +### 本章要点 + +**控制流**:if、for、switch + + +**命名类型** + + +**指针** +- 指针是一种直接存储了变量的内存地址的数据类型 + - 指针是可见的内存地址 + - &操作符可以返回一个变量的内存地址 + - *操作符可以获取指针指向的变量内容 + - 要注意区分表示指针类型的*,比如: 类型 *T 是指向 T 类型值的指针,其零值为 nil。 + + +- 不像C语言那样不受约束,也不想其他语言那样沦为单纯的“引用”,折中。 + - 没有指针运算,不能对指针进行加减。 + + +**方法和接口** +- 方法:是和命名类型关联的一类函数 + - **go特别**:方法可以被关联到任意一种命名类型 +- 接口:一种抽象类型,这种类型可以让我们以同样的方式来处理不同的固有类型, + - 不用关心它们的具体实现,而只需要关注它们提供的方法 + + +**包(packages)** +Go语言提供了一些很好用的package,并且这些package是可以扩展的。 + +- 可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。 +- godoc这个工具可以让你直接在本地命令行阅读标准库的文档。 + + +**注释** +- 在源文件的开头写的注释是这个源文件的文档 +- 每一个函数之前写一个说明函数行为的注释也是一个好习惯。 + +--- + +## 第二章 基本实体 +元素结构、变量、新类型定义、包和文件、以及作用域等概念 + +### 命名 +- 大小写敏感 +- 25个关键字不能用于命名, 30多个预定义名字可以用,但要避免过渡使用语义混乱。 +- 可见性 + - 在函数内部定义,那么它就只在函数内部有效 +- 在函数外部定义,那么将在当前包的所有文件中都可以访问 +- 函数外部定义大写包级名字(包级函数名本身也是包级名字,大写函数也就是公共函数),可以被外部的包访问。 + - 例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。 + - 包本身的名字一般总是用小写字母 +- 长度无限制,但是断点好,特别是局部变量。 +- 推荐驼峰,而不是下划线 + + +### 声明 +Go语言主要有四种类型的声明语句:var、const、type和func,分别对应**变量、常量、类型和函数实体**对象的声明。 + +- 包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用) + +**变量声明**: +- `var 变量名字 类型 = 表达式` +- 在Go语言中不存在未初始化的变量 + - 数字0,空字符串,false + - 接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil + - 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。 + - go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。 +- **初始化** + - 在包级别声明的变量会在main入口函数执行前完成初始化。 + - 局部变量将在声明语句被执行到的时候完成初始化。 +- **短声明** + - 在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。 + - 它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。 + - 例如:`t := 0.0` + - var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。例如:`var err error`,后面会进行重新赋值。 + - 请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。 + - 简短变量声明左边的变量可能并不是全部都是刚刚声明的。 + - 如果有一些**已经在相同的词法域声明过了**,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。 + - 简短变量声明语句中**必须至少**要声明一个**新**的变量. + - 如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一**个新的变量** +```go +f, err := os.Open(infile) +// 解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。就是用=号 +f, err := os.Create(outfile) // compile error: no new variables +``` + + +**指针** +如果用“var x int”声明语句声明一个x变量: +- `&x`表达式(取x变量的内存地址),将产生一个指向该整数变量的指针,指针对应的**数据类型是*int**,“指向int类型的指针”。 +- 如果指针名字为p,可以说p指针指向变量x,或者p指针保存了x变量的内存地址。 +- `*p`:**对应p指针指向的变量的值** + - 因为*p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。 +```go +x := 1 +p := &x // p, of type *int, points to x +fmt.Println(*p) // "1" +*p = 2 // equivalent to x = 2 +fmt.Println(x) // "2" +``` +- 任何类型的指针的零值都是nil。 + - 如果p指向某个有效变量,那么p != nil测试为真。 + - 指针之间也是可以进行相等测试的,只有当它们**指向同一个变量**或**全部是nil时才相等**。 + + +- 每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。 +- 指针特别有价值的地方在于我们可以不用名字而访问一个变量 +- 但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作) + + +指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。 +- -n用于忽略行尾的换行符,-s sep用于指定分隔字符(默认是空格) +- 程序中的sep和n变量分别是指向对应命令行标志参数变量的指针,因此必须用\*sep和*n形式的指针语法间接引用它们 +```go +// Echo4 prints its command-line arguments. +package main + +import ( + "flag" + "fmt" + "strings" +) +//调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。 +//第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。 +//如果用户在命令行输入了一个无效的标志参数,或者输入-h或-help参数,那么将打印所有标志参数的名字、默认值和描述信息。 +var n = flag.Bool("n", false, "omit trailing newline") +var sep = flag.String("s", " ", "separator") +//程序中的sep和n变量分别是指向对应命令行标志参数变量的指针,因此必须用*sep和*n形式的指针语法间接引用它们 +func main() { + //用于更新每个标志参数对应变量的值(之前是默认值) + flag.Parse() + //对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应一个字符串类型的slice + fmt.Print(strings.Join(flag.Args(), *sep)) + if !*n { + fmt.Println() + } +} +``` + + +**new函数** +另一个创建变量的方法是调用内建的new函数。 +- 表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值 +- 然后返回变量地址,**返回的指针类型为*T** + +```go +p := new(int) // p, *int 类型, 指向匿名的 int 变量 +``` + + +- 每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的: +```go +p := new(int) +q := new(int) +fmt.Println(p == q) // "false" +``` +>如果两个类型都是空的,也就是说类型的大小是0,例如struct{}和[0]int,有可能有相同的地址(依赖具体的语言实现) + + +由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子: +```go +//由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。 +func delta(old, new int) int { return new - old } +``` + + +**变量的生命周期** +- 对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。 +- 局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间**可能被回收**. + + +**Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?** +- 基本的实现思路是,从每个包级的变量和每个当前运行函数的**每一个局部变量开始**,通过指针或引用的访问路径遍历,是否可以找到该变量。 +- 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。局部变量可能在函数返回之后依然存在。 + + +- 编译器会**自动选择**在栈上还是在堆上分配局部变量的存储空间。 不是根据是用var还是new声明觉得的,而是 + - 下面f函数的x在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个**x局部变量从函数f中逃逸**了 + - 当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。 +```go +var global *int + +func f() { + var x int + x = 1 + global = &x +} + +func g() { + y := new(int) + *y = 1 +} +``` +- 虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期 +>例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。 + + +### 赋值 +- 自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的 +- 元组赋值:允许同时更新多个变量的值,在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。 + - 对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样**交换两个变量**的值: +```go +x, y = y, x + +a[i], a[j] = a[j], a[i] + +//计算最大公约数 +func gcd(x, y int) int { + for y != 0 { + x, y = y, x%y + } + return x +} + + +//计算斐波那契额数列的第N个数 +func fib(n int) int { + x, y := 0, 1 + for i := 0; i < n; i++ { + x, y = y, x+y + } + return x +} +``` +- 如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功 +```go +v, ok = m[key] // map lookup +v, ok = x.(T) // type assertion +v, ok = <-ch // channel receive + +//也有只产生一个结果的情形 +v = m[key] // map查找,失败时返回零值 +v = x.(T) // type断言,失败时panic异常 +v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败) + +_, ok = m[key] // map返回2个值 +_, ok = mm[""], false // map返回1个值 +_ = mm[""] // map返回1个值 +``` + + +**可赋值性** +- 不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。 +- 大部分的类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。 +- 常量则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。 +- 对于两个值是否可以用==或!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的 + + + +### 类型 +- 变量或表达式的类型定义了对应存储值的属性特征。例如 + - 数值在内存的存储大小(或者是元素的bit个数), + - 它们在内部是如何表达的, + - 是否支持一些操作符, + - 以及它们自己关联的方法集等。 +- 一些变量有有着相同的内部结构,但是却表示完全不同的概念,通过创建不一样的类型名称,分割开。 + - 声明类型: `type 类型名字 底层类型` + - 类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。 + + +**将不同温度单位定义为不同的类型** +- 它们不可以被相互比较或混在一个表达式运算。 +- 需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型 +>Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用。 +- 好处 + - 可以避免一些像无意中使用不同单位的温度混合计算导致的错误 +```go +/ Package tempconv performs Celsius and Fahrenheit temperature computations. +package tempconv + +import "fmt" + +type Celsius float64 // 摄氏温度 +type Fahrenheit float64 // 华氏温度 + +const ( + AbsoluteZeroC Celsius = -273.15 // 绝对零度 + FreezingC Celsius = 0 // 结冰点温度 + BoilingC Celsius = 100 // 沸水温度 +) + +func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } + +func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } + + +//如果两个值有着不同的类型,则不能直接进行比较 +var c Celsius +var f Fahrenheit +fmt.Println(c == 0) // "true" +fmt.Println(f >= 0) // "true" +fmt.Println(c == f) // compile error: type mismatch +//Celsius(f)是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。测试为真的原因是因为c和g都是零值。 +fmt.Println(c == Celsius(f)) // "true"! +``` + + +**类型转换操作**: +- 类型转换不会改变值本身,但是会使它们的语义发生变化 +- 对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0)) +- 只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。 +- 数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的, + - 可能改变值的表现:将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice将拷贝一个字符串数据的副本 + - 在**任何情况下**,**运行时不会发生转换失败的错误**(译注: 错误只会发生在编译阶段)。 + + +**命名类型**: +- 可以提供书写方便 +- 还可以为该类型的值定义新的行为。(这些行为表示为一组关联到该类型的函数集合,我们称为**类型的方法集**。) + + +**String()方法,类似toString**:当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印。 +```go +func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } + +c := FToC(212.0) +fmt.Println(c.String()) // "100°C" +fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly +``` + + +### 包和文件 +- 每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。 + + +**包的初始化** +- 首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化 +- 如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。 +- 对于在包级别声明的变量,如果有初始化表达式则用表达式初始化.复杂的一般用一个特殊的**init初始化函数**来简化初始化工作。每个文件都可以包含多个init初始化函数 +```go +func init() { /* ... */ } +``` + - 每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用 + - 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。 + - 初始化工作是自下而上进行的,main包最后被初始化。这种方式可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。 + + +### 作用域 +- 对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。 +- 当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。 +- 如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则**内部块的声明首先被找到**。 +- 下面**第二个if语句嵌套在第一个内部**,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。 +```go +if x := f(); x == 0 { + fmt.Println(x) +} else if y := g(x); x == y { + fmt.Println(x, y) +} else { + fmt.Println(x, y) +} +fmt.Println(x, y) // compile error: x and y are not visible here +``` +- 下面代码:变量f的作用域只在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。 +```go +if f, err := os.Open(fname); err != nil { // compile error: unused: f + return err +} +f.ReadByte() // compile error: undefined f +f.Close() // compile error: undefined f +``` +- 通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量: +```go +f, err := os.Open(fname) +if err != nil { + return err +} +f.ReadByte() +f.Close() +``` + +**有个例子:要特别注意短变量声明语句的作用域范围。**考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。 +```go +var cwd string + +func init() { + cwd, err := os.Getwd() // NOTE: wrong! + if err != nil { + log.Fatalf("os.Getwd failed: %v", err) + } + log.Printf("Working directory = %s", cwd) +} +``` + - 虽然cwd在外部已经声明过,但是:=语句还是将cwd和err**重新声明为新的局部变量**。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并**不会正确更新包级声明的cwd变量**。 + - 全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。 +- 有许多方式可以避免出现类似潜在的问题。最直接的方法是**通过单独声明err变量**,来**避免使用:=的简短声明方式**: +```go +var cwd string + +func init() { + var err error + cwd, err = os.Getwd() + if err != nil { + log.Fatalf("os.Getwd failed: %v", err) + } +} +``` + + +## 第三章 基础数据类型:数字、布尔值、字符串和常量 + +### 整型 + +- 有两种一般**对应特定CPU平台机器字大小**的有符号和无符号整数**int和uint**;其中int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit,但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。 +- Unicode字符**rune类型是和int32等价**的类型,通常用于**表示一个Unicode码点**。这两个名称可以互换使用。 +- 同样**byte也是uint8类型的等价类型**,byte类型一般用于**强调数值是一个原始的数据**而不是一个小的整数。 +- 还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。 +- 不管它们的具体大小,int、uint和uintptr是**不同类型的兄弟类型**。其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。 + + +**取模%** +- 在Go语言中,%取模运算符的符号和被取模数的符号总是一致的,因此-5%3和-5%-3结果都是-2。 +- 除法运算符/的行为则依赖于操作数是否全为整数,比如5.0/4.0的结果是1.25,但是5/4的结果是1,因为整数除法会向着0方向截断余数 + + +**溢出**: +- 一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的bit位是1的话,那么最终结果可能是负的,例如int8的例子: + +```go +var u uint8 = 255 +fmt.Println(u, u+1, u*u) // "255 0 1" + +var i int8 = 127 +fmt.Println(i, i+1, i*i) // "127 -128 1" +``` + + +- 位操作运算符`^`作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;也就是说,它返回一个每个bit位都取反的数。 +- 位操作运算符`&^`用于按位置零(AND NOT):如果对应y中bit位为1的话,表达式z = x &^ y结果z的对应的bit位为0,否则z对应的bit位等于x相应的bit位的值。 + + +下面的代码演示了如何**使用位操作解释uint8类型值的8个独立的bit位**。 +它使用了Printf函数的%b参数打印二进制格式的数字;其中%08b中08表示打印至少8个字符宽度,**不足的前缀部分用0填充**。 +```go +var x uint8 = 1<<1 | 1<<5 //00000010 | 00100000 +var y uint8 = 1<<1 | 1<<2 + +fmt.Printf("%08b\n", x) // "00100010", the set {1, 5} +fmt.Printf("%08b\n", y) // "00000110", the set {1, 2} + +fmt.Printf("%08b\n", x&y) // "00000010", the intersection {1} +fmt.Printf("%08b\n", x|y) // "00100110", the union {1, 2, 5} +fmt.Printf("%08b\n", x^y) // "00100100", the symmetric difference {2, 5} +fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5} +``` + + +- **无符号数**往往只有在**位运算**或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常**并不用于仅仅是表达非负数量的场合**。 + + +**类型转换**:对于每种类型T,**如果转换允许的话,类型转换操作T(x)将x转换为T类型**。 + + +**fmt.Printf格式**: +- 当使用fmt包打印一个数值时,我们可以用`%d`、`%o`或`%x`参数控制输出的进制格式,就像下面的例子: +```go +o := 0666 +fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666" +x := int64(0xdeadbeef) +fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x) +// Output: +// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF +``` +请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数, +- 但是%之后的\[1]副词告诉Printf函数再次使用**第一个操作数**。 +- 第二,%后的#副词告诉Printf在用`%o`、`%x`或`%X`输出时生成0、0x或0X前缀。 + + +字符使用%c参数打印,或者是用%q参数打印带单引号的字符: +```go +ascii := 'a' +unicode := '国' +newline := '\n' +fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'" +fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'" +fmt.Printf("%d %[1]q\n", newline) // "10 '\n'" +``` + + +用Printf函数的%g参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用%e(带指数)或%f的形式打印可能更合适. + +所有的这三个打印形式都可以指定打印的宽度和控制打印精度。 +```go +for x := 0; x < 8; x++ { + //打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度 + fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x))) +} +``` + + +### 浮点数 +Go语言提供了两种精度的浮点数,float32和float64。 + + +通常应该**优先使用float64类型**,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大。 +>(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差): +```go +var f float32 = 16777216 // 1 << 24 +fmt.Println(f == f+1) // "true"! +``` + + +### 复数 +- Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。 +- 内置的complex函数用于构建复数,内建的**real和imag函数**分别返回复数的实部和虚部: +```go +var x complex128 = complex(1, 2) // 1+2i +var y complex128 = complex(3, 4) // 3+4i +fmt.Println(x*y) // "(-5+10i)" +fmt.Println(real(x*y)) // "-5" +fmt.Println(imag(x*y)) // "10" +``` +- 如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0:`fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1` +- 复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的. 风险跟浮点数比较类似。 + + +### 布尔类型 +- 布尔值可以和&&(AND)和||(OR)操作符结合,并且**有短路行为**:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值。 +- **&&的优先级比||高** +>(助记:&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的: +```go +if 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z' || + '0' <= c && c <= '9' { + // ...ASCII letter or digit... +} +``` +- 布尔值并**不会隐式转换为数字值0或1**,反之亦然。必须使用一个显式的if语句辅助转换 + + +### 字符串 +* 一个字符串是一个不可改变的字节序列。 +* 字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。 +* 内置的`len`函数可以返回一个字符串中的**字节数目**(**不是rune字符数目**),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。 + * 第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。 +* 子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。 +- 字符串**可以用==和<进行比较**;比较通过**逐个字节比较**完成的,因此比较的结果是**字符串自然编码的顺序**。 + + + +**字符串不可变?** +- 我们也可以给一个字符串变量分配一个新字符串值。下面代码:并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。 +```go +s := "left foot" +t := s +s += ", right foot" + +fmt.Println(s) // "left foot, right foot" +fmt.Println(t) // "left foot" +``` +- s[0] = 'L' // compile error: cannot assign to s[0] + + + +#### **原生字符串** +一个原生的字符串面值形式是`...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行。 +- 常用于编写正则表达式。 + + +#### **编码:Unicode和UTF-8** +- Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。 +- UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。 +- UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。 + - 每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。 + - 如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。 + - 如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头, 后面更大的也是类似操作。 + - 1110需要三个字节,11110需要四个字节,后面字节都是以10开头。 + + +变长的编码无法直接通过索引来访问第n个字符,但是UTF8编码获得了很多额外的优点: +- 更加紧凑,完全兼容ASCII码,并且可以自动同步。 +- 可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。 +- 它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看 +- 没有任何字符的编码是其他字符编码的子串,因此搜索一个字符时只要搜索它的字节编码序列即可。 +- UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。 +- 同时因为没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。 + + +Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字,或者是字母的大写和小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。 + + +**字符串中字节数和字符数**:字符串包含13个字节,以UTF8形式编码,但是只对应9个Unicode字符: +```go +import "unicode/utf8" + +s := "Hello, 世界" +fmt.Println(len(s)) // "13" +fmt.Println(utf8.RuneCountInString(s)) // "9" +``` +- Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。 +```go +for i, r := range "Hello, 世界" { + fmt.Printf("%d\t%q\t%d\n", i, r, r) +} +``` +- UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。 + + +#### 字符串处理和转换 + +**字符串和Byte切片** + + +- 标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。 + - strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。 + - bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。 + - strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。 + - unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。 + + +- 实现一个将path文件路径简化成文件名(去掉前面目录和后缀): +```go +func basename(s string) string { + slash := strings.LastIndex(s, "/") // -1 if "/" not found + s = s[slash+1:] + if dot := strings.LastIndex(s, "."); dot >= 0 { + s = s[:dot] + } + return s +} +``` + + +- **字符串和字节slice之间可以互相转换** +```go +s := "abc" +b := []byte(s) +s2 := string(b) +``` + - 需要确保在变量b被修改的情况下,原始的s字符串也不会改变。 +- strings包中的六个函数:(bytes包中也对应的六个函数,区别就是类型换成了字节slice类型) + +```go +func Contains(s, substr string) bool +func Count(s, sep string) int +func Fields(s string) []string +func HasPrefix(s, prefix string) bool +func Index(s, sep string) int +func Join(a []string, sep string) string + +``` + +- bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的 +```go +// intsToString is like fmt.Sprint(values) but adds commas. +func intsToString(values []int) string { + var buf bytes.Buffer + buf.WriteByte('[') + for i, v := range values { + if i > 0 { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%d", v) + } + buf.WriteByte(']') + return buf.String() +} + +func main() { + fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]" +} +``` +>bytes.Buffer类型有着很多实用的功能,我们在第七章讨论接口时将会涉及到,我们将看看如何将它用作一个I/O的输入和输出对象,例如当做Fprintf的io.Writer输出对象,或者当作io.Reader类型的输入源对象。 + + +**字符串与数字的转换** + + + +由strconv包提供这类转换功能。 +- 将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv.Itoa(“整数到ASCII”): +```go + x := 123 +y := fmt.Sprintf("%d", x) +fmt.Println(y, strconv.Itoa(x)) // "123 123" + +``` +- FormatInt和FormatUint函数可以**用不同的进制来格式化数字**: +```go +fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011" +``` + +- `fmt.Printf`函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要**包含有附加额外信息**的时候: +```go +s := fmt.Sprintf("x=%b", x) // "x=1111011" +``` + +- 如果要将一个**字符串解析为整数**,可以使用strconv包的**Atoi或ParseInt**函数,还有用于解析无符号整数的ParseUint函数: +```go +x, err := strconv.Atoi("123") // x is an int +y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits +``` + - ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。 + - 在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。 + + + +### 常量 +- 存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。 +- 常量表达式的值在编译期计算,而不是在运行期。 +- 如果没有显式指明类型,那么将从右边的表达式推断类型。如果转换合法的话。 + - 可以通过%T参数打印类型信息:fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0" + - 无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。 + - 如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型:var i = int8(0) + + +#### iota 常量生成器 +常量声明可以使用iota常量生成器初始化,它用于生成一组以相**似规则初始化的常量**,但是不用每行都写一遍初始化表达式。 +- 比如星期、月份、年份 +- 在第一个声明的常量所在的行,**iota将会被置为0**,然后在每一个有常量声明的行加一。 + + +#### 无类型常量 +编译器为这些没有明确基础类型的数字常量**提供比基础类型更高精度的算术运算**; +>你可以认为至少有256bit的运算精度。 + +这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。 + +- 通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接**用于更多的表达式而不需要显式的类型转换。** + + +- 对于常量面值,**不同的写法可能会对应不同的类型**。 +>例如0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型 + + +--- + + +## 第四章 复合类型:数组和结构体 + +从简单的数组、字典、切片到动态列表 + +### 定长数组 array +数组是一个由**固定长度**的**特定类型**元素组成的序列,一个数组可以由零个或多个元素组成。 +>因为长度固定,而且没有任何添加或删除数组元素的方法。 +>Go语言中很少直接使用数组。 除了像SHA256这类需要处理特定大小数组的特例外。 一般用slice,但是要先理解数组。 + + +- 索引下标的范围是从0开始到数组长度减1的位置。内置的**len函数**将返回数组中**元素的个数**。 +- 在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。 +- `var r [3]int = [3]int{1, 2}`, 优先赋值前面的,r|[2]为默认值0 +- 长度是数组类型的组成部分,长度不同的数组,类型是不一样的。 + + +**初始化**: +```go +var r [3]int = [3]int{1, 2} +fmt.Println(r[2]) // "0" + +s := [...]int{99: -1} +``` + + +**比较**: +如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的 +```go +import "crypto/sha256" + +func main() { + c1 := sha256.Sum256([]byte("x")) + c2 := sha256.Sum256([]byte("X")) + //%x 十六进制的格式打印, %t布尔类型, %T对应的数据类型 + fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1) + // Output: + // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881 + // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015 + // false + // [32]uint8 +} +``` + + +**数组参数,值传递**: +当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。**而数组是值类型**,所以复制的数组改变不会影响外面。 如果是引用类型,那改变的就是复制进去的指针地址对应的值。 + + +当然,我们可以**显式地传入一个数组指针**,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。 +```go +func zero(ptr *[32]byte) { + *ptr = [32]byte{} +} +``` + + + + +### 可变数组 slice +- 每个元素类型相同,没有固定长度。 +- 是否可称为引用类型? 有说可以叫指针结构的包装,比叫引用类型更严谨。 + + +#### 获取子序列 +- 用s[i]访问单个元素,用s[m:n]获取子序列。Go言里也采用左闭右开形式,0 ≤ m ≤ n ≤ len(s),包含n-m个元素。 + +如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为**新slice的长度会变大**: +```go +months := [...]string{1: "January", /* 省略掉定义,自行补充 */, 12: "December"} +summer := months[6:9] //len:3 , cap:7 + +fmt.Println(summer[:20]) // panic: out of range +endlessSummer := summer[:5] // extend a slice (within capacity) +fmt.Println(endlessSummer) // "[June July August September October]" +``` + +- []byte是字节类型**切片** +- 复制一个slice只是对底层的数组创建了一个新的slice别名 + + +#### 难点一: 长度len 和 容量cap +**一个切片的容量总是固定的。** slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个**新的**slice。 +- 容量一般是从slice的**开始位置到底层数据的结尾**位置。 +- 内置的len和cap函数分别返回slice的长度和容量 + + +例子: +```go +s3 := []int{1, 2, 3, 4, 5, 6, 7, 8} +s4 := s3[3:6] +``` + +s3的长度和容量都是 8 +s4的长度(大小)是3,**s4的容量是多少?** +- 切片的容量代表了它的底层数组的长度,但这仅限于使用make函数或者切片值字面量初始化切片的情况。 +- 更通用的规则是: 一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。 + - 而在底层数组不变的情况下,切片代表的**窗口可以向右扩展,直至其底层数组的末尾**。 + - 这里底层数组是最底层,哪怕slicea 从arr而来,sliceB从sliceA而来。 + + +#### 比较 +* slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。 +>不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte). + +- 对于其他类型的slice,我们必须自己展开每个元素进行比较,为啥不支持实现呢? + - 第一个原因,一个slice的元素是间接引用的,一个slice甚至可以包含自身(译注:当slice声明为[]interface{}时,slice的元素可以是自身) + - 第二个原因,因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为**底层数组的元素可能会被修改**。 + + +* slice唯一合法的比较操作是和nil比较,与任意类型的nil值一样,我们可以用`[]int(nil)`类型转换表达式来生成一个对应类型slice的nil值. +* 如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。 + + +#### make生成slice +- 内置的make函数创建一个指定元素类型、长度和容量的slice。 +- 容量部分可以省略,在这种情况下,容量将等于长度。 +- 在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。 + + +#### append函数 +内置的append函数用于向slice追加元素:`sliceA = append(sliceA, r)`。下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice: +```go +func appendInt(x []int, y int) []int { + var z []int + zlen := len(x) + 1 + if zlen <= cap(x) { + // There is room to grow. Extend the slice. + z = x[:zlen] + } else { + // There is insufficient space. Allocate a new array. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + //copy函数将返回成功复制的元素的个数,等于两个slice中较小的长度 + copy(z, x) // a built-in function; see text + } + z[len(x)] = y + return z +} +``` +- 内置的append函数则可以追加多个元素,甚至追加一个slice。 +- 内置的append函数可能使用比appendInt更复杂的内存扩展策略。 +>因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也**不能确认新的slice和原始的slice是否引用的是相同的底层数组空间**。同样,我们不能确认在原先的slice上的操作**是否会影响到新的slice**。 + +因此,通常是将append返回的结果直接赋值给输入的slice变量:`sliceA = append(sliceA, r)` +- 更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。 +- 要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。 +- 要更新这些信息需要像上面例子那样一个显式的赋值操作。 + + +输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样: +```go +func main(){ + data := []string{"one", "", "three"} + fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]` + fmt.Printf("%q\n", data) // `["one" "three" "three"]` +} + +func nonempty(strings []string) []string { + i := 0 + for _, s := range strings { + if s != "" { + strings[i] = s + i++ + } + } + return strings[:i] +} + +``` + +#### 为啥要弄这种滑动窗口式的设计 + + +#### slice[2:] 省略掉的是len(slice) +省略掉的:默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)] + + +从Go1.2开始slice支持了三个参数的slice: +```go +var array [10]int +sliceA := array[2:4] //slice的容量是8 +sliceB = array[2:4:7] //这个7是底层数组的位置,表示该切片最多到这个位置。上面这个的容量就是7-2,即5。这样这个产生的新的slice就没办法访问最后的三个元素。 +sliceB[2] 会抛异常,超出边界。 长度只有2. +``` + + + +#### 扩容: +- 它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。 + - 拷贝耗费性能嘛? 原来的切片是否保留?回收啥时候回收? + - 扩容2倍,当原长度大于或等于1024时,Go 语言将会以原容量的 1.25倍作为新容量的基准(以下新容量基准) + + +**切片的底层数组什么时候会被替换?** +- 确切地说,一个切片的底层数组永远不会被替换。 +- 为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。 +- 没有为切片替换底层数组这一说,扩容是直接换新的切片。。。 +- 在无需扩容时,append函数返回的是**指向原底层数组的新切片**,而在需要扩容时,append函数返回的是**指向新底层数组的新切片**。 + + +**知识点** +1. 初始时两个切片引用同一个底层数组,在后续操作中对某个切片的操作超出底层数组的容 量时,这两个切片引用的就不是同一个数组了 + + +### map + +#### 初始化和取值 +map[keyType]valueType +- map的key,可以是int,可以是string及所有完全定义了==与!=操作的类型 + - 虽然浮点数类型也是支持相等运算符比较的,但是**将浮点数用做key类型则是一个坏的想法**,最坏的情况是可能出现的NaN和任何浮点数都不相等。 +- 值则可以是任意类型,但是键之间、值之间类型要相同。 +- 不要使用new,永远用make来构造map + + +```go +ages := make(map[string]int) +ages := map[string]int{ + "alice": 31, + "charlie": 34, +} +fmt.Println(ages["alice"]) // "32" +delete(ages, "alice") // remove element ages["alice"] +``` + + +- 即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。 如何 +```go +ages["bob"] = ages["bob"] + 1 // happy birthday! +``` +- map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。 +- 但是向一个nil值的map存入元素将导致一个panic异常: +```go +ages["carol"] = 21 // panic: assignment to entry in nil map +``` +- 在向map存数据前必须先创建map。 声明了一个nil的map,如何再创建? 再写一遍`ages = make(map[string]int, 10)`? +- map下标获取值可选获取两个值: `age, ok := ages["bob"] 和 age := ages["bob"]`都是合法的? + + +#### 用map实现set +Go程序员将这种忽略value的map当作一个**字符串集合**。 +```go +func main() { + seen := make(map[string]bool) // a set of strings + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + line := input.Text() + if !seen[line] { + seen[line] = true + fmt.Println(line) + } + } + + if err := input.Err(); err != nil { + fmt.Fprintf(os.Stderr, "dedup: %v\n", err) + os.Exit(1) + } +} +``` + + +### 结构体 +结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。 +```go +type Employee struct { + ID int + Name string + Address string + DoB time.Time + Position string + Salary int + ManagerID int +} + +var dilbert Employee +``` + +**访问**: +- 直接点操作符访问和赋值:`dilbert.Salary -= 5000` +- 对成员取地址,然后通过指针访问: +```go +position := &dilbert.Position +*position = "Senior " + *position +``` + + +**点操作符也可以和指向结构体的指针一起工作:** +```go +var employeeOfTheMonth *Employee = &dilbert +employeeOfTheMonth.Position += " (proactive team player)" +//上一句相当于下面,为啥呢??? +(*employeeOfTheMonth).Position += " (proactive team player)" + +``` + + +**调用函数返回的是值,并不是一个可取地址的变量,所以不能直接用点操作符赋值**: +```go +func EmployeeByID(id int) *Employee { /* ... */ } + +fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss" +id := dilbert.ID +//这个赋值操作是可行的,但如果把函数方法EmployeeByID的返回值改为Employee,那下面赋值语句就会报错。 +EmployeeByID(id).Salary = 0 // fired for... no real reason + +``` + + +**命名为S的结构体成员可以包含 *S指针成员** + +一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。) +但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。 +可以使用一个二叉树来实现一个插入排序: + + +**空结构体**: +写成struct{},它的大小为0,也不包含任何信息,但是有时候依然是有价值的: + + +#### 结构体字面量、声明初始化 +- 写法一:只有值,按照类型和顺序一一对应。 + - 缺点:如果做了调整,就需要修改代码。 + - 一般用在: 定义结构体的包内部使用、或者较小的结构体重使用,这些结构体成员排列比较规则 + - 比如:image.Point(x,y)、color。RGBA(red, green, blue, alpha) +- 写法二:以成员名字和相应的值来初始化,可以包含**部分**或全部的成员 + - 好处:没写的成员默认用零值,顺序不重要,切新增字段,老代码也就是默认零值,不用可以不改也不报错。 + + +**使用规则**: +- 两种写法不能混用 +- 不能在外部包中用写法一来偷偷初始化结构体中**未导出**的成员。 **TODO:未验证** +```go +package p +type T struct{ a, b int } // a and b are not exported + +package q +import "p" +var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b +var _ = p.T{1, 2} // compile error: can't reference a, b +``` +- 函数对结构体进行修改操作,必须传入指针; +>因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。 +- 初始化一个结构体变量(下面三种写法等价): +```go +//这个写法可以直接在表达式中使用,比如一个函数调用 +pp := &Point{1, 2} + +pp := new(Point) +*pp = Point{1, 2} +``` + + +#### 结构体嵌入和匿名成员 +```go +type Point struct { + X, Y int +} + +type Circle struct { + Center Point + Radius int +} + +type Wheel struct { + Circle Circle + Spokes int +} +``` +- 结构体类型清晰,但是访问每个成员变量变得繁琐,需要多级。w.Circle.Center.Y = 8 + + +**如何解决访问繁琐的问题?** +- **匿名成员**:只声明一个成员对应的数据类型而不指名成员的名字 + - 匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。 + - +```go +type Point struct { + X, Y int +} + +type Circle struct { + Point + Radius int +} + +type Wheel struct { + Circle + Spokes int +} +``` +- **匿名嵌入的特性**是啥? 可以让我们直接访问叶子属性:`w.X = 8 //equivalent to w.Circle.Point.Y = 8` + - 匿名成员Circle和Point都有自己的名字——就是**命名的类型名字**——但是这些名字在点操作符中是可选的 + - 不能同时包含两个类型相同的匿名成员,这会导致名字冲突。 + - 包内Point和Circle匿名成员都是导出的。即使它们不导出(比如结构名改成小写字母开头的point和circle)。但是在包外部,因为circle和point没有导出,不能访问它们的成员 +- 但是匿名成员就没法用下面方式进行声明初始化了: +```go +w = Wheel{8, 8, 5, 20} // compile error: unknown fields +w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields +``` +需要写成下面这样: +```go +w = Wheel{Circle{Point{8, 8}, 5}, 20} + +w = Wheel{ + Circle: Circle{ + Point: Point{X: 8, Y: 8}, + Radius: 5, + }, + Spokes: 20, // NOTE: 这个逗号是必须的 +} + +// Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。 +fmt.Printf("%#v\n", w) //Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} +``` + + +**匿名成员还有其他好处嘛?**为什么要嵌入一个没有任何子成员类型的匿名成员类型呢? +- 匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。 +- **匿名类型的方法集**:简短的点运算符语法除了访问匿名成员嵌套的成员,还可以访问它们的方法。这个机制可以用于**将一些有简单行为的对象组合成有复杂行为的对象**。组合是Go语言中面向对象编程的核心 + + +### JSON +Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持 +>Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的API接口。 + + +**JSON与go基础类型、对象的对应表示**: +```go +boolean true +number -273.15 +string "She said \"Hello, BF\"" +array ["gold", "silver", "bronze"] +object {"year": 1980, + "event": "archery", + "medals": ["gold", "silver", "bronze"]} +``` + + +**定义** +```go +type Movie struct { + Title string + Year int `json:"released"` //一个结构体成员Tag是和在编译阶段关联到该成员的元信息字符串 + Color bool `json:"color,omitempty"` //json串这个字段的field是 color; + // omitempty 当Go语言结构体成员为空或零值时不生成该JSON对象(只是这个字段不生成) + Actors []string +} + +//该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进: +data, err := json.MarshalIndent(movies, "", " ") + +//下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员。 +//通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。 +var titles []struct{ Title string } +if err := json.Unmarshal(data, &titles); err != nil { + log.Fatalf("JSON unmarshaling failed: %s", err) +} +fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]" +``` +- Go => JSON, 叫**编组**(marshaling) +- JSON => Go,叫**解码**(Unmarshal) + + + +**应用** +- 许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信息。 + - 第一步:定义结构 + - 第二步:定义方法,处理结构体,编码解码 + - 第三步:调用方法 +```go +//gopl.io/ch4/github +const IssuesURL = "https://api.github.com/search/issues" + +type IssuesSearchResult struct { + TotalCount int `json:"total_count"` + Items []*Issue +} + +type Issue struct { + Number int + HTMLURL string `json:"html_url"` + Title string + State string + User *User + CreatedAt time.Time `json:"created_at"` + Body string // in Markdown format +} + +type User struct { + Login string + HTMLURL string `json:"html_url"` +} + +// SearchIssues queries the GitHub issue tracker. +func SearchIssues(terms []string) (*IssuesSearchResult, error) { + q := url.QueryEscape(strings.Join(terms, " ")) + resp, err := http.Get(IssuesURL + "?q=" + q) + if err != nil { + return nil, err + } + + // We must close resp.Body on all execution paths. + // (Chapter 5 presents 'defer', which makes this simpler.) + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("search query failed: %s", resp.Status) + } + + var result IssuesSearchResult + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + resp.Body.Close() + return nil, err + } + resp.Body.Close() + return &result, nil +} + +//gopl.io/ch4/issues +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%d issues:\n", result.TotalCount) + for _, item := range result.Items { + fmt.Printf("#%-5d %9.9s %.55s\n", + item.Number, item.User.Login, item.Title) + } +} +``` +- https://developer.github.com/v3/ +- https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打印和命令行输入的检索词相匹配的漫画的URL。 +- 检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对应的海报。 + + +### 文本和HTML模板 + +- 一个模板是一个字符串或一个文件,连包含了一个或多个由双花括号包含的`{{action}}`对象。 +- 大部分的字符串只是按字面值打印,但是对于actions部分将触发其它的行为. +- 模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性 + +#### 模板字符串 +```go +const templ = `{{.TotalCount}} issues: +{{range .Items}}---------------------------------------- +Number: {{.Number}} +User: {{.User.Login}} +Title: {{.Title | printf "%.64s"}} +Age: {{.CreatedAt | daysAgo}} days +{{end}}` +``` +- 对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。 +- 当前值“.”**最初被初始化为调用模板时的参数**,在当前例子中对应github.IssuesSearchResult类型的变量。 +- 模板中`{{range .Items}}和{{end}}`对应一个循环action,每次迭代的当前值对应当前的Items元素的值。 +- `|`操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。 + >`printf`一个基于fmt.Sprintf实现的内置函数,所有模板都可以直接使用。 + +```go +// 先创建并返回一个模板; +report, err := template.New("report"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). // Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板; + Parse(templ) //调用Parse函数分析模板 +if err != nil { + log.Fatal(err) +} + +// template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。 +var report = template.Must(template.New("issuelist"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ)) + +//使用github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板: +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := report.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} +``` + + +#### HTML模板 +- html/template包已经自动将特殊字符转义 +- 如果不想被转义,可以把对应字符串定义到信任的template.HTML字符串类型,最终生成的html文件就不会转义这个字段的字符串。 +```go +import "html/template" + +var issueList = template.Must(template.New("issuelist").Parse(` +

{{.TotalCount}} issues

+ + + + + + + +{{range .Items}} + + + + + + +{{end}} +
#StateUserTitle
{{.Number}}{{.State}}{{.User.Login}}{{.Title}}
+`)) + +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := issueList.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} +``` + + +## 第五章 函数、错误处理、panic、recover、有defer语句。 +>引用类型包括指针(§2.3.2)、切片(§4.2))、字典(§4.3)、函数(§5)、通道(§8),它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。 + + +**简写**: +```go +//以下语句等价 +func f(i, j, k int, s, t string) { /* ... */ } +func f(i int, j int, k int, s string, t string) { /* ... */ } + +func first(x int, _ int) int { return x } +fmt.Printf("%T\n", first) // "func(int, int) int" 类型 +``` + +- 函数的类型被称为函数的签名。 + - 如果两个函数**形式参数列表和返回值列表中的变量类型**一一对应,那么这两个函数被认为**有相同的类型或签名**。 + - 形参和返回值的**变量名不影响函数签名**,也不影响它们是否可以以省略参数类型的形式表示。 +>每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。 +>在函数调用时,Go语言**没有默认参数值**,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。 + +- **实参通过值的方式传递**,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。 +- 但是,如果**实参包括引用类型**,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用**被修改**。 + +- 没有函数体的函数声明:表示该函数不是以Go实现的 + + +### 递归 +```go +//!+ +func main() { + for _, url := range os.Args[1:] { + links, err := findLinks(url) + if err != nil { + fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err) + continue + } + for _, link := range links { + fmt.Println(link) + } + } +} + + +// findLinks performs an HTTP GET request for url, parses the +// response as HTML, and extracts and returns the links. +func findLinks(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("getting %s: %s", url, resp.Status) + } + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) + } + return visit(nil, doc), nil +} + + +// visit appends to links each link found in n, and returns the result. +func visit(links []string, n *html.Node) []string { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key == "href" { + links = append(links, a.Val) + } + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + links = visit(links, c) + } + return links +} + +``` + +- 在findlinks中,我们必须确保resp.Body被关闭,释放网络资源。虽然**Go的垃圾回收机制**会回收不被使用的内存,但是这**不包括操作系统层面的资源,比如打开的文件、网络连接**。因此我们必须显式的释放这些资源。 +- 一个函数内部可以将另一个有多返回值的函数调用作为返回值,下面的例子展示了与findLinks有相同功能的函数。 +```go +func findLinksLog(url string) ([]string, error) { + log.Printf("findLinks %s", url) + return findLinks(url) +} +``` +- 如果一个函数**所有的返回值都有显式的变量名**,那么该函数的**return语句可以省略操作数*。这称之为bare return。 + - 但是使得代码难以被理解。 + - 用在卫语句挺合适,还没赋值的就是默认零值。 Go会将返回值 words和images在函数体的开始处,根据它们的类型,将其初始化为0。 +```go +func CountWordsAndImages(url string) (words, images int, err error) { + resp, err := http.Get(url) + if err != nil { + return + } + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + err = fmt.Errorf("parsing HTML: %s", err) + return + } + words, images = countWordsAndImages(doc) + return +} +func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ } +``` + +### 错误 +>一个良好的程序永远不应该发生panic异常。 + +- 如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为**ok**。 + >比如,cache.Lookup失败的唯一原因是key不存在。 比如map获取值。 +- error类型可能是nil或者non-nil。 + - nil意味着函数运行成功,non-nil表示失败。 + - 对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。`fmt.Printf("%v", err)` + - 当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。 + - 有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。 **应该是先处理这些不完整的数据,再处理错误** + - Go语言将函数运行失败时返回的错误信息当做一种预期的值,而不是异常。 而Go对于异常处理是针对哪些未被预料到的错误。panic?bug? + - Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理。 + + +**错误处理策略** + +1. 传播错误 + - 要增加必要的上下文,再传播到上游。 + ```go + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) + } + ``` + - 由于错误信息经常是以链式组合在一起的,所以错误信息中应**避免大写和换行符**。 + - 要注意**错误信息表达的一致性**,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。 + >一般而言,被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要**添加一些错误信息中不包含的信息**,比如添加url到html.Parse返回的错误中。 +2. 重新尝试失败的操作 + - 什么情况下用?如果错误的发生是偶然性的,或由不可预知的问题导致的。 + - 注意事项? 限定重试时间间隔或重试次数 +```go +func WaitForServer(url string) error { + const timeout = 1 * time.Minute + deadline := time.Now().Add(timeout) + for tries := 0; time.Now().Before(deadline); tries++ { + _, err := http.Head(url) + if err == nil { + return nil // success + } + log.Printf("server not responding (%s);retrying…", err) + time.Sleep(time.Second << uint(tries)) // exponential back-off + } + return fmt.Errorf("server %s failed to respond after %s", url, timeout) +} +``` + +3. 输出错误信息并**结束程序** + - 注意的是,这种策略只应在main中执行。 + - 对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。 + +```go +// (In function main.) +if err := WaitForServer(url); err != nil { + fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) + os.Exit(1) +} + +//等价于 +if err := WaitForServer(url); err != nil { + log.Fatalf("Site is down: %v\n", err) +} +``` + + +4. 只输出错误信息。 不中断也不往上传,使用log打印错误 +```go +if err := Ping(); err != nil { + log.Printf("ping failed: %v; networking disabled",err) +} +``` +>log包中的所有函数会为没有换行符的字符串增加换行符。 + + +5. 直接忽略掉错误。 + - 当你决定忽略某个错误时,你应该清晰地写下你的意图 + - 比如删除临时目录,有定时任务兜底,你主动删除操作失败也无所谓,就可以不处理删除失败的情况。 + + +**文件结尾错误(EOF)** + + +- io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF +```go +if err == io.EOF { + break // finished reading + } +``` + + +**函数值** + + +- 函数值:被作为参数的函数,可以带有参数值(就是带着行为的状态?)。 +- 函数类型的零值是nil。调用值为nil的函数值会引起panic错误。 +- 函数值可以与nil比较,但是函数值之间是不可比较的,也不能用函数值作为map的key。 + +```go + var f func(int) int + f(3) // 此处f的值为nil, 会引起panic错误 + + if f != nil { + f(3) + } +``` +- 通过行为来参数化函数:下面的`strings.Map`对字符串中的每个字符调用add1函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。 +```go +func add1(r rune) rune { return r + 1 } + +fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111" +``` + + +### 匿名函数 +>拥有函数名的函数只能在包级语法块中被声明 +**匿名函数**: 通过函数字面量(是一种表达式,就是函数声明func后不带函数名,而是直接func())语法可以在任何表达式中表示一个函数值。 +```go +strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000") +``` + +- 更为**重要**的是,通过这种方式定义的函数可以访问完整的**词法环境**(lexical environment),这意味着**在函数中定义的内部函数可以引用该函数的变量**。 +>不是很理解这个意味着有啥特别? 啥场景? 很难嘛?之前不支持? **看代码吧** +```go +// squares返回一个匿名函数。 +// 该匿名函数每次被调用时都会返回下一个数的平方。 +func squares() func() int { + var x int + return func() int { + x++ + return x * x + } +} +func main() { + f := squares() + fmt.Println(f()) // "1" + fmt.Println(f()) // "4" + fmt.Println(f()) // "9" + fmt.Println(f()) // "16" +} +``` +- 为啥每次调用值不一样?squares()里的局部变量不是每次调用`f()匿名函数`就会归零? + - 第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。 为啥第二个x变量初始值是1? +- **squares的例子证明,函数值不仅仅是一串代码,还记录了状态**。 +- 在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着**匿名函数和squares中,存在变量引用**。 +- 这就是**函数值**属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现**函数值**,Go程序员也**把函数值叫做闭包**。 + >这函数值是啥,越来越糊涂了? +- 这个例子展示了:**变量的生命周期不由它的作用域决定**:squares返回后,变量x仍然隐式的存在于f中。 + + +**网页抓取的核心问题就是如何遍历图**。 +- 在topoSort的例子中,已经展示了深度优先遍历,在网页抓取中,我们会展示如何用广度优先遍历图。 + + +**警告:捕获迭代变量** + +>介绍Go词法作用域的一个陷阱 + +需求:创建一些目录,然后将目录删除。 +- 没问题的代码: + - 为什么要在循环体中用循环变量d赋值一个新的局部变量,为什么要在循环体中用循环变量d赋值一个新的局部变量? 后面一种是错误的 +```go +//没问题的 +var rmdirs []func() //方法切片 +for _, d := range tempDirs() { + dir := d // NOTE: necessary! + os.MkdirAll(dir, 0755) // creates parent directories too + rmdirs = append(rmdirs, func() { + os.RemoveAll(dir) + }) +} + +// ...do some work… +for _, rmdir := range rmdirs { + rmdir() // clean up +} +``` +- 有问题的 +```go +var rmdirs []func() +for _, dir := range tempDirs() { + os.MkdirAll(dir, 0755) + rmdirs = append(rmdirs, func() { + os.RemoveAll(dir) // NOTE: incorrect! 这一步是把的dir传入函数中作为函数的变量?存起来了 + }) +} +// ...do some work… +for _, rmdir := range rmdirs { + rmdir() // clean up +} +``` +- for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。 +- 在该循环中生成的所有**函数值**(带参数的函数?)都共享相同的循环变量 +- **注意**:函数值中记录的是循环变量的**内存地址**,而不是循环变量某一时刻的值。 +- 以dir为例,后续的迭代会不断更新dir的值,**当删除操作执行时,for循环已完成**,dir中存储的值等于最后一次迭代的值。 +- 这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。 + + +### 可变参数 +参数数量可变的函数称为可变参数函数。 +- 需要在参数列表的最后一个**参数类型之前**加上省略符号“...” +- 调用者**隐式的创建一个数组**,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用函数。 +- 可以使用range去遍历它,那如果可变参数的类型就是一个切片呢? 相当于传了个二维切片,可以两次for循环遍历。 +```go +values := []int{1, 2, 3, 4} +fmt.Println(sum(values...)) // "10" +//等价于下面的 +fmt.Println(sum(1, 2, 3, 4)) // "10" +``` + + +**用处**: +- 可变参数函数经常被用于格式化字符串。 format后面带的多个参数。 + + +### defer函数 +- 当执行到该条语句时,**函数和参数表达式得到计算**,但直到包含该defer语句的函数执行完毕时,**defer后的函数**才会被执行。 (看下面的例子,似乎是defer最后的return是最后执行,其他的都是在经过时运行?) + >不太理解函数和参数表达式得到计算是什么意思? defer语句里面带的函数和表达式先计算了? 那不就是执行了嘛? 还是主函数流程? defer后的函数是啥 + **!!!测试发现**:defer后面的函数里面只要有return语句,那return之前的语句会执行(比如打印进入函数的时间),会停留大return位置,当原函数结束时执行return; + - 测试的方法是**返回一个函数**符合上面的说法,如果返回int呢? + - **这里要注意一个细节:** `defer trace("bigSlowOperation")()`后面的圆括号,表示前面部分返回必须为一个func然后方便带上()? 返回int这么写会报错,可以去掉圆括号,那么就不会报错,但是整个`trace("bigSlowOperation")`都会在原函数结束时执行. + + +- 不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。 +- 可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序**相反**。经过一条defer语句就入栈,最后执行时是出栈这些defer语句。 +- **常被用于**处理**成对**的操作,如打开、关闭、连接、断开连接、加锁、释放锁。 + + +调试复杂程序时,defer机制也常被**用于记录何时进入和退出函数**。 +```go +func bigSlowOperation() { + defer trace("bigSlowOperation")() // don't forget the extra parentheses + // ...lots of work… + time.Sleep(10 * time.Second) // simulate slow operation by sleeping +} +func trace(msg string) func() { + start := time.Now() + log.Printf("enter %s", msg) + return func() { + log.Printf("exit %s (%s)", msg,time.Since(start)) + } +} +``` + +>注意一点:不要忘记defer语句后的**圆括号**,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。 + + +defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数**包括返回值变量在内的所有变量**. +所以,对匿名函数采用defer机制,可以使其**观察函数的返回值**。 就是在defer里打印出来?对于有许多return语句的函数而言,这个技巧很有用。 + + +defer后面的函数可以修改 命名的返回值。 + + +for循环里面,不断打开file,只定义一个defer,可能出现打开太多,用满文件描述符。 解决办法: 将打开操作抽象出一个函数,在函数里定义defer,那每次打开就会及时关闭,再打开下一个。 + + +### Panic异常 +- 一般会将panic异常和日志信息一并记录。 +- 直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。 +- 当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径: +>在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理 + +**panic的适用场景与其他语言Exception的区别** +- panic一般用于严重错误,如程序内部的逻辑不一致。 优先使用错误处理机制,而不是panic。 + + +**在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。** +- 如何使程序从panic异常中恢复,阻止程序的崩溃。? +>为了方便诊断问题,runtime包允许程序员输出堆栈信息。在下面的例子中,我们通过在main函数中延迟调用printStack输出堆栈信息。 就是defer捕获,然后优雅的输出panic等错误信息 +```go +func main() { + defer printStack() + f(3) +} +func printStack() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) + os.Stdout.Write(buf[:n]) +} +``` + + +### Recover捕获异常 +> 一般不应该对panic异常做任何处理 +如果想从异常中恢复,或者说在程序奔溃前做一些操作,可以考虑使用recover() + +- 比如: 当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭; + + +如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。 +- 这个panic value就是recover函数返回值,由上游 调用 panic(value)传入 + +* 导致panic异常的函数不会继续运行,但能正常返回。 +- 在未发生panic时调用recover,recover会返回nil。 + +- 不应该试图去恢复其他包或者由他人开发的函数引起的panic。 +- 公有的API应该将函数的运行失败作为error返回,而不是panic。 +- 只恢复应该被恢复的panic异常: + - web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。 + - 为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。 + - 在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为error处理。 +```go +func soleTitle(doc *html.Node) (title string, err error) { + type bailout struct{} + defer func() { + switch p := recover(); p { + case nil: // no panic + case bailout{}: // "expected" panic + err = fmt.Errorf("multiple title elements") + default: + panic(p) // unexpected panic; carry on panicking + } + }() + // Bail out of recursion if we find more than one nonempty title. + forEachNode(doc, func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && + n.FirstChild != nil { + if title != "" { + panic(bailout{}) // multiple titleelements + } + title = n.FirstChild.Data + } + }, nil) + if title == "" { + return "", fmt.Errorf("no title element") + } + return title, nil +} +``` + +--- + +## 第六章 方法(method) +一个方法则是一个一个和特殊类型关联的函数。 + + +### 方法声明 +**在函数声明时**,在其名字之前放上一个变量,**即是一个方法**。 + +这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。 + +```go +package geometry + +import "math" + +type Point struct{ X, Y float64 } + +// traditional function +func Distance(p, q Point) float64 { + return math.Hypot(q.X-p.X, q.Y-p.Y) +} + +// same thing, but as a method of the Point type +func (p Point) Distance(q Point) float64 { + return math.Hypot(q.X-p.X, q.Y-p.Y) +} +``` +- 上面的代码里那个`(p Point)`附加的参数p,叫做**方法的接收器**(receiver).命名使用类型的第一个字母,简约。 +- p.Distance的表达式叫做**选择器**,因为他会选择合适的对应p这个对象的Distance方法来执行。 +- 选择器也会被用来选择一个struct类型的字段,比如p.X。 +>由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会**有歧义.** **方法和字段不能同名?** 但是方法却可以同名?比如下面的:`func (path Path) Distance()`与上面的在一个包下,由于接收器不同,也可以同名而不报错。**两个Distance方法有不同的类型**。他们两个方法之间没有任何关系。 +``` +// A Path is a journey connecting the points with straight lines. +type Path []Point +// Distance returns the distance traveled along the path. +func (path Path) Distance() float64 { + sum := 0.0 + for i := range path { + if i > 0 { + sum += path[i-1].Distance(path[i]) + } + } + return sum +} +``` +>但是**函数的签名其实就是函数的参数列表和结果列表的统称**,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。 +上面同名方法算函数嘛? 如果算,那签名是一致的,名字也一样,只有接收器不一样,不会报错? +- **进入了误区**: 函数同名不能出现一个包下,哪怕签名不一样也不行。 跟参数名字冲突也不行? 但是接收器不同那就是可以? + +>因为**每种类型都有其方法的命名空间**,我们在用Distance这个名字的时候,不同的Distance调用指向了不同类型里的Distance方法。 + + +也就是说,一个包内有一个方法的命名空间,而每种类型有自己的方法命名空间。 +- 在每个命名空间内部 方法、函数、字段都不能重名,调用会有歧义。 +- 在不同命名空间可以出现重名(哪怕都在一个文件里,也可以) + + +Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。**这也是Go与其他语言不同** +- 可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型不是指针或者interface。 + + +### 基于指针对象的方法 +- 一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。 +- 在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的。 + + + +如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,可以用下面这种简短的写法: +```go +p.ScaleBy(2) +``` +编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”。临时变量的地址获取不到,所以不行:`Point{1, 2}.ScaleBy(2)` + + +在每一个合法的方法调用表达式中,也就是下面三种情况里的任意一种情况都是可以的: + +1. 要么接收器的实际参数和其形式参数是相同的类型,比如两者都是类型T或者都是类型*T: +```go +Point{1, 2}.Distance(q) // Point +pptr.ScaleBy(2) // *Point +``` +2. 或者接收器实参是类型T,但接收器形参是类型*T,这种情况下编译器会隐式地为我们取变量的地址: +```go +p.ScaleBy(2) // implicit (&p) +``` +3. 或者接收器实参是类型*T,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量: +```go +pptr.Distance(q) // implicit (*pptr) +``` + + +- 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。 +- 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。 + + +**Nil也是一个合法的接收器类型** +- 一个map取值的知识点 + - 直接写nil.Get("item")的话是无法通过编译的,因为nil的字面量编译器无法判断其准确类型。 + - 尝试更新一个空map会报panic +```go +m = nil +fmt.Println(m.Get("item")) // "" +m.Add("item", "3") // panic: assignment to entry in nil map +``` + + +### 通过嵌入结构体来扩展类型 +- 被嵌入类型可以直接访问(匿名)嵌入的类型的字段和方法,不需要调用嵌入类型。 `cp.Point.X` 可以简写成 `cp.Y` +- 类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(访问时也需要先调用该对象再访问其字段) +- 当编译器解析一个选择器到方法时,比如p.ScaleBy,它会首先去找直接定义在这个类型里的ScaleBy方法,然后找被ColoredPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。 + - 在同一级出现一样的就会报错,有歧义,编译器不知道选择哪个。 + + +### 方法值和方法表达式 +没太看懂啥用 + + +### Bit数组 +- Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。 +- 表示非负整数时,使用bit数组,当集合的第i位被设置时,我们才说这个集合包含元素i。 + + +**fmt会直接调用用户定义的String方法** +- 当时有个需要注意的地方: + - 直接fmt.Println(x),会调用x类型的String(),如果只定义了x指针接收器的String()方法,那这里就会以原始的方式打印。 所以在这种情况下&符号是不能忘的(fmt.Println(&x))。在我们这种场景下,你把String方法绑定到IntSet对象上,而不是IntSet指针上可能会更合适一些。 + - 当然,如果这样写 `fmt.Println(x.String())`,编译器会隐式地在x前插入&操作符,也能正确调用到x的指针方法。 + + +### 封装 +**优点** +- 首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。 +- 第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样针对实现可以做很多优化,只要不破坏对外暴露的api。 +- 第三,也是最重要的,阻止了外部调用方对对象内部的值任意地进行修改。 + + +在命名一个**getter方法**时,我们通常会**省略掉前面的Get前缀**。 +- 这种简洁上的偏好也可以推广到各种类型的前缀比如Fetch,Find或者Lookup。 + + +## 第七章 接口 +>接口类型是对其它类型行为的抽象和概括; +- Go语言的接口类型——满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型。 + - 好处一:可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义; + - 好处二:当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。 + + +### 接口约定 +- 它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。 +- 当我们看到一个接口类型的值时,不知道它是什么,只知道通过它的方法来做什么。 + + +**举个例子:** +- fmt.Printf,它会把结果写到标准输出 +- fmt.Sprintf,它会把结果以字符串的形式返回 +- 它们都使用了另一个函数fmt.Fprintf来进行封装。 +```go +package fmt + +func Fprintf(w io.Writer, format string, args ...interface{}) (int, error) +func Printf(format string, args ...interface{}) (int, error) { + return Fprintf(os.Stdout, format, args...) +} +func Sprintf(format string, args ...interface{}) string { + var buf bytes.Buffer + Fprintf(&buf, format, args...) + return buf.String() +} +``` +- **注意**: Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。 + - 在Printf函数中的第一个参数os.Stdout是*os.File类型; + - 在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区,然而它并不是一个文件类型尽管它在某种意义上和文件类型相似。 + - **其实**,只要第一个参数实现了io.Writer接口类型的方法就行。io.Writer类型定义了函数Fprintf和这个函数调用者之间的**约定**。 +```go +// Writer is the interface that wraps the basic Write method. +type Writer interface { + // Write writes len(p) bytes from p to the underlying data stream. + // It returns the number of bytes written from p (0 <= n <= len(p)) + // and any error encountered that caused the write to stop early. + // Write must return a non-nil error if it returns n < len(p). + // Write must not modify the slice data, even temporarily. + // + // Implementations must not retain p. + Write(p []byte) (n int, err error) +} +``` + + +### 接口类型 +>一个实现了这些方法的**具体类型**是这个接口类型的**实例**。 + +io.Writer类型是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。 +**Go语言有单方法接口的命名习惯**,比如Reader,Closer。 + + +可以内嵌组合这些接口,当然,方式有多种,下面三种都是等价的。 +```go +//组合内嵌 +type ReadWriter interface { + Reader + Writer +} +//不用内嵌 +type ReadWriter interface { + Read(p []byte) (n int, err error) + Write(p []byte) (n int, err error) +} +//混搭 +type ReadWriter interface { + Read(p []byte) (n int, err error) + Writer +} +``` + + +### 实现接口的条件 +Go的程序员经常会简要的把一个具体的类型描述成一个特定的接口类型。举个例子,`*bytes.Buffer`是`io.Writer`;`*os.Files`是`io.ReadWriter`。 + + +如果类型实现了接口x,那就能直接赋值给接口x。 +```go +var w io.Writer +w = os.Stdout // OK: *os.File has Write method +w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method +w = time.Second // compile error: time.Duration lacks Write method +``` + + +先解释一个类型持有一个方法的表示当中的细节 +- 对于每一个命名过的具体类型T;它的一些方法的接收者是类型T本身然而另一些则是一个*T的指针。 +- 在T类型的参数上调用一个*T的方法是合法的,只要这个参数是一个变量;(编译器隐式的获取了它的地址) + - 但请注意:这说明T类型的值没有拥有全部*T指针的方法,它就可能只实现了部分的接口。 + - 举个例子:IntSet类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet值上调用这个方法。 + ```go + type IntSet struct { /* ... */ } + func (*IntSet) String() string + var _ = IntSet{}.String() // compile error: String requires *IntSet receiver + + ``` + - 但是可以在一个IntSet变量上调用这个方法 + ```go + var s IntSet + var _ = s.String() // OK: s is a variable and &s has a String method + ``` +- 也就是说只有 *IntSet类型实现了fmt.Stringer接口 +```go +var _ fmt.Stringer = &s // OK +var _ fmt.Stringer = s // compile error: IntSet lacks String method +``` + + +**空接口: interface{}** +- 空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。 + - 当然不能直接对它持有的值做操作,因为interface{}没有任何方法。 + - 可以用类型断言来获取interface{}中值的方法。 + + +如果我们发现我们需要以同样的方式处理Audio和Video,我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。 +```go +type Audio interface { + Stream() (io.ReadCloser, error) + RunningTime() time.Duration + Format() string // e.g., "MP3", "WAV" +} +type Video interface { + Stream() (io.ReadCloser, error) + RunningTime() time.Duration + Format() string // e.g., "MP4", "WMV" + Resolution() (x, y int) +} +type Streamer interface { + Stream() (io.ReadCloser, error) + RunningTime() time.Duration + Format() string +} +``` + + +### 7.4 flag.Value接口 +一个标准的接口类型flag.Value是怎么帮助命令行标记定义新的符号的? + + +### 接口值 +在一个接口值中,类型部分代表与之相关类型的描述符。 类型描述符--比如类型的名称和方法 + +一个接口的零值就是它的类型type和值value的部分都是nil。 + + +**比较** +- 接口值可以使用==和!=来进行比较。 +- 两个接口值**相等**仅当它们都是nil值,或者它们的**动态类型相同**并且**动态值**也相等(根据这个动态值类型对应的==操作相等,要求必须是可比较的)。 +- 接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。 +- 如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic: +- 接口介于安全的可比较类型和完全不可比较类型之间 + - 在比较接口值或者包含了接口值的聚合类型时,我们必须要意识到潜在的panic + - 在使用接口作为map的键或者switch的操作数也要注意 + - 只能比较你非常确定它们的动态值是可比较类型的接口值。 + - **可以使用使用fmt包的%T动作获取接口值的动态类型。**(内部使用反射来获取接口动态类型的名称) + + +**注意:一个包含nil指针的接口不是nil接口** +- 主要区别就是接口的type是否也为nil? +```go +const debug = true + +func main() { + var buf *bytes.Buffer + if debug { + buf = new(bytes.Buffer) // enable collection of output + } + f(buf) // NOTE: subtly incorrect! + if debug { + // ...use buf... + } +} + +// If out is non-nil, output will be written to it. +func f(out io.Writer) { + // ...do something... + if out != nil { //它的动态类型是*bytes.Buffer,是一个包含空指针值的非空接口 + out.Write([]byte("done!\n")) // panic: nil pointer dereference, 因为 out的动态值为空。 + } +} +``` +- 解决方案:就是将main函数中的变量buf的类型改为io.Writer,因此可以避免一开始就将一个不完整的值赋值给这个接口。 + +**为啥???** + + +### sort.Interface接口 +Go语言的sort.Sort函数**不会对具体的序列和它的元素做任何假设**。 +- 使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。 + - 一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式; +```go +package sort + +type Interface interface { + Len() int + Less(i, j int) bool // i, j are indices of sequence elements + Swap(i, j int) +} +``` + + +### http.Handler接口 + +- net/http包提供了一个请求多路器ServeMux来简化URL和handlers的联系。一个ServeMux将一批http.Handler聚集到一个单一的http.Handler中。 +```go +func main() { + db := database{"shoes": 50, "socks": 5} + mux := http.NewServeMux() + mux.Handle("/list", http.HandlerFunc(db.list)) + mux.Handle("/price", http.HandlerFunc(db.price)) + log.Fatal(http.ListenAndServe("localhost:8000", mux)) +} +type database map[string]dollars + +func (db database) list(w http.ResponseWriter, req *http.Request) { + for item, price := range db { + fmt.Fprintf(w, "%s: %s\n", item, price) + } +} +//省略price方法 + +``` + - db.list是一个实现了handler类似行为的函数, 它不满足http.Handler接口并且不能直接传给mux.Handle。 + - 语句http.HandlerFunc(db.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型。 + + +```go +package http + +type HandlerFunc func(w ResponseWriter, r *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +``` +- HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点: + - 它是一个实现了接口http.Handler的方法的函数类型。 + - ServeHTTP方法的行为是调用了它的函数本身。因此**HandlerFunc是一个让函数值满足一个接口的适配器**,这里函数和这个接口**仅有的方法有**相同的函数签名。 + +- 为了方便,net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。 +- 现在,为了**使用DefaultServeMux作为服务器的主handler**,我们不需要将它传给ListenAndServe函数;nil值就可以工作。 + - 下面代码就把`mux := http.NewServeMux()`这一步省了,除非需要多个服务器监听不同的端口,然后再构建不同的ServeMux去调用ListenAndServe。 但大部分情况只需要一个web服务器。 +```go +func main() { + db := database{"shoes": 50, "socks": 5} + http.HandleFunc("/list", db.list) + http.HandleFunc("/price", db.price) + log.Fatal(http.ListenAndServe("localhost:8000", nil)) +} +``` + + +>go语言目前还没有一个全为的web框架,Go语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外,尽管在一个项目早期使用框架是非常方便的,但是它们带来额外的复杂度会使长期的维护更加困难。 + + +### error接口 +```go +type error interface { + Error() string +} +``` +创建一个error最简单的方法就是调用errors.New函数,它会根据传入的错误信息返回一个新的error。整个errors包仅只有4行: + +```go +package errors +//每个New函数的调用都分配了一个独特的和其他错误不相同的实例。哪怕是一样的错误信息。 +func New(text string) error { return &errorString{text} } + +//使用结构而不是直接暴露字符串,为了保护它表示的错误避免粗心(或有意)的更新 +type errorString struct { text string } + +func (e *errorString) Error() string { return e.text } +``` + +- 用得更多的是fmt.Errorf,它还会处理字符串格式化。 +``` +func Errorf(format string, args ...interface{}) error { + return errors.New(Sprintf(format, args...)) +} +``` + + +### 类型断言 x.(T) +类型断言是一个使用在接口值上的操作,形如**x.(T)**。 +- x表示一个接口的类型和T表示一个类型。 +- 检查它操作对象x的动态类型是否和断言的类型T匹配。 + - 如果断言的类型T是一个**具体类型**,然后类型断言检查x的动态类型是否和T相同。 + - 检查成功,断言的结果是 x的动态值,类型是T。 也就是说具体类型的类型断言从它的操作对象中获得具体的值。 + - 检查失败,抛出panic + - 如果断言的类型T是一个**接口类型**,???(莫名其妙的翻译) + - 如果检查成功了,断言结果为类型T,不过保留了接口值内部的动态类型和值的部分? + - 如果失败了? +- 如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。 +```go +//第一个类型断言后,w和rw都持有os.Stdout,它们都有一个动态类型*os.File +var w io.Writer +//w只公开了Write方法 +w = os.Stdout +//rw变量还公开了它的Read方法 +rw := w.(io.ReadWriter) // success: *os.File has both Read and Write +w = new(ByteCounter) +rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method +``` +- 几乎不需要对一个更少限制性的接口类型(更少的方法集合)做断言。 +- 判断接口值的是否是某动态类型,然后根据结果去做一些操作,一般会使用返回两个结果断言: + - 第一个结果是表示断言得到的类型。如果失败了,就会等于被断言类型的零值 + - 第二个结果是ok bool,表示判断是否成功。 + + +**通过类型断言识别错误类型** + + +举例:os包中文件操作返回的错误原因,有三种经常的错误必须进行不同的处理:文件已经存在(对于创建操作),找不到文件(对于读取操作),和权限拒绝。 +- 如何对错误值表示的失败进行分类? + - 直接判断是否包含特定子字符串是不健壮的。 + - 更可靠的是使用一个专门的类型来描述结构化的错误。 比如os包中的PathError、LinkError。 + - 调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开;具体的类型可以比字符串提供更多的细节。 +```go +_, err := os.Open("/no/such/file") +fmt.Println(err) // "open /no/such/file: No such file or directory" +fmt.Printf("%#v\n", err) +// Output: &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2} + +//有几个特定的方法可以对错误类型进行判断 +func IsExist(err error) bool +func IsNotExist(err error) bool +func IsPermission(err error) bool + +//使用 +os.IsNotExist(err) +``` +- 区别错误通常必须在失败操作后,错误传回调用者前进行。 + + +**通过类型断言查询接口** + + +- 有一个允许字符串高效写入的WriteString方法;这个方法会避免去分配一个临时的拷贝。 +- 但是我们不确定某个io.Writer类型的变量是否拥有这个方法,可以定义一个只有这个方法的新接口,然后使用类型断言检测w的动态类型是否满足这个新接口。 +```go +func writeString(w io.Writer, s string) (n int, err error) { + type stringWriter interface { + WriteString(string) (n int, err error) + } + if sw, ok := w.(stringWriter); ok { + return sw.WriteString(s) // avoid a copy + } + return w.Write([]byte(s)) // allocate temporary copy +} + +func writeHeader(w io.Writer, contentType string) error { + if _, err := writeString(w, "Content-Type: "); err != nil { + return err + } + if _, err := writeString(w, contentType); err != nil { + return err + } + // ... +} +``` +- 这个例子的神奇之处在于,没有定义了WriteString方法的标准接口,也没有指定它是一个所需行为的标准接口。 +- 一个具体类型只会通过它的方法决定它是否满足stringWriter接口,而不是任何它和这个接口类型所表达的关系。 + + +定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手,特别是那些来自有强类型语言使用背景的新手,可能会发现它缺乏显式的意图令人感到混乱,但是在实战的过程中这几乎不是一个问题。除了空接口interface{},接口类型很少意外巧合地被实现。 + + +### 类型分支 +接口有两种使用方式: +- 第一种,以io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler和error为典型,一个接口的方法表达了实现这个接口的具体类型间的相似性,但是隐藏了代码的细节和这些具体类型本身的操作。**重点在于方法上,而不是具体的类型上。** +- 第二种,利用一个接口值可以持有各种具体类型值的能力,将这个接口当成这些类型的联合。使用类型断言用来动态地区别这些类型。 重点在于具体的类型满足这个接口,而不在于接口的方法,且没有隐藏任何信息。我们将以这种方式使用的接口描述为discriminated unions(可辨识联合)。 + + +- 一个类型分支像普通的switch语句一样,它的运算对象是`x.(type)`——它使用了关键词字面量type——并且每个case有一到多个类型。 +- 对于bool和string情况的逻辑需要通过类型断言访问提取的值,所以对于这个断言的值需要用一个临时变量存起来,方便使用。 +- 在每个单一类型的case内部,变量x和这个case的类型相同。 +```go +func sqlQuote(x interface{}) string { + switch x := x.(type) { + case nil: + return "NULL" + case int, uint: + return fmt.Sprintf("%d", x) // x has type interface{} here. + case bool: + if x { + return "TRUE" + } + return "FALSE" + case string: + return sqlQuoteString(x) // (not shown) + default: + panic(fmt.Sprintf("unexpected type %T: %v", x, x)) + } +} +``` + + +### 示例:基于标记的XML解码 +* encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。 +* 在基于标记的样式中,解析器消费输入并产生一个标记流; +* 四个主要的标记类型 + * StartElement,EndElement,CharData,和Comment + * 每一个都是encoding/xml包中的具体类型。 + + +### 接口补充说明 +- 接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。遵循这条规则必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口,而小的接口更容易满足。 +- 对于接口设计的一个好的标准就是 ask only for what you need(只考虑你需要的东西) + + + +--- + +## 第八章 并发编程(一)基于顺序通信进程(CSP) +>本章讲解goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。CSP是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中。 + +- 在语法上,go语句是一个普通的函数或方法调用前加上关键字go。 `go f()` +- go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。 +- 主函数返回时,所有的goroutine都会被直接打断,程序退出。 +- 除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行. 可以使用通信去让其主动退出。 + + +### 并发示例1和2 + +**示例1:并发的Clock服务** + +- 第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端: +- 但是那样客户端必须等待第一个客户端完成工作,这样服务端才能继续向后执行;因为我们这里的服务器程序同一时间只能处理一个客户端连接。 +- 我们这里对服务端程序做一点小改动,使其支持并发:在handleConn函数调用的地方增加go关键字,让每一次handleConn的调用都进入一个独立的goroutine。 +```go + +func handleConn(c net.Conn) { + defer c.Close() + for { + _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) + if err != nil { + return // e.g., client disconnected + } + time.Sleep(1 * time.Second) + } +} + +func main() { + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) // handle connections concurrently + } +} +``` + + +**示例2:并发的Echo服务** +```go +//reverb1 +func echo(c net.Conn, shout string, delay time.Duration) { + fmt.Fprintln(c, "\t", strings.ToUpper(shout)) + time.Sleep(delay) + fmt.Fprintln(c, "\t", shout) + time.Sleep(delay) + fmt.Fprintln(c, "\t", strings.ToLower(shout)) +} + +func handleConn(c net.Conn) { + input := bufio.NewScanner(c) + for input.Scan() { + echo(c, input.Text(), 1*time.Second) + //如果增加go,才能实现 上一次还没说完三次,下一次也会开始说。 不然就只能顺序说,只有上一句话说完,才能开始下一句。 + go echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() +} + +//netcat2 +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + go mustCopy(os.Stdout, conn) + mustCopy(conn, os.Stdin) +} +``` + +### 8.4 Channels +- 如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的**通信机制** +- 一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。 + + +**初始化** + +make函数初始化一个channel: +```go +ch := make(chan int) //可以发送int类型数据的channel +``` + + +**比较** + +- 两个相同类型的channel可以使用==运算符比较。 +- 如果两个channel引用的是相同的对象,那么比较的结果为真。 +- 一个channel也可以和nil进行比较。 + + +**操作** + +- 一个channel有发送和接受两个主要操作,都是通信行为。 +- 发送: `ch <- x // a send statement` +- 接收:一个不使用接收结果的接收操作也是合法的。 + - `x = <-ch` + - `<-ch` +- **关闭**:`close(ch)`。 关闭后 + - 基于该channel的任何发送操作都将导致panic异常。 + - 对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据; + - 如果channel中已经没有数据的话将产生一个零值的数据。 + + +**缓存通道** + +- 以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。 +```go +ch = make(chan int) // unbuffered channel +ch = make(chan int, 0) // unbuffered channel +ch = make(chan int, 3) // buffered channel with capacity 3 +``` + + +**不带缓存的Channels** + +- 一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。 +- 基于无缓存Channels的发送和接收操作将**导致两个goroutine做一次同步操作**。因为这个原因,无缓存Channels有时候也被称为**同步Channels**。 + + + +## 第九章 并发编程(二)传统的基于共享变量 +>在多goroutine之间的共享变量,并发问题的分析手段,以及解决这些问题的基本模式。 + + +## 第十章 包机制和包的组织结构 +>这一章还展示了如何有效地利用Go自带的工具,使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。 + + +- Go语言标准包200多个(查看命令:go list std | wc -l) +- 目前互联网上已经发布了非常多的Go语言开源包,它们可以通过 http://godoc.org 检索 + + +Go语言的**闪电**般的**编译速度主要得益于**三个语言特性。 +1. 第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。 +2. 第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。 +3. 第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。 +>在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件(译注:很多都是重复的间接依赖)。 + + +**导入路径**:为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。 + + +**包声明**:默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。 + + +关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况: +- main包:名字为main的包是给go build,构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。 +- _test.go结尾的文件:并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包: + - 一种是普通包, + - 另一种则是测试的外部扩展包。 + - 所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。 +- 带版本号:例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是yaml。 + + +**导入声明**: +- 如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做**导入包的重命名**。 +```go +import( + "crypto/rand" + mrand "math/rand" // alternative name mrand avoids conflict +) +``` +- 如果文件中已经有了一个名为path的变量,那么我们可以将“path”标准包重命名为pathpkg。 +- 每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。**如果遇到包循环导入的情况,Go语言的构建工具将报告错误。** + + +**包的匿名导入** +- 导入了又不用会编译报错。 + - 导入的意义是啥? 导入包后会做一些预处理,我只需要这些处理后的效果:它会计算包级变量的初始化表达式和执行导入包的init初始化函数。 + - 例如图像处理image包有这种场景:主程序只需要匿名导入特定图像驱动包就可以用image.Decode解码对应格式的图像了。 + - 例如数据库,匿名导入相应的数据库驱动包,直接就可以用。 +```go +import ( + "database/sql" + _ "github.com/lib/pq" // enable support for Postgres + _ "github.com/go-sql-driver/mysql" // enable support for MySQL +) + +db, err = sql.Open("postgres", dbname) // OK +db, err = sql.Open("mysql", dbname) // OK +db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3" +``` + - 怎么规避报错? 匿名导入,就是重命名为下划线 _ 。 + + +**包的命名原则** + +- 一般使用短小的,但也要易于理解无歧义。 ioutils够简洁了,就不需要命名为util。 +- 避免包名使用常用作局部变量的名字。例如path +- 一般采用单数 +- 设计变量名时,考虑与包名的混用。 所以不需要在变量名里包含包名的重复意思。 比如包名bos,变量名就不需要bosName +- 只暴露一个主要的数据结构和与它相关的方法,还有一个以New命名的函数用于创建实例。 + + +### 包的工具 +**GOPATH** + + +当需要切换到不同工作区的时候,只要更新GOPATH就可以了。 `export GOPATH=$HOME/gobook` + +GOPATH对应的工作区目录有三个子目录。 +- src子目录用于存储源代码 +- pkg子目录用于保存编译后的包的目标文件 +- bin子目录用于保存编译后的可执行程序 + + +**GOROOT** + + +GOROOT用来指定Go的安装目录,还有它自带的标准库包的位置。 +- 用户一般不需要设置GOROOT,默认情况下Go语言安装工具会将其设置为安装的目录路径。 + + +**其他环境变量** +- GOOS环境变量用于指定目标操作系统(例如android、linux、darwin或windows) +- GOARCH环境变量用于指定处理器的类型,例如amd64、386或arm等。 + + +**下载包** + + +- go get命令获取的代码是真实的本地存储仓库,而不仅仅只是复制源文件,因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。 + - -u 表示下载最新版本。 +- 进入文件目录,然后获取版本号,这里地址其实是有个转换的, https://golang.org/x/net/html 包含了如下的元数据,它告诉Go语言的工具当前包真实的Git仓库托管地址. +``` +$ cd $GOPATH/src/golang.org/x/net +$ git remote -v +origin https://go.googlesource.com/net (fetch) +origin https://go.googlesource.com/net (push) + +$./fetch https://golang.org/x/net/html | grep go-import + +``` + + +**编译build** + + +``` +$ cd anywhere +$ go build gopl.io/ch1/helloworld +``` +- go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。 +- 被编译的包会被保存到\$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。 +- go install命令和go build命令都**不会重新编译没有发生变化的包**. +- go build -i命令将安装每个目标所依赖的包。 + + +**分系统** +- 如果一个文件名包含了一个操作系统或处理器类型名字,例如net_linux.go或asm_amd64.s,Go语言的构建工具将只在对应的平台编译这些文件。 +- 在包声明和包注释前面,可以增加参数告诉build只在特定系统编译或者不编译这个文件: +``` +// +build linux darwin 只编译 +// +build ignore 不编译 + +``` + + +**包文档** +- 包中每个**导出**的成员和包声明前都应该包含目的和用法说明的注释。 +- 文档注释一般是完整的句子,第一行摘要说明,以被注释者的名字开头。 +- 注释中的参数直接用定义的名字就行,不需要额外的引号或者标记注明。 +- 包注释 + - 注释之后紧跟着包,这个注释就是包注释,只能有一个,多个文件同样包的注释会合并。 + - 如果包注释过长,可以单独放在一个文件里,一般叫做doc.go。 + - 文档要简洁不可忽视。 多看标准库。 +- `go doc`命令,该命令打印其后所指定的实体的声明与文档注释,该实体可能是一个包、某个具体的包成员、一个方法 + - 不需要输入完整的包导入路径或正确的大小写 +``` +$ go doc time +package time // import "time" +。。。 +$ go doc time.Since +$ go doc time.Duration.Seconds + +``` +- godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。 +- 也可以在自己的工作区目录运行godoc服务。运行下面的命令,然后在浏览器查看 http://localhost:8000/pkg 页面:`godoc -http :8000` + - 其中-analysis=type和-analysis=pointer命令行标志参数用于打开文档和代码中关于静态分析的结果。 + + +**内部包** + +作用:希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。 + +怎么用:Go语言的构建工具对**包含internal名字的路径段**的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。 +- 例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入 + + +**查询包**:go list命令可以查询可用包的信息。 `go list github.com/go-sql-driver/mysql` +- 用"..."表示匹配任意的包的导入路径。导出工作区所有包。 +- 某个主题相关的所有包:`go list ...xml...` +- -json命令行参数表示用JSON格式打印每个包的元信息: `go list -json hash` +- 命令行参数-f则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。 + - `go list -f '{{join .Deps " "}}' strconv` 用join模板函数将结果链接为一行 + - 打印compress子目录下所有包的导入包列表: `go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...` + + +## 第十一章 单元测试 +Go语言的工具和标准库中集成了轻量级的测试功能,避免了强大但复杂的测试框架。测试库提供了一些基本构件,必要时可以用来构建复杂的测试构件。 + + +- 文件名是:被测文件_test.go,这个后缀的源文件在执行go build时不会被构建成包的一部分。 +- 函数名:TestAdd(t *testing.T) +- 参数: + - 功能测试:`t *testing.T` + - 性能测试: `b *testing.B` + - 示例测试: 不限制 +- 功能 + - 功能测试(测试函数):以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确; + - 性能测试(基准测试):以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。 + - 示例函数:以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。 +- `go test`命令如果没有参数指定包那么将默认采用当前目录对应的包。 +- `go test`命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。 + + +### 测试函数(功能测试) + +- 每个测试函数必须导入testing包。函数有如下签名。 +```go +func TestName(t *testing.T) { + // ... +} +``` + + + + +## 第十二章 反射 + +一种程序在运行期间审视自己的能力。反射是一个强大的编程工具,不过要谨慎地使用;这一章利用反射机制实现一些重要的Go语言库函数,展示了反射的强大用法。 + + + +## 第十三章 底层编程的细节 + +在必要时,可以使用unsafe包绕过Go语言安全的类型系统。 \ No newline at end of file diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213.md" new file mode 100644 index 0000000..416a287 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213.md" @@ -0,0 +1,2010 @@ + +# 《Go语言核心36讲》 +[TOC] +## 学习路线 + +## 模块一:Go语言基础知识 + +### 工作区与GOPATH +- **GOROOT**:Go语言安装根目录的路径,也就是Go语言的安装路径。 +- **GOPATH**:若干工作目录的路径(可以是多个目录路径)。是我们自己定义的工作空间。 +- **GOBIN**:GO程序生成的可执行文件的路径。 + +问题: +1. Go语言源码的组织方式是怎样的? + - 以代码包为基本组织单元。跟java一样,多级目录就是子包。 + - 代码包一般会与源码文件所在目录同名(java好像是必须同名)。 如果不同名,在构建、安装过程中以代码包名称为准。 + - 而其他代码在使用该包中的实体时,引用的路径为**包路径?**(还是目录路径) + >一个代码包的导入路径实际上就是从 src 子目录,到该包的实际存储位置的相 对路径 + - 每个包可以包含任意个.go的源码文件。 + - Go 语言源码的组织方式就是以环境变量 GOPATH、工作区、src 目录和代码包为 主线的 +2. 你是否了解源码安装后的结果?(只有安装后,Go语言源码才能被我们或者其他代码使用) +3. 你是否理解构建和安装Go程序的过程? + +### 命令行源码文件 VS 库源码文件 + + +**库源码文件**是不能被直接运行的源码文件,它仅用于存放程序实体,这些**程序实体**可以被其他代码使用(只要遵从 Go 语言规范的话)。 +- 程序实体是变量、常量、函数、结 构体和接口的统称。 + +**问题**: 怎么把命令源码文件中的代码拆分到其他库源码文件? + +### 程序实体那些事儿 + +## 模块二:Go语言进阶技术 + +### 7. 数组和切片 + + +### 8. container包中的那些容器 +- **List** 实现了一个双向链表(以下简称链表) +- **Element** 则代表了链表中元素的结构。 + + +**可以把自己生成的Element类型值传给链表吗?** + + +List的四个方法: +- `MoveBefore`方法和`MoveAfter`方法,它们分别用于**把给定的元素移动**到另一个元素的**前面和后面**。 +- `MoveToFront`方法和`MoveToBack`方法,分别用于把给定的元素移动到链表的**最前端和最后端**。 + - “给定的元素”都是*Element类型的 + - *Element**类型**是Element类型的**指针类型** + - *Element的**值**就是元素的**指针**。 + - `Front`和`Back`方法分别用于**获取**链表中最前端和最后端的元素。 + - `InsertBefore`和`InsertAfter`方法分别用于在**指定的元素**之前和之后插入新元素。 + - `PushFront`和`PushBack`方法则分别用于在链表的**最**前端和最后端**插入**新元素。 +```go +//move +func (l *List) MoveBefore(e, mark *Element) +func (l *List) MoveToFront(e *Element) +//get +func (l *List) Front() *Element +//insert +func (l *List) InsertBefore(v interface{}, mark *Element) *Element +func (l *List) PushFront(v interface{}) *Element + +``` +- 函数名MoveBefore前面的`(l *List)`是啥东西? +- 这些方法都会把一个Element值的指针作为结果返回,它们就是链表留给我们的安全“接口”。 + + +**开箱即用** +- List和Element都是结构体类型。结构体类型有一个特点,那就是它们的零值都会是拥有 特定结构,但是没有任何定制化内容的值,相当于一个空壳。值中的字段也都会被分别赋予 各自类型的零值。 + - 零值:只做声明还未初始化的变量被给予了默认值。 比如, + - 经过语句var s []int声明的变量s的值将会是一个 []int类型的、值为nil的切片。 + - 经过语句var l list.List声明的变量l的值将会是一个长度为0的链表。这个链表持有的根元素也将会是一个空壳,其中只会包含缺省的内容 +- Go 语言标准库中很多结构体类型的程序实体都做到了开箱即用。这也是在编写可供别人使用的代码包(或者说程序库)时,我们推荐遵循的最佳实践之一。 +- List如何做到开箱即用? + - 关键在于它的“延迟初始化”机制,把初始化操作延后,仅在实际需要的时候才进行。 + - 这里的链表实现中,一些方法是无需对是否初始化做判断的。比如Front方法和Back方 法,一旦发现链表的长度为0, 直接返回nil就好了。 + - 链表的PushFront方法、PushBack方法、PushBackList方法以及 PushFrontList方法总会先判断链表的状态,并在必要时进行初始化,这就是延迟初始 化。 + - List利用了自身以及Element在结构上的特点,巧妙地平衡了延迟初始化的优 缺点,使得链表可以开箱即用,并且在性能上可以达到最优。 +- Element类型包含了几个包级私有的字段,分别用于存储前一个元素、后一个元素以及所 属链表的指针值。 +- 另外还有一个名叫Value的公开的字段,该字段的作用就是持有元素的实 际值,它是interface{}类型的。 + + +**问题 2:Ring与List的区别在哪儿?** +- ring实现的是一个循环列表,俗称的环。 +- 其实List在内部就是一个循环列表,只是它的根元素永远不会有任何的元素值,该元素的存在就是为了连接这个循环链表的首尾两端。 +- 区别? + - Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。 + - 一个Ring类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List类 型的值则代表了一个完整的链表。 + - 创建并初始化一个Ring值的时候,我们可以指定它包含的元素的数量,但是对于一个 List值来说却不能这样做(也没有必要这样做),ring是固定大小的。 + - 仅通过`var r ring.Ring`语句声明的r将会是一个长度为1的循环链表,而List类型的 零值则是一个长度为0的链表。 + - Ring值的Len方法的算法复杂度是 O(N) 的,而List值的Len方法的算法复杂度则是 O(1) 的 + + +切片这种动态扩展的特性,会出现复制很多个?啥时候回收?很浪费呀! + +- 在切片被频繁“扩容”的情况下,新的底层数组会不断产生,这时内存分配的量以及元素复制的次数可能就很可观了,这肯定会对程序的性能产生负面的影响。 +- 尤其是当我们没有一个合理、有效的”缩容“策略的时候,旧的底层数组无法被回收,新的底层数组中也会有大量无用的元素槽位。 +- 过度的内存浪费不但会降低程序的性能,还可能会使内存溢出并导致程序崩溃。 + + +由此可见,正确地使用切片是多么的重要。 + + +**典型使用场景** +- list的一个典型应用场景是构造FIFO队列; 作为queue和stack的基础数据结构 +- ring的一个典型应用场景是构造定长环回队列, 比如网页上的轮播; +- heap的一个典型应用场景是构造优先级队列。heap可以用来排序 + + +### 9. 字典(map)的操作和约束 +**创建**: +- 直接声明“var m map[int]string”的形式声明出来的m为nil; +- 采用make函数创建的map不为nil,可以进行添加键值对的操作。 + + +其实是一个哈希表的特定实现,在这个实现中,**键的类型是受限的,而元素却可以是任意类型的**。 +- 如何通过键值去定位映射的value元素 + - 哈希表会先用哈希函数(hash function)把键值转换为哈希值。**哈希值通常是一个无符号的整数**。 + - 一个哈希表会持有一定数量的桶(bucket),我们也可以叫它哈希桶,这些哈希 桶会均匀地储存其所属哈希表收纳的键 - 元素对。 + - 哈希表会先用这个键哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。 + - 由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素 值。 + + +字典不会单独存储键的值,只会存储键的哈希值,而查找的时候如果哈希值匹配上,还需要去比较键值,防止哈希碰撞。这里的键值是放在哪里?如何拿到的? +>哈希桶里的结构是,“键的哈希值-内部结构”对的集合,这个内部结构的结构是“键1 元素1 键2 元素2 键3 元素3”,是一块连续的内存。在通过键的哈希值定位找到哈希桶和那 个“键的哈希值-内部结构”对之后,就开始在这个内部结构里找有没有这个键。 + +**字典的键类型不能是哪些类型?** +>Go 语言 字典的**键**类型**不**可以是**函数类型、字典类型和切片类型**。 +- 键类型的值**必须要支持判等**操作。而函数类型、字典类型和切片类型的值并不支持判等操作 + - 因为要去根据哈希值寻找值在哪个桶去判断等,如果哈希值相等,还需要判断值是否相等,这里就要求值必须可以比较相等了。因为会存在哈希碰撞。 +- 如果键的类型是接口类型的,那么键值的实际类型也不能是上述三种类型 +```go +//变量badMap2的类型是键类型为interface{}、值类型为int的字典类型。这样声明并不会引起什么错误。 +或者说,我通过这样的声明躲过了 Go 语言编译器的检查 +var badMap2 = map[interface{}]int{ + "1": 1, + []]int{2}: 2, // 这里会引发 panic。 + 3: 3, + } +``` +- 当我们运行这段代码的时候,Go 语言的运行时(runtime)系统就会发现这里的问 题,它会抛出一个 panic。 +- 我们越晚发现问题,修正问题的成本就会越高,所以**最好不要把字典的键类型设定为任何接口类型**。 +- 如果键的类型是数组类型,那么还要确保该类型的元素类型不是函数类型、字典 +类型或切片类型。 + + +**应该优先考虑哪些类型作为字典的键类型?** +- 从性能考虑:**求哈希和判等操作的速度越快**,对应的类型就**越适合**作为键类型 + - 以求哈希的操作为例,宽**度越小**的类型**速度通常越快**。 + - 类型的**宽度**是指它的单个值需要占用的字节数。bool、int8和uint8类型的一个值 需要占用的字节数都是1 + - 对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。 + - 对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈 希越快。 + - 对数组类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度。 修改数组的值就是不一样的hash值了,所以不推荐使用 + - 对结构体类型的值求哈希实际上就是对它的所有字段值求哈希并进行合并,所以关键在于它的各个字段的类型以及字段的数量。可以控制其中各字段的访问权限的话,就可以阻止外界修改它了 + - 而对于接口类型,具体的哈希算法,则由值的实际类型决定。把接口类型作为字典的键类型**最危险**。 + - 优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。 + + +**在值为nil的字典上执行读操作会成功吗,那写操作呢?** +- 在一个值为nil的字典上,添加键-元素会引起运行时抛出一个panic,其他任何操作都不会引起错误。 + - 既然无法添加键值对,那是不是就无用? **可以对m直接用索引表达式添加** + + +其他: +- map不是并发安全的, 判断一个操作 是否是原子的可以使用 `go run race` 命令做数据的竞争检测 + +### 10. 通道的基本操作 +>Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。) ---Go 语言的主要创造者之一的 Rob Pike 的至理名言 + +通道类型是后半句话的完美实现,我们可以利用通道在多个 goroutine 之间传递数据。 + + +**基础知识**: +- **通道类型的值**本身就是**并发安全**的,这也是 Go 语言自带的、唯一一个可以满足并发安全性 的类型。 +- 用内建函数make声明一个通道类型变量,第一个参数用代表**通道具体类型**的类型字面量。 同时要确认该通道的**元素类型**,决定这个通道传递声明类型数据。 + - `chan int`,其中chan表示通道类型的关键字, int是该通道的元素类型。 + - make函数除了必须接收这样的类型字面量作为参数,还可以接收一个可选的int类型的参数作为容量。 + - 当容量为0时,我们可以称通道为非缓冲通道,反之就是缓存通道。 +- 一个通道相当于一个**先进先出**(FIFO)的队列。 + - 也就是说,通道中的各个元素值都是严格地**按照发送的顺序排列**的,先被发送通道的元素值一定会先被接收。 +- 元素值的发送和接收都需要用到**操作符<-**。 + - 我们也可以叫它**接送操作符**。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。 +- 从通道接收元素值的时候,同样要用接送操作符<-,只不过,这时需要把它写 在变量名的左边,用于表达“要从该通道接收一个元素值”的语义。 + - 比如`<-ch1`,这也可以被叫做**接收表达式**。 + - 如果我们需要把如此得来的元素值存起来,那么在接收表达式的左边就需要依次添加赋值符 号(=或:=)和用于存值的变量的名字。 +```go +func main() { + ch1 := make(chan int, 3) + ch1 <- 2 + ch1 <- 1 + ch1 <- 3 + //将最先进入ch1 的元素2接收来并存入变量elem1 + elem1 := <-ch1 + fmt.Printf("The first element received from channel ch1: %v\n",elem1) +``` + + +**对通道的发送和接收操作都有哪些基本特性?** +- 对于同一个通道,发送操作之间和接收操作之间是互斥的。 + - 发送和接收操作之间呢? + - 对于通道中的**同一个元素值**来说,**发送操作和接收操作之间也是互斥的**。 +- 发送操作和接收操作对元素值的处理是原子性的不可分割。 + - 元素值从外界进入通道时会被复制,也就是进入通道的是其副本。 + - 移动操作分两步: 先生存通道值这个元素值的副本,准备给到接收方,接着删除通道中这个元素值。 + - 接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。 +- 发送操作完成之前会被阻塞,接收操作也是如此。 + - 接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。 + - 在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。 + - 如此阻塞代码其实就是为了实现操作的互斥和元素值的完 整。 +- 长度代表通道当前包含的元素个数,容量就是初始化时你设置的那个数。 +- 通道底层存储数据的是环形链表。 + + +**发送操作和接收操作在什么时候可能被长时间的阻塞?** +- 缓存通道: + - 满了-》所有发送操作会被阻塞,知道元素被接收走,会优先通知最早等待、那个发送操作所在的goroutine。 + - 空了-》接收操作被阻塞。。。 +- 非缓存通道:无论是发送操作还是接收操作,一开始执行就会被阻 塞,直到配对的操作也开始执行,才会继续传递。 + - 就是**同步**的方式传递数据。 + - 数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。**那还会有删除通道中的元素这个操作嘛?** +- 一定不要忘记初始化通道。(用make函数去做初始化) + - 对于值为nil的通道,无论它具体是什么类型,对它的发送操作和接收操作都会永久地处于阻塞状态。 + - 通道是引用类型。 + + +**发送操作和接收操作在什么时候会引发 panic?** +- 对已关闭的通道进行发送操作,会应发panic +- 试图关闭一个已经关闭的通道。 + +接收操作是可以感知到通道的关闭的,并能够安全退出。 +- 接收表达式的结果会有两个变量,第二个变量类型是bool,为false表示通道已经关闭,并且没有元素可以取了。 +- 如果还有元素可以取,但是通道关闭了,那么接收表达式的第一个结果,仍会是 通道中的某一个元素值,而第二个结果值一定会是true。(这个接收操作是在关闭后做的还是?是在关闭后,**通道关闭后还是可以进行接收操作**?直到其没有元素) + - 因此,通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。 + - 所以,**千万不要让接收方关闭通道,而是让发送发做这件事。** + + +### 11. 通道的高级用法 +**单项通道**: 只能接收或者只能发送的通道 +- 定义(**从操作chan的代码的角度**): + - chan<- int 只能发送不能接收。(只能把数据发给chan) + - <-chan int 只能接收不能发送。 +```go +var uselessChan = make(chan<- int, 1) +``` + + +**单项通道有什么应用价值?** +最主要的用途就是约束其他代码的行为。 +>一个类型如果想成为一个接口类型的实现类型,那么就必须实现这个接口中定义的所有方法。 +- 因此,如果我们在某个方法的定义中使用了单向通道类型,那么就相当于在对它的所有实现做出约束。 +- 在**函数声明的结果列表中使用单向通道**: 函数getIntChan会返回一个<-chan int类型的通道,这就意味着得到该通道的程序, 只能从通道中接收元素值。这实际上就是对函数调用方的一种约束了。 +```go +func getIntChan() <-chan int { + num := 5 + ch := make(chan int, num) + for i := 0; i < num; i++ { + ch <- i + } + close(ch) + return ch +} +``` +- 在 Go 语言中还可以声明函数类型,如果我们在函数类型中使用了单向通道,那 么就相等于在约束所有实现了这个函数类型的函数. + + +顺便看一下调用getIntChan的代码: +```go +intChan2 := getIntChan() +for elm := range intChan2 { + fmt.Printf("The element in intChan2: %v\n", elem) +} +``` +把调用getIntChan得到的结果值赋给了变量intChan2,然后用for语句循环地取出了 该通道中的所有元素值,并打印出来。 + +带有range子句的for语句的用法说明: +- 一、这样一条for语句会不断地尝试从intChan2种取出元素值,即使intChan2被关 闭,它也会在取出所有剩余的元素值之后再结束执行。 +- 二、当intChan2中没有元素值时,它会被阻塞在有for关键字的那一行,直到有新的元 素值可取。 +- 三、假设intChan2的值为nil,那么它会被永远阻塞在有for关键字的那一行。 +>这就是带range子句的for语句与通道的联用方式。 Go 语言还有一种专门为了操作 通道而存在的语句:select语句。 + + +**select语句与通道怎样联用,应该注意些什么?** +- select语句只能与通道联用,它一般由若干个分支组成。每次只有一个分支中的代码会被运行。 +- 分支分为2种: + - 候选分支: 总是以 case 开头,后跟一个case表达式和一个冒号,再下一行写需要执行的语句。 类似switch的case + - 默认分支: default case, 同上。 +- 每个case表达式只能包含操作通道的表达式,比如接收表达式。 + >如果我们需要把接收表达式的结果赋给变量的话,还可以把这里写成**赋值语句或者短变量声明。** +```go +func example1() { + // 准备好几个通道。 + intChannels := [3]chan int{ + make(chan int, 1), + make(chan int, 1), + make(chan int, 1), + } + // 随机选择一个通道,并向它发送元素值。 + index := rand.Intn(3) + fmt.Printf("The index: %d\n", index) + intChannels[index] <- index + // 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。 + select { + case <-intChannels[0]: + fmt.Println("The first candidate case is selected.") + case <-intChannels[1]: + fmt.Println("The second candidate case is selected.") + case elem := <-intChannels[2]: + fmt.Printf("The third candidate case is selected, the element is %d.\n", elem) + default: + fmt.Println("No candidate case is selected!") + } +} +``` + + +### 12. 使用函数的正确姿势 +下面这段代码中的`printToStd`函数不需要定义两个返回值的嘛?确实没有报错。 +```go +package main + +import "fmt" + +type Printer func(contents string) (n int, err error) + +func printToStd(contents string) (bytesNum int, err error) { + return fmt.Println(contents) +} + +func main() { + var p Printer + p = printToStd + p("something") +} +``` + + +**如何编写高阶函数?** + + +“把函数传给函数”以及“让函 数返回函数”来编写高阶函数。 +>既不要把你程序的细节暴露给外界,也尽量不要让外界的变动影响到你的程序。 +```go +type operate func(x, y int) int + +func calculate(x int, y int, op operate) (int, error) { + if op == nil { + return 0, errors.New("invalid operation") + } + return op(x, y), nil 6 +} + +func main(){ + +} +``` +- 把函数作为一个普通的值赋给一个变量。 +- 实现一个签名与operate类型的签名一致的函数即可。 + + +**如何实现闭包?** +- 在一个函数中存在对外来标识符的引用 + - **外来标识符**:也叫自由变量, 既不代表当前函数的任何参数或结果,也不是函数内部声明的,而是直接从外边拿过来的。 +- 闭包体现的是由”不确定“变为”确定“的一个过程。 +- 我们常说的 **闭包函数**: 就是引用了自由变量,而呈现出一种“不确定”的状态,也叫“开放状态” + - 它的**内部逻辑并不是完整**的,有一部分逻辑需要这个自由变量参与完成,在闭包函数被定义的时候自由变量到底代表了什么是未知的。(只知道类型) +- 示例: + - genCalculator函数只做了一件事:定义一个**匿名的**、calculateFunc**类型**的**函数并把它作为结果值**返回 + - 这个匿名的函数就是一**个闭包函数**。 + - 它里面使用的变量op既不代表**它**的任何参数或结果 也不是它自己声明的,而是定义它的genCalculator函数的参数,所以是一个自由变量 + - 这个自由变量究竟代表了什么?不是在这个闭包函数定义的时候确定的,而是在genCalculator函数被调用的时候确定的————会把op作为其参数传进来,只有给定了该函数的参数op,我们才能知道它返回给我们的闭包函数可以用于什么运算。) + - 当运行到`if op == nil`·时,编译器会试图寻找op所代表的东西,发现op代表genCalculator函数的参数,这个时候该自由变量被捕获了,也就是确定它是啥了。如此一来,这个闭包函数的状态就 由“不确定”变为了“确定”,或者说转到了“闭合”状态,至此也就真正地形成了一个闭包。 +```go + +func genCalculator(op operate) calculateFunc { + return func(x int, y int) (int, error) { + if op == nil { + return 0, errors.New("invalid operation") + } + return op(x, y), nil + } +} +``` + +用高阶函数实现闭包。 +![](../img/《Go语言核心36讲》/《Go语言核心36讲》_2022-05-14-16-41.png) + + +**实现闭包的意义是啥?** +- 表面上看,就是延迟实现了一部分程序逻辑或功能而已。 +- 实际上,是在 **动态地生成**那部分逻辑。 + - 可以根据生成功能不同的函数,从而影响后续的程序行为,**类似模板方法**。 + + +**函数的传参** + + +- 传值的拷贝 + - 数组是值类型,其拷贝就是值,内部对其修改,不会影响外面的数组 + - 对于引用类型,拷贝是浅拷贝,只是拷贝指向其本身(也就是引用、指针),而不是其背后的值。 所以对引用、指针做了修改,也就是会改动地址背后的值了。 + - 如果拷贝的是值类型的参数值,**但这个参数值中某个元素是引用类型**? 比如一个数组里面存的是切片,那这个数组作为参数被修改,对外面会有影响嘛? +```go +complexArray := [3][]string{ + []string{"a", "b", "c"}, + []string{"d", "be", "g"}, + []string{"f", "g", "r"}, +} +``` +- 如果修改的是数组,比如把 `arr[0] = []string{"ww", "gg", "cc"} `,那不会影响外面; 如果修改的是这个数组的值也就是里面的切面,那就会影响。 + + + +**返回值是否也会进行拷贝?** +- 当函数**返回指针类型时不会发生拷贝**。 +- 当函数返回非指针类型并把结果赋值给其它变量肯定会发生拷贝。 那如果不进行赋值,就是调用下? + + +**函数和方法的区别?** +- 函数是独立的程序实体。 声明时可以有名/无名,可以当做普通的值传来传去。 +- 我们能把具有相同签名的函数抽象成独立的函数类型,以作为一组输入、输出(或者说一类逻辑组件)的代表。 +- 方法: **需要有名字**, 不能当做值来开年代,**比如隶属于某一个类型**(通过声明中的接收者声明体现)。 + - 接收者声明必须包含确切的名称和类型字面量(类型T或者其指针 *T,后者需要改变其状态时使用) + - 接收者的类型其实就是当前方法所属的类型, + - 而接收者的名称,则用于在当前方法中引用它所属的类型的当前值。 + - 通过方法add的接收者p,我们可以在其中引用到当前值的任何一个字段,或者调用到当前值的任何一个方法(也包括add方法自己) + - 方法隶属的类型其实并不局限于结构体类型,但必须是某个自定义的数据类型,并且**不能是任何接口类型**。 + + +有一种说法:方法的定义感觉本质上也是一种语法糖形式,其本质就是一个函数,**声明中的方法接收者就是函数的第一个入参**,在调用时go会把施调变量作为函数的第一个入参的实参传入。 + +### 13. 结构体及其方法使用法门 + +**内嵌其他结构体** + +- 如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。 +- 我们可以通过此类型变量的名称后 跟“.”,再后跟嵌入字段类型的方式引用到该字段。其实默认生成了一个字段名,就是类型名。 +- 但是也可以直接用大的结构体名,直接.操作符 获取子匿名结构体字段 +>注意,只要名称相同,无论这两个方法的签名是否一致,被嵌入类型的方法都会“屏蔽”掉嵌入字段的同名方法。 比如string方法。 字段同名也会被覆盖?是否无视类型是否相同? + + +**值方法和指针方法的区别** +- 方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指针类型。 +- 所谓的值方法,就是接收者类型是非指针的自定义数据类型的方法。值方法的接收者是该方法所属的那个类型值的一个副本。对齐修改一般不会影响原值(除非原值就是指针类型) +- 指针方法接收者是那个基本类型的指针值的副本。 + +### 14. 接口类型的合理利用 +```go +type Fly interface{ + +} +``` + +* 接口类型与其他数据类型不同,它是**没法被实例化**。 + * 既不能通过调用new函数或make函数创建出一个接口类型的值,也无法 用字面量来表示一个接口类型的值 +- 接口类型的类型字面量与结构体类型的看起来有些相似,它们都用花括号包裹一些核心信息。 + - 只不过,**结构体类型包裹的是它的字段声明**,而**接口类型包裹的是它的方法定义**。 + - 一个接口的方法集合就是它的全部特征。 + - 对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型。 + + +**怎样判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢?** +- 方法签名一致, 方法名称也要一致。 + + +```go +dog := Dog{"little pig"} +var pet Pet = &dog +``` +**针对接口类型的几个名词** +- **动态值**: 就是把dog的实例赋给了 pet +- **动态类型**: 这个值的实际类型就叫动态类型。 +- **静态类型**: pet 的静态类型是Pet,并且永远是Pet,动态值可以值dog或者cat等 + + +如果我们使用一个变量给另外一个变量赋值,那么真正赋给后者的,并不是前者持有的那个值,而是该值的一个副本。 +```go +dog := Dog{"little pig"} +var pet Pet = dog +dog.SetName("monster") +``` +>接口变量的值并不等同于这个可被称为动态值的副本。它会包含两个指针,一个指针指向动态值,一个指针指向类型信息. 基于此,扩展下面一个知识点。 + + +**知识点** +``` +var dog1 *Dog //dog1 is nil +dog2 := dog1 //dog2 is nil +var pet Pet = dog2 //pet is not nil +``` +- 只要我们把一个有类型的nil赋给接口变量,那么这个变量的值就一定不会是那个真正的 nil。 +- 因此,上面代码当我们使用判等符号==判断pet是否与字面量nil相等的时候,答案一定会是 false。 + - 在 Go 语言中,我们把由字面量nil表示的值叫做无类型的 nil。这是真正的nil,因为它的类型也是nil的。 + - 虽然dog2的值是真正的nil,但是当我们把这个变量赋给pet的时候,Go 语言会把它的类型和值放在一起考虑。这时 Go 语言会识别出赋予pet的值是一个*Dog类型的nil。然后,Go 语言就会用一个iface的实例包装它,包装后的产物肯定就不是nil了。 +- 怎样才能让一个接口变量的值真正为nil呢? + - 要么只声明它但不做初始化, + - 要么直接 把字面量nil赋给它。 + + +**接口之间组合** +- 通过嵌入,而且不像结构体嵌入会出现同名字段方法的屏蔽, **接口出现同名方法无法通过编译**,哪怕方法签名不同也不行。 +- Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程 序、增加程序的灵活性。 +- + +### 15. 关于指针的有限操作 +- 指针是一个指向某个确切的内存地址的值。 +- **uintptr**类型: 实际上是一个数值类型,也是 Go 语言内建的数据类型之一。 + - 它可以存储 32 位或 64 位的无符号整数,可以代表任 何指针的位(bit)模式,也就是原始的内存地址。 +- unsafe包中有一个类型叫做**Pointer**,也代表 了“指针”。 + - unsafe.Pointer可以表示任何指向**可寻址**的值的指针,同时它也是前面提到的指针值和 uintptr值之间的桥梁。 + + +**Go 语言中的哪些值是不可寻址?** +- **不可变的**: + - 常量 + - 基本类型值的字面量 + - 字符串值也是不可变的,所以字符串的索引和切片也是不可寻址的。 + - 函数以及方法的字面量。(也考虑到安全性) +- **临时结果**: + - 算术操作的结果值。 这些结果值赋给任何变量或者常量之前是临时的,之后就可以寻址了。 + - 可以把各种对**值字面量**施加的**表达式的求值结果**都看做是临时结果: + - 获取某个元素的索引表达式 + - 数组字面量和字典字面量的索引结果值 + - 数组字面量和切片字面量的切片结果值 + - 获取某个切片的切片表达式(不是索引表达式) + - **例外**:切片的索引表达式,也就是切片字面量的索引结果值是可寻址的。每个切片值都会持有一个底层数组,而这个底层数组中的每个元素值都有一个确切的内存地址。 + - 而切片的切片表达式是不可寻址的,因为切片表达式总会返回一个新的切片值,而这个新的切片值在被赋予给变量之前属于临时结果。赋值之后就是可以寻址的了。 + >**注意**:上面在说针对数组值、切片值或字典值的字面量的表达式会产生临时结果。如果针对的是数组类型或切片类型的变量,那么索引或切片的结果值就都不属于临时结果了,是可寻址的。 因为**变量的值本身就不是临时的**。 + + - 访问某个字段的选择表示 + - 调用某个函数或方法的调用表达式 + - 转换值的类型的类型转换表达式 + - 判断值的类型的类型判断表达式 + - 向通道发送元素值或者从通道接收元素值的接收表达式 +- **不安全的**: + - 不安全的”操作很可能会破坏程序的一致性,引发不可预知的错误,从而严重影响程序的功能和稳定性。 + - 对字典类型的变量施加索引表达式,得到的结果值不属于临时结果,可是,这样的值却是不可寻址的。原因是,字典中的每个键 - 元素对的存储位置都可能会变化,而且这种变化外界是无法感知的。不安全。 + - 获取由字面 量或标识符代表的函数或方法的地址显然也是不安全的。 + + +**不可寻址的值在使用上有哪些限制?** +- 无法使用取址操作符&获取它们的指针。 会编译报错。 + + + +引入个问题,下面程序如果直接调用New函数**初始化Dog类型的值**,然后 **直接以链式手法**调用其结果值的**指针方法**SetName,是否能达到预期效果? +```go +func New(name string) Dog { + return Dog{name} +} +``` +- 调用New函数所得到的结果值属于临时结果,是不可寻址的。 +- 但是在一个基本类型的值上调用它的指针方法,这是因为 Go 语言会自动地帮我们转译。 +>`dog.SetName("monster") `会被自动地转译为`(&dog).SetName("monster")`,即:先取dog的指针值,再在该指针 值上调用SetName方法。 +- 对不可寻址的结果值做取址操作,会报错。 但是如果调用的是非指针方法,就没问题。 + + +Go 语言中的++和--并不属于操作符,而分别是自增语句和自减 语句的重要组成部分。 +- 只要在++或--的左边添加一个表达式,就可以组成一 个自增语句或自减语句。 +- 这个表达式的结果值必须是可寻址的。 这导致 **值字面量**的表达式都无法使用自增、自减语句。 +- **例外**: 字典字面量和字典变量索引表达式的结果值都是不可寻址的。 + - 可以被用在自增自减语句中。 + - 还能被用在赋值语句的赋值操作符左边的表达式里(这个结果值也必须是可寻址的) + - 还有,range关键字左边的表达式的结果值也都必须是可寻址的 + +### 16. go语句 +>Don’t communicate by sharing memory; share memory by communicating. + +goroutine 代表着并发编程模型中的用户级线程。 + + +- Go语言不仅有独特的并发编程模型以及用户级线程goroutine,还拥有强大的用于调度goroutine、对接系统级线程的调度器。 +- 调度器负责统筹调配Go并发编程模型中的三个主要元素:G(gogroutine)、P(processor 对接G和M)、M(machine 系统级线程) + + +**什么是主gogroutine?它与我们启动的其他goroutine有什么区别?** + +看个题目:下面代码会打印出什么? 会是“打印出 10 个10”嘛? **啥都不打印** +>由于go func后,主代码会继续往下,也就是下个迭代,但快速迭代完10次之后,第一个进入栈的go func也许都还没打印,主流程就结束退出了。当然,也可能在主流程结束前,go func有被运行的,但是不保证顺序,不保证有多少. + +**注意:** i的值为啥会变化?不是值的副本嘛? +>如果go函数是无参数的匿名函数,那么在它里面的 fmt.Println函数的参数只会在go函数被执行的时候才会求值。到那个时候,i的值可能已经是 10(最后一个数)了,因为for语句那时候可能已经都执行完毕了。 +```go +package main + +import "fmt" + +func main(){ + for i := 0; i < 10; i++ { + go func(){ + fmt.Println(i) + }() + } +} +``` +- go函数的执行时间总是会明显滞后于它所属的go语句的执行时间(做一些上下文准备)。 + >**准备**:在拿到了一个空闲的 G 之后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数 (或者说该函数中的那些代码),然后再把这个 G 追加到某个存放可运行的 G 的队列中 +- 因为只要go语句本身执行完毕, Go 程序完全不会等待go函数的执行,它会立刻去执行后边的语句。 +- 一旦主 goroutine 中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。 + + +**用什么手段可以对 goroutine 的启用数量加以限制?** +- goroutine pool +- buffered channel +- WaitGroup + + +**怎样才能让主 goroutine 等待其他 goroutine?** +- 使用通道,让子go干完活后通知主go。 +```go +func main() { + num := 10 + sign := make(chan struct{}, num) + + for i := 0; i < num; i++ { + go func() { + fmt.Println(i) + sign <- struct{}{} + }() + } + + // 办法1。 + //time.Sleep(time.Millisecond * 500) + + // 办法2。 + for j := 0; j < num; j++ { + <-sign + } + + //方法3 sync.WaitGroup 后面再讲 +} +``` +- 为啥用struct{}类型的通道? 因为空结构体(struct{}类型 值的表示法只有一个 struct{}{})其内存占用为0字节(这个值整个go程序永远只会存在一份)。 + + +**怎么让启动的多个gogroutine按照既定顺序运行?** +可以自己思考下怎么设计,用一个count参数去匹配每次进来的i,如果子go带着i进来发现不等于count(从0开始安全的累加),就会自旋判断。 等于就打印,然后count++,其他自旋的共享一个count会不断满足,然后依次输出。 +- 注意count的安全累加,毕竟会竞争。 + + +**runtime包中提供了哪些与模型三要素 G、P 和 M 相关的函数?** +- runtime.GOMAXPROCS 这个应该能控制P的数量 + + + + +### 17. if语句、for语句、switch语句 + +**for range时的注意事项** + +- range 一个数组或者切片的时候,会得到两个值,第一个是索引值i,第二个值是是存的值v,如果只接收一个默认是索引值。 +- 切片与数组是不同的,前者是引用类型的,而后者是值类型的。而又因为 + - range表达式只会在for语句开始执行时被求值一次,无论后边会有多少次迭代。 什么意思?看下面代码 + - range表达式的求值结果会被复制,也就是说,被迭代的对象是range表达式结果值的 +副本而不是原值。 +```go + numbers2 := [...]int{1, 2, 3, 4, 5, 6} + maxIndex2 := len(numbers2) - 1 + for i, e := range numbers2 { + if i == maxIndex2 { + numbers2[0] += e + } else { + //每次都会把相邻的两个值相加赋值给后面的数。 + numbers2[i+1] += e + } + } + fmt.Println(numbers2) +``` +打印出: 7 3 5 7 9 11 +- 第一次循环后num[1]等于1+2=3没问题 +- 按道理第二次循环 i=1, num[2] = num[2] + e(也就是n[1]) 应该等于3+3=6,实际却是5,那是因为 每次的e在最开始已经确定了,也就是说n[1]是固定的2, 所以这里是 3 + 2 = 5 +>被迭代的对象的第二个元素却没有任何改变,毕竟它与numbers2已经是毫不相关的 两个数组了。 + + +改成切片就可以不断更新值了。因为切片是引用类型。 + + +**switch语句中的switch表达式和case表达式之间有着怎样的联系?** +- case表达式的类型要跟switch的相同,不相同编译报错。 + - 如果case表达式中子表达式的结果值是无类型的常量,那么它的类型会被自动地转换为switch表达式的结果类型。 转换失败也无法通过编译。 + - 如果这些表达式的结果类型有某个接口类型,那么一定要小心检查它们的动态值是否 都具有可比性。 如果没有会抛异常。 +- 所有case表达式的子表达式的结果值不能重复。(只对于由字面量直接表示的子表达式而言,如果是索引值等间接的不要求,除了switch是判断类型的,比如 `switch x.(type)`) + + + + +**在if语句中,初始化子句声明的变量的作用域是什么?** +if后面的表达式以及大括号内的区域都是。 + + +### 18. 错误处理 + +- 在生成error类型值的时候用到了errors.New函数。 这是一种**最基本**的生成错误值的方式。 +- 我们调用它的时候**传入一个由字符串**代表的错误信息,它会给**返回**给我们一个**包含了这个错误信息的error类型值**。 + - 该值的静态类型当然是 error, + - 而动态类型则是一个在errors包中的,包级私有的类型*errorString。 +- 实际上,error类型值的 Error方法就相当于其他类型值的String方法。 +- fmt.Printf函数如果发现被打印的值是一个error类型的值,那么就会去调用它的Error方法。fmt包中的这类打印函数其实都是这么做的。 +- 当我们想通过模板化的方式生成错误信息,并得到错误值时,可以使用 `fmt.Errorf`函数。 + - 该函数会先调用fmt.Sprintf函数,得到确切的错误信息;再调用errors.New函数,得到包含该错误信息的error类型值。 + + +**怎样判断一个错误值具体代表的是哪一类错误?** +>error是一个接口类型,所以即使同为error类型的错误值,它们的实际类型也可能不同。 + +1. 对于类型在已知范围内的**一系列**错误值,一般使用**类型断言表达式**或类型switch语句来判断; +>只要类型不同,我们就可以如此分辨 +```go +//获取和返回已知的操作系统相关错误的潜在错误值 +func underlyingError(err error) error { + switch err := err.(type) { + case *os.PathError: + return err.Err + case *os.LinkError:... + case *os.SynscallError: + case *exec.Error: + } + return err +} +``` +2. 对于**已有相应变量**且**类型相同**的一系列错误值,一般直接使用**判等**操作来判断; +```go +switch err { + case os.ErrClosed: + fmt.Printf("error(closed)[%d]: %s\n", i, err) + case os.ErrInvalid: + fmt.Printf("error(invalid)[%d]: %s\n", i, err) + case os.ErrPermission: + fmt.Printf("error(permission)[%d]: %s\n", i, err) +} + +``` +3. 对于**没有**相应变量且类型未知的一系列错误值,只能**使用其错误信息的字符串**表示形式 +来做判断。 + + +**怎样根据实际情况给予恰当的错误值。** + + +- 构建错误值体系的基本方式有两种,即: + - 创建立体的错误类型体系 + - 创建扁平的错误值列表。 + + +**错误体系** +- error (Error()) + - net.Error (..,Timeout(), Temporary) + - *net.OpError + - *net.AddrError + - net.UnkonwNetworkError +>当net包的使用者拿到一个错误值的时候,可以先判断它是否是net.Error类型的,也就是说该值是否代表了一个网络相关的错误。 +>os包中的一些错误类型类似,它们也都有一个名为Err、类型为error接口类型的字段,代表的也是当前错误的潜在错误。 + +- 如果你不想让包外代码改动你返回的错误值的话,一定要小写其中字段的名称首字母。 +- 你可以通过暴露某些方法让包外代码有进一步获取错误信息的权限,比如编写一个 可以返回包级私有的err字段值的公开方法Err。 +- 由于error是接口类型,所以通过errors.New函数生成的错误值只能被赋给变量,而**不能赋给常量**,又由于这些代表错误的变量需要给包外代码使用,所以其**访问权限只能是公开的**。 + + +### 19. panic函数、recover函数以及defer语句 + +**怎么让 panic 包含一个值,以及应该让它包含什么样的值?** +- 将值作为参数传给该函数就可以,panic函数的唯一一个参数是**空接口**(也就是interface{})类型的,所以从语法上讲, 它可以**接受任何类型的值。** + - 最好传入error类型的错误值,或者其他的可以被有效序列化的值。 + - 有效序列化: 如果你觉得某个值有可能会被记到日志里,那么就应该为它关联String方法。如果 这个值是error类型的,那么让它的Error方法返回你为它定制的字符串表示形式就可以 了。 + + +**怎样施加应对 panic 的保护措施,从而避免程序崩溃?** +- recover函数无需任何参数,并且会返回一个空接口类型的值。 +- 这个值实际上就是即将恢复的 panic 包含的值。并且,如果这个 panic 是因我们调用panic函数而引发的,那么该值同时也会是我们此次调用panic函数时,传入的参数值副本。 + + +**用法不正确的情况** +- 在panic之后才定义recover(),运行不到。 +- 在同级panic之前定义也运行不到,因为会立即返回上一层。而且上一层也会立即返回上上一层。 +- 需要在defer后面放recover,因为defer是无论什么情况结束的程序,之后都会执行defer后面的函数(注意是后面的函数,不是表达式) + - **注意**:被延迟执行的是defer函数,而不是defer语句 +- 多条defer,按照入栈顺序,后入先出。 + + +## 模块三:Go语言实战应用 + +### 23. 测试的基本规则和流程 +>用测试找出自己的缺点,人是否会进步以及进步得有多快,依赖的恰恰就是对自我的否定,这包 括否定的深刻与否,以及否定自我的频率如何。这其实就是“不破不立”这个词表达的含 义。 + + +**分类**:都算单元测试 +- 功能测试: 用于测试程序的一些逻辑行为是否正确. +- 基准测试: 以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能.会多次运行基准测试函数以计算一个平均的执行时间 +- 示例测试: 以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。 + +>一个测试源码文件**只会针对于某个命令源码文件**,或库源码文件(以下简称被测源码文件)做测试,所以我们总会(并且应该)把它们**放在同一个代码包内**. + +go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。 + + +**命名** +- 测试文件:应该以被测源码文件的主名称为前导,并且必须以“_test”为后缀。 +- 测试函数: + - 对于功能测试函数来说,其名称必须以**Test**为前缀,并且参数列表中只应有一个 `*testing.T`类型的参数声明。 + - 对于性能测试函数来说,其名称必须以**Benchmark**为前缀,并且唯一参数的类型必须是 `*testing.B`类型的。 + - 对于示例测试函数来说,其名称必须以**Example**为前缀,但对函数的参数列表**没有强制规定**。 + + +go test命令执行的主要测试流程是什么? +- 准备工作: 确定内部需要用到的命令,检 查我们指定的代码包或源码文件的有效性,以及判断我们给予的标记是否合法。 +- 然后针对每个测试包**依次**进行(串行这些动作): + - 构建 + - 执行包中符合要求的测试函数 + - 清理临时文件 + - 打印测试结果 +>但是,为了加快测试速度,它通常会**并发地对多个被测代码包**进行功能测. 只不过在最后打印测试结果时候,它会按照给定的顺序逐个进行,虽然才会让我们感知到是串行。 + + +**性能测试特殊之处** +- 由于并发的测试会让**性能测试**的结果存在偏差,所以性能测试一般都是串行进行 的。更具体地说,只有在所有构建步骤都做完之后,go test命令才会真正地开始进行性 能测试。 +- 下一个代码包性能测试的进行,总会等到上一个代码包性能测试的结果打印完成才会开始,而且性能测试函数的执行也都会是串行的。 + >这也就解释了为啥简单性能测试也比很多功能测试慢。 + + +>多次执行一样的测试,可能会使用缓存,直接返回测试结果。 go clean -cache 手动清理缓存。运行go clean -testcache将会删除所有 的测试结果缓存。设置值为gocacheverify=1将会导致 go 命令绕过任何的缓存数据。 + + +- 当我们在其中调用t.Fail方法时,虽然当前的测试函数会继续执行 下去,但是结果会显示该测试失败. +- 对于失败测试的结果,go test命令并不会进行缓存 + + +**更多测试手法** + + +>涉及testing包中更多的 API、go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析。 + + +- `-cpu`标记正是用于设置P的最大个数的。最大 P 数量就代表着 Go 语言运行时系统同时运行 goroutine 的能力,也可以被视为其中逻辑 CPU 的最大个数。 + - 在默认情况下,最大 P 数量就等于当前计算机 CPU 核心的实际数量。设置的最大 P 数量,最好不要超过当前计算机 CPU 核心的实际数量。 + - 标记-cpu的值应该是一个正整数的列表,英文逗号分隔。 + - 针对于此值中的每一个正整数,go test命令都会先设置最大P数量为**该数**,然后再执行测试函数。 列表有多个值就会执行多轮测试函数。 + - 如果该命令发现我们并没有追加这个标记,那么就会让逻辑CPU切片只包含一个元素值, 即最大 P 数量的默认值,也就是当前计算机 CPU 核心的实际数量。 + + +- `-count`标记是专门用于重复执行测试函数的。它的值必须大于或等于0,并且默认值为1。 +- **性能**测试函数的**执行次数** = -cpu的正整数 * -count表示的数 * 探索式执行中的函数执行的总次数 +>探索式执行,是指在测试函数的执行时间上限不变的前提下,尝试找到被测程序的最大执行次数。每次执行都会修改b.N的最大值,直到超过上限。 +- 功能测试的执行次数 = -cpu的正整数 * -count标记的值。 + + +在对 Go 程序执行某种自动化测试的过程中,测试日志会显得特别多,而且好多都是重复的。 +- 功能测试一般一次就行。 + + +- `-parallel`:设置同一个被测代码包中的功能测试函数的**最大并发执行数**。 + - 该标记的默认值是测试运行时的最大 P 数量(通过调用表达式runtime.GOMAXPROCS(0)获得)。 + - 默认情况下,对于同一个被测代码中的多个功能测试函数,命令会串行地执行它们。除非我们**在一些功能测试函数中显式地调用t.Parallel方法**。这个时候,这些包含了t.Parallel方法调用的功能测试函数就会被go test命令并发地执行,而并发执行的最大数量正是由-parallel标记值决定的 + - 同一个功能测试函数的多次执行之间一定是串行的。 + + +**性能测试函数中的计时器有什么作用?** +`testing.B`类型有这么几个指针方法: +- StartTimer、StopTimer 和 ResetTimer。 + - 这些方法都是用于操作当前的性能测试函数专属的计时器的。 + - 就是testing.B类型的一些字段,这些字段用于记录:当前测试函数在当次执行过程中耗费的时间、分配的堆内存的字节数以及分配次数。 + - 之前不断尝试找到最大执行次数的逻辑也用到这个计时器,所以如果我们在测试函数中自行操作这个计时器,就一定会影响到这个探索式执行的结果。就是说,这会让命令找到被测程序的最大执行次数有所不同。 +- 用法: 在性能测试函数中,我们可以通过对b.StartTimer和b.StopTimer方法的联合运用,再去除掉任何一段代码的执行时间。 +- 实例:下面代码就是通过组合使用,去掉了一个比较耗时但是不属于要测试函数的逻辑: +```go +func BenchmarkGetPrimes(b *testing.B) { + b.StopTimer() + time.Sleep(time.Millisecond * 500) //模拟某个耗时但与被测程序关系不大的操作 + max := 10000 + b.StartTimer() + + for i := 0; i < b.N; i++ { + GetPrimes(max) + } +} +``` +- b.ResetTimer方法只能用于:去除在调用它之前那些代码的执行时间。不过,无论在调用它的时候,计时器是不是正在运行,它都可以起作用。 + + +- benchmem标记和-benchtime标记的作用分别是什么? + - -benchmem 输出基准测试的内存分配统计信息。 + - -benchtime 用于指定基准测试的探索式测试执行时间上限 + + +- 怎样在测试的时候开启测试覆盖度分析?如果开启,会有什么副作用吗? + + +### 26. sync.Mutex 和 sync.RWMutex + +**竞态条件、临界区与同步工具** + + +>相比于 Go 语言宣扬的“用通讯的方式共享数据”,通过共享数据的方式来传递信息和协调 线程运行的做法其实更加主流 +- 竞态条件: 一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。 +- 共享数据一致性: 代表着某种约定,即:多个线程对共享数据的操作总是可以达到它们各自预期的效果。 +- **同步的用途**有两个: + - 一个是避免多个线程在同一时刻操作同一个数据块 + - 另一个是协调多个线程,以避免它们在同一时刻执行同一个代码块。 +- 临界区: 只要一个代 码片段需要实现对共享资源的串行化访问,就可以被视为一个临界区(critical section)。 进入临界区需要获取令牌以便一致的访问共享资源。 + - 总是需要保护的,否则就会产生竞态条件。 而施加保护的重要手段之一就是使用实现了某种同步机制的工具,也叫同步工具。 + + +而go语言中最重要且最常用的**同步工具**当属**互斥量**(mutual exclusion,简称 mutex),也叫互斥锁。 +- 被用来保护一个临界区或者一组相关临界区,在同一时刻只有一个goroutine处于该临界区之内。 通过加锁解锁进行控制。 + - mu.Lock()、m.Unlock() +- **我们使用互斥锁时有哪些注意事项?** + - 不要重复锁定 + - 避免让一个互斥锁保护多个临界区,那容易出现死锁,混乱竞争,让程序变慢 + - 对一个已经被锁定的互斥锁进行锁定,是会立即阻塞当前的 goroutine 的。这个 goroutine 所执行的流程,会一直停滞在调用该互斥锁的Lock方法的那行代码上。直到该互斥锁的Unlock方法被调用,并且这里的锁定操作成功完成,后续的代码(也就是 临界区中的代码)才会开始执行。 + - 死锁:go语言系统出现死锁会报错:发现所有的用户级 goroutine 都处于 等待状态,就会自行抛出一个带有如下信息的 panic: `fatal error: all goroutines are asleep - deadlock!` + >这种由 Go 语言运行时系统自行抛出的 panic 都属于致命错误,都是无法被恢复,调用recover函数对它们起不到任何作用。也就是说,一旦产生死锁,程序必然崩溃。 + + - 不要忘记解锁,必要时用defer语句. + - defer语句应该紧跟在锁定操作之后 + - 忘记解锁很可能导致重复加锁。 + - 还会使其他goroutine无法进入该互斥锁保护的临界区。这会导致程序功能失效甚至死锁或者程序崩溃。 + - 不要对尚未解锁或者已经解锁的互斥锁做解锁操作 + - 解锁未锁定的互斥锁会立即引发 panic,也是无法恢复的异常。 + - 不要在多个函数之间传递互斥锁 + - 一旦我们声明了一个sync.Mutex类型的变量,就可以直接使用它了。 该类型是结构体,属于值类型的一种。 传递给函数或者从函数返回、赋给其他变量、让它进入通道等操作都会导致它的副本的产生 + - 其多个副本是完全独立的,是不同的互斥锁。如果你把一个互斥锁作为参数值传给了一个函数,那么在这个函数中对传入的锁的所有操作,都不会对存在于该函数之外的那个原锁产生任何的影响。 + + +**读写锁** +- sync.RWMutex类型中的**Lock方法 和Unlock方法**分别用于对**写锁**进行锁定和解锁,而它的RLock方法和RUnlock方法则分别 用于对**读锁**进行锁定和解锁。 +- 写写、读写都互斥,读读可以同时。 +- 对写锁进行解锁,会唤醒“所有因试图锁定读锁,而被阻塞的 goroutine”,并且,这通常会使它们都成功完成对读锁的锁定。 +- 是互斥锁的一种扩展,实现更细腻的控制。 解锁未被锁定的写锁或者毒素哦都会抛异常。 + + +### 条件变量sync.Cond +- 实际上,条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。 +- 它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。 +- 条件变量提供的方法有三个:等待通知(wait)、单发通知(signal)和广播通知 (broadcast)。 +- sync.Cond类型并不是开箱即用的。我 们只能利用sync.NewCond函数创建它的指针值。这个函数需要一个sync.Locker类型的 参数值。 +```go +var mailbox uint8 +var lock sync.RWMutex +sendCond := sunc.NewCond(&lock) +recvCond := sync.NewCond(lock.RLocker()) + +// sign 用于传递演示完成的信号。 + sign := make(chan struct{}, 3) + max := 5 + go func(max int) { // 用于发信。 + defer func() { + sign <- struct{}{} + }() + for i := 1; i <= max; i++ { + time.Sleep(time.Millisecond * 500) + lock.Lock() + for mailbox == 1 { + sendCond.Wait() + } + log.Printf("sender [%d]: the mailbox is empty.", i) + mailbox = 1 + log.Printf("sender [%d]: the letter has been sent.", i) + lock.Unlock() + recvCond.Signal() + } + }(max) + + go func(max int) { // 用于收信。 + defer func() { + sign <- struct{}{} + }() + for j := 1; j <= max; j++ { + time.Sleep(time.Millisecond * 500) + lock.RLock() + for mailbox == 0 { + recvCond.Wait() + } + log.Printf("receiver [%d]: the mailbox is full.", j) + mailbox = 0 + log.Printf("receiver [%d]: the letter has been received.", j) + lock.RUnlock() + sendCond.Signal() + } + }(max) + + <-sign + <-sign +``` +- 条件变量的**Wait方法做了什么**? Wait方法主要做了四件事情 + - 把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中(队尾)。 + - 解锁当前的条件变量基于的那个互斥锁(就是for循环外面lock起来那个) + - 让当前的goroutine处于等待状态,等到通知到来时再决定是否唤醒它。(从对首开始)。此时,这个goroutine就会阻塞在这行代码上。 + - 如果通知到来,并且觉得唤醒这个goroutine,那么就会重新锁定之前解开的那个锁。 然后代码就可以继续往后走了。 +- **为什么先要锁定条件变量基于**的互斥锁,才能调用它的Wait方法? + - 阻塞前会先解锁(第二步),而如果不先锁定,那这里会抛出panic。 那为啥还要锁定呢? 不对,那为啥要用wait? + - 如果不解锁,这个wait阻塞了,那只能由外部go进行解锁,这是不稳定不安全的。 (吐槽:那为啥要先锁定? 这用结论去做论据是真的骚。。。) +- 为什么要用for语句来包裹调用其Wait方法的表达式,用if语句不行吗? + - 如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的 状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。 比如 + - 多个go等待共享资源的同一种状态,每次只能成功一个,而醒来发现资源被其他用掉了,那这个时候就需要再继续等待通知。 + - 共享资源的状态可能多个,而通知是一样的,但条件可能不一定是自己想要的,这就有必要继续等待和检查 + - 多cpu核心的系统中,即使没有收到条件变量的通知, 调用其Wait方法的 goroutine 也是有可能被唤醒的。 +- 条件变量的Signal方法和Broadcast方法有哪些异同? + - Signal只会唤醒一个等待的goroutine,后者是所有的。 + - Wait方法总会把当前的go添加到通知队列的队尾,而Signal方法总会从队首开始查找被唤醒的go。 + - 除非你确定只有一个go在等待通知,或者只需要唤醒一个go就可以满足需求,不然就用广播。 + - 条件变量的Signal方法和Broadcast方法**并不需要在互斥锁的保护下执行**。恰恰相反,我们最好在解锁条件变量基于的那个互斥锁之后, 再去调用它的这两个方法。这更有利于程序的运行效率。 + + +>件变量主要是用于协调想要访问共享资源的那些线程。 + + +### 29. 原子操作 +为了公平起见,调度器总是会频繁地换上或换下这些 goroutine。 +这个**中断的时机**有很多,任何两条语句执行的间隙,甚至在某条语句执行的过程中都是可以的。**即使这些语句在临界区之内也是如此**。 +>互斥锁虽然可以保证临界区中代码的 串行执行,但却不能保证这些代码执行的原子性(atomicity) + + +原子操作在进行的过程中是不允许中断的。 +在底层,这会由 CPU 提供芯片级别的支持,所以绝对有效。即使在拥有多 CPU 核心,或者多 CPU 的计算机系统中,原子操作的保证也 是不可撼动的。 + + +正是因为原子操作不能被中断,所以它**需要足够简单,并且要求快速**。 +因此,操作系统层面只对针对二进制位或整数的原子操作提供了支持。 go语言只针对少数数据类型的值提供了原子操作函数。这些函数都存在于标准库代码包`sync/atomic`中。 + + +**sync/atomic包中提供了几种原子操作?可操作的数据类型又有哪?** +- **原子操作**有: 加法(add)、比较并交换(compare and swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。 +- **数据类型**有:int32、int64、uint32、uint64、uintptr, 以及unsafe包中的Pointer。不过,针对unsafe.Pointer类型,该包并**未提供进行原子加法操作**的函数。 +>此外,sync/atomic包还提供了一个名为Value的类型,它可以被用来存储任意类型的值。 + + +- 为啥这些原子操作需要的参数类型都是支持数据类型的指针? +- 原子操作函数需要的是被操作值的指针,而不是这个值本身. + + +**比较并交换操作与交换操作相比有什么不同?优势在 哪里?** +- 比较并交换操作即 CAS 操作,是有条件的交换操作,**只有在条件满足的情况下**才会进行值的交换。 +- 使用CAS是乐观锁,其假设共享资源状态的改变并不频繁,或者,它的状态总会变成期望的那样。 +>如果保证了写操作是原子操作,为了安全读操作也有必要使用原子操作,完全的保护基本上与不保护没有什么区别。 + + +什么时候使用原子操作? +>只涉及并发地读写单一的整数类型值,或者多个互不相关的整数类型值,那就不要再考虑互斥锁了,而是优先用原子操作。 + + +**怎样用好sync/atomic.Value?** + + +- 此类型相当于一个容器,可以被用来“原子地” **存储**和**加载**任意的值。 +- 它只有两个指针方法:Store和Load +- 一旦atomic.Value类型的值(以下简称原子值)被真正使用,它就不应该再 被复制了。 +- 第一条规则,不能用原子值存储 nil。 动态值是nil而动态类型不是nil的接口是可以存储的。 +- 第二条原则,我们向原子值存储的第一个值,决定了它今后能且只能存储哪一个类型的值。 +- 我们无法通过某种方法获知一个原子值是否已经被真正使用,也不知道其实际类型,所以使用时要注意: + - 不要把内部使用的原子值暴露给外界 + - 如果需要让包外使用,可以通过公开函数让外界间接使用。 + - 存储值前需要判定值是否合法 + - 如果可以,尽量把原子值封装在一个数据类型中,比如结构体类型。 这 样,我们既可以通过该类型的方法更加安全地存储值,又可以在该类型中包含可存储值 的合法类型信息。 + - 尽量不要向原子值中存储引用类型的值,容易造成安全漏洞 + + +### 31. sync.WaitGroup和sync.Once +使用通道作为同步工具,需要声明一个通道,让其他的 goroutine 在运行结束之前,都向这个通道发送一个元 素值,并且,让主 goroutine 在最后从这个通道中接收元素值,接收的次数需要与其他的 goroutine 的数量相同。 + + +**这样太丑了**。 可以使用WaitGroup进行替代,它比通道更加适合实现这种一对多的 goroutine 协作流程。 + +- WaitGroup类型拥有三个指针方法:Add、Done和Wait。 +- Add(-3)、Done()表示-1(可以在需要等待的goroutine中,通过defer语句调用它。 无论成功失败都会done) +- Wait方法的功能是,阻塞当前的 goroutine,直到其所属值中的计数器归零。 + >如果在该方法被调用的时候,那个计数器的值就是0,那么它将不会做任何事情。 + +```go + func coordinateWithWaitGroup() { + var wg sync.WaitGroup + wg.Add(2) + num := int32(0) + fmt.Printf("The number: %d [with sync.WaitGroup]\n", num) + max := int32(10) + go addNum(&num, 3, max, wg.Done) + go addNum(&num, 4, max, wg.Done) + wg.Wait() + } +``` +- sync.WaitGroup类型值中计数器的值可以小于0吗? **不可以**,但是参数可以传入负数。 +- 如果一个此类值的Wait方法在它的某个计数周期中被调用,那么就会立 即阻塞当前的 goroutine,直至这个计数周期完成。 也就是说wait方法不能跨越两个计数周期 +- 不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行。换句话说,要杜绝对同一个WaitGroup值的两种操作的并发执行。 + + +**Once** +- Once类型的Do方法只接受一个参数,这个参数的类型必须是func(),即:无参数声明和 结果声明的函数。 +- 只执行“首次被调用时传入的”那个函数,并且之后不会再执行任何参数函数。 +- done字段 + - done的uint32类型的字段。它的作用是记录其所属值的Do方法 被调用的次数。不过,该字段的值只可能是0或者1。一旦Do方法的首次调用完成,它的值 就会从0变为1。 + - 对它的操作必须是原子的。 + - 而且对其操作前,会进行双重判断 + - Do方法在一开始就会通过调用 atomic.LoadUint32函数来获取该字段的值,并且一旦发现该值为1,就会直接返回。这 也初步保证了“Do方法,只会执行首次被调用时传入的函数”。 + - 在这个条件判断之后,Do方法会立即锁定其所属值中的那个sync.Mutex类型的字段m。 + - 然后,它会在临界区中再次检查done字段的值,并且仅在条件满足时,才会去调用参数函数,以及用原子操作把done的值变为1。 +- 第一个特点,由于Do方法只会在参数函数执行结束之后把done字段的值变为1,因此,如 果参数函数的执行需要很长时间或者根本就不会结束(比如执行一些守护任务),那么就有 可能会导致相关 goroutine 的同时阻塞。 +- 第二个特点,Do方法在参数函数执行结束后,对done字段的赋值用的是原子操作,并且, 这一操作是被挂在defer语句中的。因此,不论参数函数的执行会以怎样的方式结束,done字段的值都会变为1。 + - 即使这个参数函数没有执行成功(比如引发了一个 panic),我们也无法使用同 一个Once值重新执行它了。 + - 所以,如果你需要为参数函数的执行设定重试机制,那么就要 考虑Once值的适时替换问题。 + + +### 32. context.Context类型 +在使用WaitGroup值的时候,我们最好用“先统一Add,再并发Done,最后Wait”的标 准模式来构建协作流程。如果在调用wait方法的同时,为了增大其计数器的值,并发地调用add方法,可能会引发panic,而我们如果不能在一开始就确定执行子任务的go数量,那waitgroup就不是很适应这种场景。 + + +- 一个解决方案是:**分批地启用**执行子任务的 goroutine。 + +```go + +``` + + +**怎样使用context包中的程序实体,实现一对多的 goroutine 协作流程?** +```go +func coordinateWithContext() { + total := 12 + var num int32 + fmt.Printf("The number: %d [with context.Context]\n", num) + cxt, cancelFunc := context.WithCancel(context.Background()) + for i := 1; i <= total; i++ { + go addNum(&num, i, func() { + //如果所有的addNum函数都执行完毕,那么就立即通知分发子任务的 goroutine。 + if atomic.LoadInt32(&num) == int32(total) { + cancelFunc() + } + }) + } + //试图针对该函数返回的通 道,进行接收操作。 通道没有值就会阻塞在这里 + //一旦cancelFunc函数被调用,针对该通道的接收操作就会马上结束 + <-cxt.Done() + fmt.Println("End.") +} +``` +- context.Background() 生成一个根上下文 +>所有的Context值共同构成了一颗代表了上下文全貌的树形结构。这棵树的 树根(或者称上下文根节点)是一个已经在context包中预定义好的Context值,它是全 +局唯一的。通过调用context.Background函数,我们就可以获取到它 + +- context.WithCancel(contextXX) 返回两个值 + - cxt 可撤销的Context类型的值 + - cancelFunc context.CancelFunc类型的撤销函数 + + +**Context类型** + +- 它是一种非常通 用的同步工具。它的值不但可以被任意地扩散,而且还可以被用来传递额外的信息和信号。 +- Context类型可以提供一类代表上下文的值**。此类值是并发安全的**,也就是 说它可以被传播给多个 goroutine。 +- Context类型的值是可以繁衍的,其四个方法都可以返回一个新的子Context值: WithCancel、 WithDeadline、WithTimeout和WithValue。 + - 这些函数的第一个参数的类型都是context.Context,而名称都为parent。 + - 这些子值可以携带其父值的属性和数据,也可以响应我们通过其父值传达的信号。 + + +**四个函数** + +- WithCancel函数用于产生一个可撤销的parent的子值: 可以尝试一个触发撤销信号的函数 +- WithDeadline和WithTimeout函数 用于产生一个会定时撤销的子值。 同上,也可以主动撤销,就是多了个把内置的计时器释放掉 +- WithValue函数 用于产生一个会携带额外数据的子值 + + +**接口有关“撤销”的两个方法** + +- 这个接口中有两个方法与“撤销”息息相关。 +- Done方法会返回一个元素类型为struct{}的接收通道。不过,这个接收通道的用途并不是传递元素值,而是**让调用方去感知“撤销”当前Context值的那个信号**。 +- Err方法 可以**得到撤销的具体原因**。 + - 该方法的结果是error类型的, + - 并且其值只可能等于context.Canceled变量的值,或者 context.DeadlineExceeded变量的值。前者用于表示手动撤销,而后者则代表:由于我们给定的过期时间已到,而导致的撤销。 +- 撤销后通过Done感知到,后面做啥自由发挥,context没有任何约束。 +- 若再深究的话,这里的“撤销”最原始的含义其实就是,终止程序针对某种请求(比 如 HTTP 请求)的响应,或者取消对某种指令(比如 SQL 指令)的处理。 + + + +**撤销信号是如何在上下文树中传播的?** + +在撤销函数被调用之后,对应的Context值会先关闭它内部的接收通道,也就是它的Done 方法会返回的那个通道。 +- 然后,它会向它的所有子值(或者说子节点)传达撤销信号。 +- 这些子值会如法炮制,把撤销 信号继续传播下去。 +- 最后,这个Context值会断开它与其父值之间的关联。 + +>撤销后的context会把信号传给下面的子值,然后断开与父值的关联 + +- 通过调用context.**WithValue函数得到的Context值是不可撤销的**。撤销信号在被传播时,若遇到它们则会直接跨过,并试图将信号直接传给它们的子值。 +>那这个树是不是就断了? 这个value节点就成为独立的了?还是会销毁? + + +**怎样通过Context值携带数据?怎样从中获取数据?** +- WithValue函数在产生新的Context值(以下简称含数据的Context值)的时候需要三个 参数,即:**父值、键和值**。 + - 键的类型必须是可判等的 +- Value方法是用来获取数据的: + - 它会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其**父值中继续查找**。 + - Context值的Value方法在沿路查找的时候,会直接跨过那可撤销的那三种context值。 + - 如果我们调用的Value方法的所属值本身就是不含数据的,那么实际调用的就将会是其父辈 或祖辈的Value方法。 + >也就是说在可撤销的context值上也可以调用value方法? + - Context接口并没有提供改变数据的方法。 + + +### 33. 临时对象池sync.Pool + +**临时对象**:不需要持久使用的某一类值。 +- 这类值对于程序来说可有可无,但如果有的话会明显更好。 +- 它们的创建和销毁可以在任何时候发生,并且完全不会影响到程序的功能。 +- 它们也应该是无需被区分的,其中的任何一个值都可以代替另一个 + +**可以把临时对象池当作针对某种数据的缓存来用** + +```go +var ppFree = sync.Pool{ + New: func() interface{} { return new(pp)} +} +``` +- 两个方法: Put和Get + - **Put** 向池中存放临时对象 + - **Get** 从池中获取临时对象 +- 一个公开字段**New**:代表着创建临时对象的函数,最好在初始化这个池的时候给定它。 + - Get方法如果到了最后,仍然无法获取到一 个值,那么就会调用该函数。 + - 该函数的结果值并不会被存入当前的临时对象池中,而是直接返回给Get方法的调用方。 +- 每个值都应该是独立的、平等的和可重用的。 +- 一个多层的数据结构支撑着对临时对象的存储。 + - 顶层是本地池列表: 包含了与某个P对应的那些本地池,并且长度与P数量相同 + - 每个本地池:包含一个私有的临时对象(只能被其所对应的P关联的那个go代码访问到)和一个共享的临时对象列表 + + + +**什么时候清理?** + +Go 语言运行时系统中的垃圾回收器,在每次开始执行之前,都会对所有已创建的临时对象池中的值进行全面地清除。 +- **注册池清理函数**: sync包在被初始化的时候,会**向 Go 语言运行时系统注册一个函数**,这个函数的功能就是: 清除所有已创建的临时对象池中的值。 +- **池汇总列表**: 在sync包中还有一个包级私有的全局变量。这个变量代表了当前的程序中使用的**所有临时对象池的汇总**,它是元素类型为`*sync.Pool`的切片。 + - 在一个临时对象池的Put方法或Get方法第一次被调用的时候,这个池就会被添加到池汇总列表中。 所以池清理函数总能访问到所有正在被真正使用的临时对象池。 +- 池清理函数会遍历池汇总列表。对于其中的每一个临时对象池,它都会先将池中所有的私有临时对象和共享临时对象列表都置为nil,然后再把这个池中的所有本地池列 表都销毁掉。 +- 最后,池清理函数会把池汇总列表重置为空的切片。 + - 如果临时对象池以外的代码再无对它们的引用,那么在稍后的垃圾回收过程中,这些临时对象就会被当作垃圾销毁掉,它们占用的内存空间也会被回收以备他用。 + + +- 每个本地池都对应着一个P。 一个临时对象池的Put方法或Get方法会获取到哪一个本地池,完全取决于调用它的代码所在的 goroutine 关联的那个 P。 +- 临时对象池的Get方法,总会先试图从对应的本地池的private字段处获取一个 临时对象。只有当这个private字段的值为nil时,它才会去访问本地池的shared字段。 +- 一个本地池的shared字段原则上可以被任何 goroutine 中的代码访问到,不论这个 goroutine 关联的是哪一个 P。 + + +**用在什么时候?** +- 它存储的临时 对象都应该是拥有较长生命周期的值,并且,这些值不应该被某个 goroutine 中的代码长期的持有和使用。 +- 临时对象池非常适合用作针对某种数据的缓存。 +- 从某种角度讲,临时对象池可以帮助程序实现可伸缩性,这也正是它的最大价值。 + + +### 34-35 并发安全字典sync.Map + +- 与单纯使用原生map和互斥锁的方案相比,使用sync.Map可以显著地减少锁 的争用。 +- 它所有的方法涉及的键和值的类型都是interface{},也就是空接口,这意味着可以包罗万象。所以,我们必须在程序中自行保证它的键类型和值类型的正确性。 + + +**并发安全字典对键的类型有要求吗?** + +- 键的实际类型不能是函数类型、字典类型和切片类型。 +- 我们应该在每次操作并发安全字典的时候,都去显式地检查键值的实际类型。无论是存、取还是删,都应该如此。 + - 可以先通过调用reflect.TypeOf函数得到一个键值对应的反射类型值(即: reflect.Type类型的值),然后再调用这个值的Comparable方法,得到确切的判断结果。 + + +**怎样保证并发安全字典中的键和值的类型正确性?** +- **(方案一)** + - 如果我们可以完全确定键和值的具体类型的情况。 + - 在这种情况下,我们可以利用 Go 语言编译器去做类型检查,并用类型断言表达式作为辅助,比如把并发安全字典封装在一个结构体类型里面。 +- **方案二** + - 不确定具体类型 + - 可以封装的结构体类型的所有方法,都可以与sync.Map类型的方法完全一致(包括方法名称和方法签名)。 + - 添加一些做类型检查的代码 + - 并发安全字典的键类型和值类型,必须在初始化的时候就完全确定。 + - 保证键类型是可比较的。 +```go +type ConcurrentMap struct { + m sync.Map + keyType reflect.Type + valueType reflect.Type +} + +func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool){ + if reflect.TypeOf != cMap.keyType { + return + } + return cMap.m.Load(key) +} + +func (cMap *ConcurrentMap) Store(key, value interface{}) { + if reflect.TypeOf(key) != cMap.keyType { + panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key))) + } + if reflect.TypeOf(value) != cMap.valueType { + panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value))) + } + cMap.m.Store(key, value) + } + +``` +- 这里ConcurrentMap类型代表的是:可自定义键类型和值类型的并发安全字典。 +- 这个类型 同样有一个sync.Map类型的字段m,代表着其内部使用的并发安全字典。 +- 它的字段keyType和valueType,分别用于保存键类型和值类型。 + - 这两个字段类型是反射类型,可以代表Go语言的任何数据类型。 + - 获取这个类型的值方式是调用reflect.TypeOf函数并传入某个样本值即可。比如reflect.TypeOf(int(123))的结果值,就代表了int类型的反射类型值。 +- Load方法 + - 当我们根据 ConcurrentMap 在m字段的值中查找键值对的时候,就必须保证 ConcurrentMap 的类型是正确的。 + - 由于**反射类型值之间可以直接使用操作符==或!=进行判等**,所以这里的类型检查代码非常简单。 + - 把一个接口类型值传入reflect.TypeOf函数,就可以得到与这个值的实际类型对应 的反射类型值 + - Load方法的第一个结果value的值为nil,而第二个结果ok的值为false。这完全 符合Load方法原本的含义。 +- Store方法 + - Store方法接受两个参数key和value,它们的类型也都是 interface{}。 + - 当参数key或value的实际类型不符合要求时,Store方法会立即引发 panic。 + - 这主要是由于Store方法没有结果声明,所以在参数值有问题的时候,它无法通过比较平和 的方式告知调用方。不过,这也是符合Store方法的原本含义的 + +>第二种方案中,我们无需在程序运行之前就明确键和值的类型,只要在初始化并发安全字 典的时候,动态地给定它们就可以了 + + +**并发安全字典如何做到尽量避免使用锁?** + + +- sync.Map类型在内部使用了大量的原子操作来存取键和值,并使用了两个原生的map作为存储介质。 + - 其中一个原生map被存在了sync.Map的read字段中,该字段是sync/atomic.Value类型的。 + - 这个原生字典可以被看作一个快照,它总会在条件满足时,去重新保存所属的 sync.Map值中包含的所有键值对。 + - 只读字典虽然不会增减其中的键,但却允许变更其中的键所对应的值。所以,它并不是传统意义上的快照,它的只读特性只是对于其中键的集合而言的。 + - **sync.Map在替换只读字典的时候根本用不着锁**。 因为,它先把值转换为了unsafe.Pointer类型的值,然后再把后者封装,并储存在其中的原生 字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子操作了。 + - sync.Map中的另一个原生字典由它的dirty字段代表。 + - 脏字典和只读字典如果都存有同一个键值对,那么这里的两个键指的肯定是同一个基本值,对于两个值来说也是如此。 + - 这两个字典在存储键和值的时候都只会存入它们的某个指针,而不是基本值。 + + +- sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。 + - 只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。 + - sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对 未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。 +- 只读字典和脏字典之间是会互相转换的。在脏字典中查 找键值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read 字段中,然后把代表脏字典的dirty字段的值置为nil。 + + + +>在读操作有很多但写操作却很少的情况下,并发安全字典的性能往往会更好。 +>在几个写操作当中,新增键值对的操作对并发安全字典的性能影响是最大的,其次是删除操作,最后才是修改操作。 + + +### 36. unicode与字符编码 + +- 当一个string类型的值被转换为[]rune类型值的时候,其中的字符串会被拆分成一个一个的 Unicode 字符。 +- Go 语言的所有源代码,都必须按照 Unicode 编码规范中 的 UTF-8 编码格式进行编码。换句话说,**Go 语言的源码文件必须使用 UTF-8 编码格式进行存储。** +- **ASCII** 是英文“American Standard Code for Information Interchange”的缩写,中文 译为美国信息交换标准代码。 +- **Unicode** 编码规范通常使用十六进制表示法来表示 Unicode 代码点的整数值,并使 用“U+”作为前缀。比如,英文字母字符“a”的 Unicode 代码点是 U+0061。 + + +- Unicode 编码规范提供了三种不同的编码格式,即:UTF-8、UTF-16 和 UTF-32。 + - 其中的 **UTF** + - 是UCS Transformation Format 的缩写。而 **UCS** + - 又是 Universal Character Set 的 缩写,但也可以代表 Unicode Character Set。 + - 所以,UTF 也可以被翻译为 Unicode 转换格式。它代表的是字符与字节序列之间的转换方式。 +- “-”右边的整数的含义是,以多少个比特位作为一个编码单 元。以 UTF-8 为例,它会以 8 个比特,也就是一个字节,作为一个编码单元。 + + +**一个string类型的值在底层是怎样被表达的?** + +在底层,一个string类型的值是由**一系列**相对应的 Unicode 代码点的**UTF-8编码值**来表达的。 +- 一个string类型的值既可以被拆分为一个包含多个**字符**的序列。 + - 它是由一个以rune为元素类型的切片来表示。 + - rune是 Go 语言特有的一个基本数据类型,它的一个值就代表一个字符,即:一个 Unicode 字符。 +- 也可以被拆分为一个包含多个**字节**的序列。 + - 它是由一个以byte为元素类型 的切片代表。 + +- UTF-8 编码方案会把一个 Unicode 字符编码为一个长度在 [1, 4] 范围内的字节序列。 而type rune = int32, 一个rune类型的值会由四个字节宽度的空间来存储,正好够存下一个UTF-8编码值。 + + +对于一个多字节的 UTF-8 编码值来说,我们可以把它当做一个整体转换为单一的整数,也可以先把它拆成字节序列,再把每个字节分别转换为一个整数,从而得到多个整数。 +这两种表示法展现出来的内容往往会很不一样。比如, +- 对于中文字符'爱'来说,它的 UTF- 8 编码值可以展现为单一的整数7231,//fmt.Printf(" => runes(hex): %x\n", []rune(str)) +- 也可以展现为三个整数,即:e7、88和b1。 //fmt.Printf(" => bytes(hex): [% x]\n", []byte(str)) + + +**使用带有range子句的for语句遍历字符串值的时候应该注意什么?** + +1. 带有range子句的for语句会先把被遍历的字符串值**拆成一个字节序列**, +2. 然后再试图找出这个字节序列中包含的每一个 UTF-8 编码值,或者说每一个 Unicode 字符。 +3. for语句可以为两个迭代变量赋值。如果存在两个迭代变量 + 1. 那么赋给第一个变量的值,就将会是**当前字节序列**中的某个 **UTF-8 编码值的第一个字节所对应的那个索引值**。 + 2. 赋给第二个变量的值,则是这个 UTF-8 编码值代表的那个 Unicode 字符,其类型会是rune。 + +```go +str := "Go爱好者" +for i, c := range str { + fmt.Printf("%d: %q [% x]\n", i, c, []byte(string(c))) +} + +完整打印如下: +0: 'G' [47] +1: 'o' [6f] +2: '爱' [e7 88 b1] +5: '好' [e5 a5 bd] +8: '者' [e8 80 85] +``` + +- 注意打印结果的第三行,**索引值不是3**,而是5,为什么? +>因为'爱'是由三个字节共同表达的,所以第四个 Unicode 字符'好'对 应的索引值并不是3,而是2加3后得到的5。 +>len函数对于字符串,得到的是字节长度. + +为啥按照一个字节一个索引? +>一个string类型的值在底层就是一个能够表达若干个 UTF-8 编码值 的字节序列。 + +- for语句可以逐一地迭代出字符串值里的每个 Unicode 字符。 + - 但是,相邻的 Unicode 字符的索引值并不一定是连续的。这取决于前一个 Unicode 字符是 否为单字节字符。 + - for range的时候,迭代出首字节下标和rune,首字符下标可能跳跃 +- 如果我们想得到其中某个 Unicode 字符对应的 UTF-8 **编码值的宽度**,就可以**用下一个字符的索引值减去当前字符的索引值**。 + +### 37. strings包与字符串操作 + +**与string值相 比,strings.Builder类型的值有哪些优势?** +- 已存在的内容不可变,但可以拼接更多内容 +- 减少了内存分配和内容拷贝的次数 +- 可将内容重置,可重用值 + + +**string类型的值是不可变的**。 +如果我们想获得一个不一样的字符串,那么就只能基于原字符串进行裁剪、拼接等操作,从而**生成一个新的字符串**。 +- 裁剪操作用切片表达式 +- 拼接操作用+号 + + +**为何说strings.Builder拼接时更有优势** +Builder值是可以被重用的。通过调用它的Reset方法,我们可以让Builder值重 新回到零值状态,就像它从未被使用过那样 +- Builder值中有一个用于承载内容的容器(以下简称内容容器)。它是一个以byte为元素类型的切片。 +- Builder值拥有的一系列指针方法,包括: Write、WriteByte、WriteRune和WriteString。我们可以把它们统称为拼接方法。 + - 如有必要,Builder值会自动地对自身的内容容器进行扩容。这里的自动扩容策略与切片的扩容策略一致。 + - 手动扩容,调用Grow方法(也可以被称为扩容方法),它接受一个int类型的参数n,该参数用于代表将要扩充的字节数量。 + + +- Builder值是可以**被重用的**。通过调用它的Reset方法,我们可以让Builder值重 新回到零值状态,就像它从未被使用过那样。 + + +**Builder使用约束:** +- 在已被真正使用后就不可再被复制; + - 这里所说的**复制方式**,包括但不限于在**函数间传递值、通过通道传 递值、把值赋予变量**等等。 + +- **并发不安全**: + - 由于其内容不是完全不可变的,所以需要使用方自行解决操作冲突和并发安全问题。 + - 在通过传递其指针值共享Builder值 的时候,一定要确保各方对它的使用是正确、有序的,并且是并发安全的; + - 而最彻底的解决方案是,绝不共享Builder值以及它的指针值。 +- 我们可以在各处分别声明一个Builder值来使用,也可以先声明一个Builder值,然后在真正使用它之前,便将它的副本传到各处。 +- 另外,我们还可以先使用再传递,只要在传递之 前调用它的Reset方法即可。 + + +**为什么说strings.Reader类型的值可以高效地读取字符串?** + +- 它封装了很多用于在string值上 读取内容的最佳实践。Reader值实现高效读取的关键就在于它内部的已读计数。 + - 在读取的过程中,Reader值会保存已读取的字节的计数。 `readingIndex := reader1.Size() - int64(reader1.Len()) // 计算出的已读计数。` + - ReadByte方法 会在读取成功后将这个计数的值加1。 + - ReadRune方法在读取成功之后,会把被读取的字符所占用的字节数作为计数的增量。 + - ReadAt方法算是一个例外。它既不会依据已读计数进行读取,也不会在读取后更新它。正因为如此,这个方法**可以自由地读取其所属的Reader值中的任何内容**。 + - 计数的值就代表着下一次读取的起始索引位置,它可以很容易地被计算出来。Reader值的Seek方法可以直接 设定该值中的已读计数值。 + + +strings包还提供了大量的函数。比如: `Count`、`IndexRune`、`Map`、`Replace`、`SplitN`、`Trim`,等等。 + + +### 38-39. bytes包与字节串操作 + +与strings包函数的数量和功能类似,**strings包**主要面向的是**Unicode 字符和经过 UTF-8 编码的字符串**,而**bytes包面对**的则主要是**字节和字节切片**。 + + +**bytes.Buffer** + +- 字节序列的缓冲区 +- bytes.Buffer不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序地读取其中的子序列。 +- bytes.Buffer类型同样是使用字节切片作为内容容器的, 有一个int类型的字段,表示已读字节的计数。这里的已读计数就无法通过bytes.Buffer提供的方法计算出来了。 +- buffer1的Len方法返回的也是内容容器中未被读取部分的长度,而不是其中已存内容的总长度。 +- Buffer值的容量指的是它的内容容器(也就是那个字节切片)的容量,它只与在当前值 之上的写操作有关,并会随着内容的写入而不断增长。 如果一边写一遍读,容量会只增不减? 还是未读内容超过之前容量才会报错。 + + +**bytes.Buffer类型的值记录的已读计数,在其中起到了怎样的作用?** + +1. 读取内容时,相应方法会依据已读计数找到未读部分,并在读取后更新计数。 +2. 写入内容时,如需扩容,相应方法会根据已读计数实现扩容策略。 + - 扩容后,方法将会把已读计数的值置为0,以表示下一次读取需要从内容容器的第一个字节开始。 + - 用于写入内容的相应方法,包括了所有名称以Write开头的方法,以及ReadFrom方 法。 +3. 截断内容时,相应方法截掉的是已读计数代表索引之后的未读部分。 + - 用于截断内容的方法**Truncate**,它会接受一个int类型的参数,这个参数的值代表了:在截断时需要保留头部的多少 个字节。 + - 这里说的头部指的并不是内容容器的头部,而是其中的未读部分的头部。头部的起始索引正是由已读计数的值表示的。 + - 已读计数的值再加上参数值后得到的和,就是内容容器新的总长度。 +4. 读回退时,相应方法需要用已读计数记录回退点。 + - 在bytes.Buffer中,用于读回退的方法有UnreadByte和UnreadRune。 这两个方法分别用于回退一个字节和回退一个 Unicode 字符。 + - UnreadByte方法的做法比较简单,把已读计数的值减1就好了。而UnreadRune方法需要 从已读计数中减去的,是上一次被读取的 Unicode 字符所占用的字节数。 + - 只有紧接在调用ReadRune方法之后,对UnreadRune方法的调用才能够成功完成。 +5. 重置内容时,相应方法会把已读计数置为0。 +6. 导出内容时,相应方法只会导出已读计数代表的索引之后的未读部分。 +7. 获取长度时,相应方法会依据已读计数和内容容器的长度,计算未读部分的长度并返回。 + +>在已读计数代表的索引之前的那些内容,永远都是已经被读过的,它们几乎没有机会再次被读取。 + + +**bytes.Buffer的扩容策略是怎样的?** + + +- Buffer值既可以被手动扩容,也可以进行自动扩容。 两种方式策略基本一致,所以除非完全确定后续内容所需字节数,否则用自动扩容就行了。 +- 在扩容时,先判断内容容器的**剩余容量**,是否可以满足调用方的要求,或者是否足够容纳新的内容。如果可以,那么扩容代码会在当前的内容容器之上,进行长度扩充。 + - 也就是,通过切片操作对原有的内容容器的长度进行扩充。 + - 像这样: `b.buf = b.buf[:length+need]` +- 如果内容容器的剩余容量不够了,那么扩容代码可能就会用新的内容容器去替代原有的内容容器,从而实现扩容。 + - **进一步优化**, 如果当前内容容器的容量的一半,仍然大于或等于其现有长度再加上另需的字节数的和。 `cap(b.buf)/2 >= len(b.buf)+need` + - 那么,扩容代码就会复用现有的内容容器,并把容器中的未读内容拷贝到它的头部位置。 + - 这意味着已读内容将会维度内容和之后的新内容覆盖掉。 已读内容长度超过这部分覆盖数据,那剩下的是置为空? + - 为了内部数据的一致性,以及避免原有的已读内容可能造成的数据混乱,扩容代码还会把**已读计数置为0**,并再**对内容容器做一下切片操作**,以掩盖掉原有的已读内容。 + - **若这一步优化未能达成**,那么就只能再创建一个新的容器,新容器的容量将会等于原有容量的二倍再加上另需字节数的和。`新容器的容量 =2* 原有容量 + 所需字节数`。 + + +**bytes.Buffer中的哪些方法可能会造成内容的泄露?** + + +- **内容泄露**是指:使用Buffer值的一方通过某种非标准的(或者说不正式的)方式,得到了本不该得到的内容。 +- Bytes方法和Next方法都可能会造成内容的泄露。 + - 原因在于,它们都**把基于内容容器的切片直接返回给了方法的调用方**。通过切片,我们可以直接访问和操纵它的底层数组。 + - Bytes方法: 它会返回在调用那一刻其所属值中的所有未读内容。 +```go +contents := "ab" +buffer1 := bytes.NewBufferString(contents) //此时buffer1容量是8,为啥是8,看runtime包中一个 名叫stringtoslicebyte的函数 +unreadBytes := buffer1.Bytes() //获取了当下所有未读内容 +buffer1.WriteString("cdefg") + +//由于这个结果值与buffer1的内容容器在此时还共用着同一个底层数组, +//所以,我只需通过简单的再切片操作,就可以利用这个结果值拿到buffer1在此时的所有未读内容。 +unreadBytes = unreadBytes[:cap(unreadBytes)] + +//如果我当时把unreadBytes的值传到了外界,那么外界就可以通过该值操纵buffer1的内容了 +unreadBytes[len(unreadBytes) -2] = byte('X') //’X‘的ASCII编码为88 + +``` + + +### 40-41. io包中的接口与工具 +(上)-讲了一些io包中接口设计原理和好处的点。 **值得仔细看看** + + +**在io包中,io.Reader的扩展接口和实现类型都有哪些?它们分别都有什么功用?** +- 扩展接口有以下几种 + 1. io.ReadWriter:此接口既是io.Reader的扩展接口,也是io.Writer的扩展接口。该接口定义了一组行为,包含且仅包含了基本的字节序列读取方法Read,和 字节序列写入方法Write。 + 2. io.ReadCloser:此接口除了包含基本的字节序列读取方法之外,还拥有一个基本的关 闭方法Close。是io.Reader接口和 io.Closer接口的组合。 + 3. io.ReadWriteCloser:三合一 + 4. io.ReadSeeker: 拥有一个用于寻找读写位置的基本方法Seek。该方法可以根据给定的偏移量基于数据的起始位置、末尾位置,或者当前读写 位置去寻找新的读写位置。 这个位置表示下次读或者写的起始索引。 + 5. io.ReadWriteSeeker: 三合一 +- 接口的实现类型: + 1. *io.LimitedReader:此类型的基本类型会包装io.Reader类型的值,并提供一个额外的受限读取的功能。 + 1. 读取方法Read**返回的总数据量会受到限制**,无论该方法被调用多少次。 + 2. 限制由该类型的字段N指明,单位是字节。 + 2. *io.SectionReader:此类型的基本类型可以包装io.ReaderAt类型的值,并且会限制它的Read方法,**只能够读取原始数据中的某一个部分**(或者说某一段)。 + 1. 这个数据段的起始位置和末尾位置,需要在它被初始化的时候就指明,并且之后无法变更。 + 2. 类似切片,只会对外暴露在其窗口之中的那些数据。 + 3. *io.teeReader:此类型是一个包级私有的数据类型,也是io.TeeReader函数结果值的实际类型。 + 1. 这个函数接受两个参数r和w,类型分别是io.Reader和io.Writer。 + 2. 其结果值的Read方法会把r中的数据经过作为方法参数的字节切片p写入到w。 + 4. io.multiReader:此类型也是一个包级私有的数据类型。 + 1. 类似的,io包中有一个名为MultiReader的函数,它可以接受若干个io.Reader类型的参数值,并返回一个实际类型为io.multiReader的结果值。 + 2. 当这个结果值的Read方法被调用时,它会顺序地从前面那些io.Reader类型的参数值中读取数据。 + 5. io.pipe:此类型为一个包级私有的数据类型。它不但实现了io.Reader接口,而且还实现了io.Writer接口。 + 1. io.PipeReader类型和io.PipeWriter类型拥有的所有指针方法都是以它为基础的。 + 2. io.Pipe函数会返回这两个类型的指针值并分别把它们作为其生成的同步内存管道的两端。 + 3. *io.pipe类型就是io包提供的同步内存管道的核心实现。 + 6. io.PipeReader:此类型可以被视为io.pipe类型的代理类型。它代理了后者的一部分功能,并基于后者实现了io.ReadCloser接口。 + 7. io.SectionReader + + +**io包中的接口都有哪些?它们之间都有着怎样的关系?** +- 简单接口11个。 + - 根据针对的 I/O 操作的不同,可以分为四大类,这四大类接口分别针对于四种操作,即:**读取、写入、关闭和读写位置设定**。 + - 读取操作相关的接口有 5 个,写 入操作相关的接口有 4 个,而与关闭操作有关的接口只有 1 个,还有一个读写位置设定相关的接口。 +>**没有嵌入其他接口**并且**只定义了一个方法**的接口叫做**简单接口**. +- 在简单接口里面,有众多扩展接口和实现类型的接口,可以称为 **核心接口**, 有3个,它们是:io.Reader、io.Writer和io.Closer。 + + +- 简单接口--**读取类型**: 5个扩展接口,6个实现类型。 + - io.ByteReader和io.RuneReader这两个简单接口。 + - 它们分别定义了一个读取方法,即:ReadByte和ReadRune。 + - 分别只能够读取下一个单一的 字节和 Unicode 字符。 + - 数据类型strings.Reader和bytes.Buffer都是io.ByteReader和 io.RuneReader的实现类型。这两个类型还都实现了io.ByteScanner接口和io.RuneScanner接口。 + - io.ByteScanner接口内嵌了简单接口io.ByteReader,并定义了额外的UnreadByte方法。如此一来,它就抽象出了一个能够读取和读回退单个字节的功能集。与之类似,io.RuneScanner也内嵌接口io.ByteReader,并定义了额外的UnreadRune方法。 + - io.ReaderAt接口,只定义了一个方法ReadAt。只读,不会修改已读计数的值。 + - io.WriterTo接口,只定义了一个方法WriteTo,读取方法,接收一个io.Writer类型的参数值,并会把其所属值中的数据读出并写入到这个参数值中。 + - io.ReaderFrom接口与WriteTo相对应,从该参数值中读出数据, 并写入到其所属值中。 + - io.CopyN函数,在复制数据的时候会先检测其参数 src的值,是否实现了io.WriterTo接口。 还会检测dst的值是否实现了io.ReaderFrom接口。 + - io.Copy函数和io.CopyBuffer函数来说也是如此 + - 在io包 中,与写入操作有关的接口都与读取操作的相关接口有着一定的对应关系。 + - io.Seeker接口,一个读写位置设定相关的简单接口。 + - 扩展接口io.ReadSeeker和 io.ReadWriteSeeker、io.WriteSeeker。 + - 两个指针类型strings.Reader和io.SectionReader都实现了 io.Seeker接口。顺便说一句,这两个类型也都是io.ReaderAt接口的实现类型 + - 关闭操作相关的接口io.Closer,扩展接口带Closer。实现类型,io包 中只有io.PipeReader和io.PipeWriter。 + + +### 42-43. bufio包中的数据类型 +>这个代码包中的程序实体实现的 I/O 操作都内置了缓冲区 +bufio包中的数据类型主要有: +1. Reader; +2. Scanner; +3. Writer和ReadWriter。 + + +**bufio.Reader类型值中的缓冲区起着怎样的作用?** + + +- bufio.Reader类型的值(以下简称Reader值)内的缓冲区,其实就是一个数据存储中 介,它介于底层读取器与读取方法及其调用方之间。 + - 所谓的底层读取器,就是在初始化此类 值的时候传入的io.Reader类型的参数值。 + - Reader值的读取方法一般都会先从其所属值的缓冲区中读取数据。 + + +- bufio.Reader类型并不是开箱即用的,因为它包含了一些**需要显式初始化的字段**。 + - buf:[]byte类型的字段,即字节切片,**代表缓冲区**。虽然它是切片类型的,但是其长度却会在初始化的时候指定,并在之后保持不变。 + - rd:io.Reader类型的字段,代表**底层读取器**。缓冲区中的数据就是从这里拷贝来的。 + - r:int类型的字段,代表对缓冲区进行**下一次读取时的开始索引**。我们可以称它为已读计数。 + - w:int类型的字段,代表对缓冲区进行下一次**写入时的开始索引**。我们可以称之为已写计数。 + - err:error类型的字段。它的值用于表示在从底层读取器**获得数据时发生的错误**。这里的值在被读取或忽略之后,该字段会被置为nil。 + - lastByte:int类型的字段,用于记录缓冲区中最后一个被读取的字节。**读回退时**会用到它的值。 + - lastRuneSize:int类型的字段,用于记录缓冲区中最后一个被读取的 Unicode 字符所占用的字节数。读回退的时候会用到它的值。这个字段只会在其所属值的ReadRune方法中才会被赋予有意义的值。在其他情况下,它都会被置为-1。 +- 两个用于**初始化Reader值的函数**: + - NewReader; 初始化的Reader值会拥有一个默认尺寸的缓冲区。这个默认尺寸是 4096个字节,即:4 KB + - NewReaderSize; 将缓冲区尺寸的决定权抛给了使用方 + - 它们都会**返回一个*bufio.Reader类型**的值。 +- 在bufio.Reader类型拥有的**读取方法**中,Peek方法和ReadSlice方法都会调用该类型一个**名为fill的包级私有方法**。 + - fill方法的作用是填充内部缓冲区。 + - fill方法会先检查其所属值的已读计数。如果这个计数不大于0,会有两种可能: + - 一种可能是其缓冲区中的字节都是全新的,也就是说它们都没有被读取过 + - 另一种可能是**缓冲区刚被压缩过**。 + +- 对缓冲区的压缩包括两个步骤: + - 第一步,把缓冲区中在[已读计数, 已写计数)范围之内的所有元素值(或者说字节)都依次拷贝到缓冲区的头部。 + - 第二步,fill方法会把已写计数的新值设定为原已写计数与原已读计数的差(就是压缩后还剩下的内容长度)。这个差所代表的索引,就是压缩后第一次**写入字节时的开始索引。**该方法还会把已读计数的值置为0。 +- fill方法会判断从底层读取器读取数据的时候,是否有错误发生。如果有,那么它就会把错误值 赋给其所属值的err字段,并终止填充流程。 + + +**难点** + + +1. 实际上,fill方法只要在开始时发现其所属值的已读计数大于0,就会对缓冲区进行一次压缩。之后,如果缓冲区中还有可写的位置,那么该方法就会对其进行填充。 **啥意思没看懂** +2. 在填充缓冲区的时候,fill方法会试图从底层读取器那里,读取足够多的字节,并尽量把 从已写计数代表的索引位置到缓冲区末尾之间的空间都填满。 + + + +**问题 1:bufio.Writer类型值中缓冲的数据什么时候会被写到它的底层写入器?** + +- bufio.Writer类型都有哪些字段: + - err:error类型的字段。它的值用于表示在向底层写入器**写数据时发生的错误**。 + - buf:[]byte类型的字段,代表缓冲区。在初始化之后,它的长度会保持不变。 + - n:int类型的字段,代表对缓冲区进行下一次**写入时的开始索引**。我们可以称之为已写计数。 + - wr:io.Writer类型的字段,代表底层写入器。 +- bufio.Writer类型有一个名为`Flush`的方法,它的主要功能是把相应缓冲区中暂存的所有数据,都写到底层写入器中。 + - 数据一旦被写进底层写入器,该方法就会把它们从缓冲区中删除掉。有时候只是逻辑删除。 + - 不论是否成功地写入了所有的暂存数据, Flush方法都会妥当处置,并保证**不会出现重写和漏写**的情况。 + - bufio.Writer类型值(以下简称Writer值)拥有的所有数据写入方法都会在必要的时候 调用它的Flush方法。 + - 在把数据写进缓冲区之后,调用Flush方法,以便为后续的新 数据腾出空间。 + - 发现缓冲区中的可写空间不足以容纳 新的字节,或 Unicode 字符的时候,调用Flush方法。 + - 如果Write方法发现需要写入的字节太多,**同时缓冲区已空**,那么它就会跨过缓冲区,并直接把这些数据写到底层写入器中。 + - 只要缓冲区中的可写空间无法容纳需要写入的新数据,Flush方法就 一定会被调用。 比如大小超过4096字节。 + +>在你把所有的数据都 写入Writer值之后,再调用一下它的Flush方法,显然是最稳妥的。 + + +**bufio.Reader类型读取方法有哪些不同?** + + +有 4 个方法可以作为**不同读取流程**的代表,它们是: +- `Peek` : 读取并返回其缓冲区中的n个未读字节,并且它会从已读计数代表的索引位置开始读。 + - 在缓冲区未被填满,并且其中的未读字节的数量小于n的时候,该方法就会调用fill方法, 以启动缓冲区填充流程。 如果发现上次填充有错误,就不会再次填充。 + - 如果n比缓冲区长度都大,或者缓存区未读字节小于n。 + - 那么Peek方法会将“**所有未读字节组成的序列**”作为第一个返回值返回。 + - 同时,它通常还把“bufio.ErrBufferFull变量的值(以下简称缓冲区已满的错误)” 作为第二个结果值返回,用来表示:虽然缓冲区被压缩和填满了,但是仍然满足不了要求。 + - 正常情况返回:“以已读计数为起始的n个字节”和“表示未发生任何错误的nil”。 + - **鲜明特点**: 即使它读取了缓冲区中的数据,也**不会更改已读计数的值**。 +- `Read` + - 与Peek共同点: 只要它们把获取到的数据写入缓冲区,就会及时地更新已写计数的值。 + - 缓冲区还有未读字节:把缓冲区中的未读字节,依次拷贝到其参数p代表的字节切片中,并立即根据实际拷贝的字节数增加已读计数的值。 + - 当缓冲区中已无未读字节时,Read方法会先检查参数p的长度是否大于或等于缓冲区的长度。 + - 如果是,那么Read方法会索性放弃向缓冲区中填充数据,转而**直接从其底层读取器中读出数据并拷贝到p中**。此时,数据的读出速度就成为了这种情况下方法执行耗时的决定性因素。 + - 如果不是,会先把已读计数和已写计数的值都重置为0,然后再尝试着使用从底层读取器那获取的数据,对缓冲器进行一次从头到尾的填充。 +- 下面两个方法从功能上看,都是持续地读取数据,直至遇到调用方给定的分隔符为止。 +- `ReadSlice`:会先在其缓冲区的未读部分中寻找分隔符。 + - 如果未能找到,并且缓冲区未满,那么该方法会先通过调用fill方法对缓冲区进行填充,然后再次寻找,如此往复。 + - 如果缓冲区已被填满,但仍然没能找到分隔符的情况。ReadSlice方法会把整个缓冲区(也就是buf字段代表的字节切片)作为第一个结果值,并把缓冲区已满的错误(即bufio.ErrBufferFull变量的值)作为第二个结果 值。 + - 一旦ReadSlice方法找到了分隔符,它就会在缓冲区上切出相应的、包含分隔符的字节切片,并把该切片作为结果值返回。 + - `ReadBytes`:会通过调用ReadSlice方法一次又一次地从缓冲区中读取数据,直至找到分隔符为止。 + - ReadSlice方法可能会因缓冲区已满而返回所有已读到的字节和相应的错 误值,但ReadBytes方法总是会忽略掉这样的错误,并再次调用ReadSlice方法,这使得后者会继续填充缓冲区并在其中寻找分隔符。 + - 除非ReadSlice方法返回的错误值并不代表缓冲区已满的错误,或者它找到了分隔符,否则这一过程永远不会结束。 + - 如果寻找的过程结束了,不管是不是因为找到了分隔符,ReadBytes方法都会把在这个过程中读到的所有字节,**按照读取的先后顺序组装成一个字节切片,并把它作为第一个结果值**。如果过程结束是因为出现错误,那么它还会把拿到的错误值作为第二个结果值。 +- 有个**安全性方面的问题**需要你注意。bufio.Reader类型的 Peek方法、ReadSlice方法和ReadLine方法都有可能会**造成内容泄露**。 + +>bufio.Writer类型。把该类值的缓冲区中暂存的数据写进其底层写入器的 功能,主要是由它的Flush方法实现的。 + + +### 44-45. os包中的API + +>这个代码包提供的都是平台不相关的 API。在不同的os上提供统一的使用接口。 + +os包中的 API 主要可以帮助我们使用操作系统中的文件系统、权限系统、环境变量、系统 进程以及系统信号。 + + +- 操纵文件系统的 API 最为丰富。创建和删除文件以及目 录,还可以获取到它们的各种信息、修改它们的内容、改变它们的访问权限 + - os.File类型代表了操作系统中的文件,对于类 Unix 的操作系统(包括 Linux、macOS、FreeBSD 等),其中的一切都可以被看做是文件。 + + +**os.File类型都实现了哪些io包中的接口?** + + +- os.File类型拥有的都是指针方法,所以除了空接口之外,它本身没有实现任何接口。而**它的指针类型则实现了很多io代码包中的接口**。 +- 首先,对于io包中最核心的 3 个简单接口io.Reader、io.Writer和io.Closer, *os.File类型都实现了它们。 +- 其次,该类型还实现了另外的 3 个简单接口,即:io.ReaderAt、io.Seeker和 io.WriterAt。 +- 总之,os.File类型及其指针类型的值,不但可以通过各种方式读取和写入某个文件中的 内容,还可以寻找并设定下一次读取或写入时的起始索引位置,另外还可以随时对文件进行关闭。 +- 但是,它们并不能专门地读取文件中的下一个字节,或者下一个 Unicode 字符,也不能进行任何的读回退操作。因为没有实现简单接口io.ByteReader和io.RuneReader。 + + +**如何获得一个os.File类型的指针值?** + + +- `Create`:函数用于根据给定的路径创建一个新的文件。 + - 它会返回一个File值和一个错误值。 + - 返回非nil的错误:比如,如果我们给定的路径上的某一级父目录并不存在,那么该函数就会返回一个*os.PathError类型的错误值,以表示“不存在的文件或目录”。 + - 如果在我们给予os.Create函数的路径之上,已经存在了一个文件,那么该函数会先清空现有文件中的全部内容,然后再把它作为第一个结果值返回。 + - 任何能够登录其所属的操作系统的用户都能操作该文件。 + +- `NewFile`:该函数在被调用的时候,需要接受一个代表文件描述符的、uintptr类型的值,以及一个用于表示文件名的字符串值。 + - 如果我们给定的文件描述符并不是有效的,那么这个函数将会返回nil,否则,它将会返回一个**代表了相应文件的File值**。 + - 它的功能并**不是创建一个新的文件**,而是依据一个已经存在的文件的描述符,来新建一个包装了该文件的File值。 + - 比如像下面这样拿到一个包装了标准错误输出的File值: + ```go + file3 := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") + ``` +- `Open`: 打开一个文件并返回包装了该文件的File值。 + - 该函数只能以只读模式打开文件。无法写入。 + - 如果我们调用了这个File值的任何一个写入方法,那么都将会得到一个表示了“坏的文件描述符”的错误值。 + >- 所谓的文件描述符,是由通常很小的非负整数代表的。它一般会由 I/O 相关的系统调用返 回,并作为某个文件的一个标识存在。 + >- os.File类型有一个指针方法,名叫Fd。它在被调用之后将会返回一个uintptr类型的 值。这个值就代表了当前的File值所持有的那个文件描述符。 + +- `OpenFile`: 这个函数其实是os.Create函数和os.Open函数的底层支持,它最为灵活。 + - 这个函数有 3 个参数,分别名为name、flag和perm。 + - name指代的就是**文件的路径**。 + - flag参数指的则是需要施加在文件描述符之上的**操作模式**,只读模式就是这里的一个可选项(只读模式由常量os.O_RDONLY代表,它是**int**类型的)。 + - perm 代表的**权限模式**,它的类型是**os.FileMode**,此类型是一个基于uint32类型的再定义类型。 + + +**问题 1:可应用于File值的操作模式都有哪些?** +>针对File值的操作模式主要有**只读模式**、**只写模式**和**读写模式**。 + + +- 这些模式分别由常量os.O_RDONLY、os.O_WRONLY和os.O_RDWR代表。 +- 新建或者打开一个文件的时候,必须指定其中之一为该文件的操作模式。 + + +- 还可以为这里的文件设置**额外**的操作模式,可选项如下所示。 + - os.O_APPEND:当向文件中写入内容时,把新内容追加到现有内容的后边。 + - os.O_CREATE:当给定路径上的文件不存在时,创建一个新文件。 + - os.O_EXCL:需要与os.O_CREATE一同使用,表示在给定的路径上不能有已存在的文件。 + - os.O_SYNC:在打开的文件之上实施同步 I/O。它会保证读写的内容总会与硬盘上的数据保持同步。 + - os.O_TRUNC:如果文件已存在,并且是常规的文件,那么就先清空其中已经存在的任何内容。 + + +对于以上操作模式的使用,os.Create函数和os.Open函数都是现成的例子。 +```go +func Create(name string) (*File, error) { + //如果参数name代表路径之上的文件不存在,那么就新 建一个,否则,先清空现存文件中的全部内容。 + return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) +} +``` +- 多个操作模式是通过按位或操作符|组合起来的。 +- os.Open函数的功能是:以只读模式打开已经存在的文件。其根源就是它 在调用os.OpenFile函数的时候,只提供了一个单一的操作模式os.O_RDONLY。 +```go +func Open(name string) (*File, error) { + return OpenFile(name, O_RDONLY, 0) +} +``` + + +**问题 2:怎样设定常规文件的访问权限?** + + +- os.OpenFile函数的第三个参数perm代表的是权限模式,其类型是 os.FileMode(基于uint32类型的再定义类型)。 +- os.FileMode类型能够代表的,可远不只权限模式,它还可以代表文件模式(也可以称之为文件种类). +- os.FileMode包含32个比特位,在这 32 个比特位当中,每个比特位都有其特定的含义: + - 如果在其最高比特位上的二进制数是1,那么该值表示的文件模式就等同于 os.ModeDir,也就是说,相应的文件代表的是一个目录。 + - 如果其中的第 26 个比特位上的是1,那么相应的值表示的文件模式就等同于 os.ModeNamedPipe,也就是说,那个文件代表的是一个命名管道。 +- 只有最低的 9 个比 特位才用于表示**文件的权限**。 + - 当我们拿到一个此类型的值时,可以把它和os.ModePerm常量的值(是0777,是一个八进制的无符号整数,其最低的 9 个比特位上都是1,而更 高的 23 个比特位上都是0。)做按位与操作。即可得到这个FileMode值中所有用于表示文件权 限的比特位,也就是该值所表示的权限模式。 + - 在这 9 个用于表示文件权限的比特位中,每 3 个比特位为一组,共可分为 3 组。 + - 从高到低,这 3 组分别表示的是文件所有者(也就是创建这个文件的那个用户)、文件所 有者所属的用户组,以及其他用户对该文件的访问权限。 + - 而对于每个组,其中的 3 个比特 位从高到低分别表示读权限、写权限和执行权限。 + - 如果在其中的某个比特位上的是1,那么就意味着相应的权限开启,否则,就表示相应的权限关闭。 + - 只有在新建文件的时候,这里的第三个参数值才是有效的。在其他情况下,即使我们设 置了此参数,也不会对目标文件产生任何的影响。 + + +### 46. 访问网络服务 +**socket**:常被翻译为套接字, 是一种 IPC 方法。 + +- IPC 是 Inter-Process Communication 的缩写,可以被翻译为**进程间通信**。 +- IPC 这个概念(或者说规范)主要定义的是多个进程之间, 相互通信的方法。 +- 这些方法主要包括:系统信号(signal)、管道(pipe)、套接字 (socket)、文件锁 (file lock)、消息队列(message queue)、信号灯(semaphore,有的地方也称之为 信号量)等 +- 在众多的 IPC 方法中,socket 是最为通用和灵活的一种。利用 socket 进行通信的进程,可以不局限在同一台计算机当中。 + + +- 在 Linux 操作系统中,用于创建 socket 实例的 API,就是由一个名为socket的系 统调用代表的。 +- 在 Go 语言标准库的syscall代码包中,有一个与这个socket系统调用相对应的函数(syscall.Socket)。 +- 这两者的函数签名是基本一致的,它们都会**接受三个int类型的参数**,并会返回一个可以代表文件描述符的结果。 +- 在其底层,Go 语言为它支持的每个操作系统都做了适配,使得该函数跨平台。 +- Go 语言的net代码包中的很多程序实体,都会直接或间接地使用到syscall.Socket函数。 + - 在调用net.Dial函数的时候,会为它的两个参数设定值。其中的第一个参数名为network,它决定着 Go 程序在底层会创建什么样的 socket 实例,并使用什么样的协议 与其他程序通信。第二个参数是address,都是string类型的。 + + +**net.Dial函数的第一个参数network有哪些可选值?** + + +* **"tcp"**:代表 TCP 协议,其基于的 IP 协议的版本根据参数address的值自适应。 +* "tcp4":代表基于 IP 协议第四版的 TCP 协议。 +* "tcp6":代表基于 IP 协议第六版的 TCP 协议。 +* "udp":代表 UDP 协议,其基于的 IP 协议的版本根据参数address的值自适应。 +* "udp4":代表基于 IP 协议第四版的 UDP 协议。 +* "udp6":代表基于 IP 协议第六版的 UDP 协议。 +* "unix":代表 Unix 通信域下的一种内部 socket 协议,以 SOCK_STREAM 为 socket +* 类型。 +* "unixgram":代表 Unix 通信域下的一种内部 socket 协议,以 SOCK_DGRAM 为 socket 类型。 +* "unixpacket":代表 Unix 通信域下的一种内部 socket 协议,以 SOCK_SEQPACKET 为 socket 类型。 + + +**syscall.Socket函数的三个int参数**分别表示:实例通信域、类型以及使用的协议 +1. 第一个参数:**通信域**主要有这样几个可选项:IPv4 域、IPv6 域和 Unix 域。 + - Unix 域,指的是一种类 Unix 操作系统中特有的通信域。 + - 以上三种通信域分别可以由syscall代码包中的常量AF_INET、AF_INET6和AF_UNIX表示。 +2. 第二个参数:**类型**一共有 4 种,分别是:SOCK_DGRAM、SOCK_STREAM、SOCK_SEQPACKET 以及SOCK_RAW。syscall代码包中也都有同名的常量与之对应。 + - **SOCK_DGRAM**中的“DGRAM”代表的是 datagram,即**数据报文**。它是一种有消息边界, 但没有逻辑连接的非可靠 socket 类型,我们熟知的**基于 UDP 协议**的网络通信就属于此类。 + - 有消息边界:意思是与 socket 相关的操作系统内核中的程序(以下简称内核程序)在发送或接收数据的时候是以消息为单位的。 + >可以把消息理解为带有固定边界的一段数据。内核程序可以自动地识别和维护这种边界,并在必要的时候,把数据切割成一个一个的消息,或者把多个消息串接成连续的数据。 + - 逻辑连接是指:通信双方在收发数据之前必须先建立网络连接。只要应用程序指定好对方的网络地址,内核程序就可以立即把数据报文发送出去。 + - **SOCK_STREAM**: 它没有消息边界,但有逻 辑连接,能够保证传输的可靠性和数据的有序性,同时还可以实现数据的双向传输。 例如**TCP** + - 传输数据的形式是字节流,而不是数据报文。 + - 内核程序无法感知一段字节流中包含了多少个消息,以及这些消息是否完整,这完全需要应用程序自己去把控。 有序,应用程序需要根据双方的约定去数据中查找消息边界,并按照边界切割数据. +3. 第三个参数:socket 实例所使用的协议,只要明确指定了前两个参数的值,我们就无需再去确定第三个参数值了,一般置为0就可以了。这时,内核程序会自行选择最合适的协议。 + - 当前两个参数值分别为syscall.AF_INET和syscall.SOCK_DGRAM的时候,内核 程序会选择 UDP 作为协议。 + - 当前两个参数值分别为syscall.AF_INET6和syscall.SOCK_STREAM时,内 核程序可能会选择 TCP 作为协议。 + + +**调用net.DialTimeout函数时给定的超时时间意味着什么?** + + +- 代表着函数为网络连接建立完成而等待的最长时间 + - 开始的时间点几乎是我们调用net.DialTimeout函数的那一刻。 + - 在这之后,时间会主要 花费在“解析参数network和address的值”,以及“创建 socket 实例并建立网络连接”这两件事情上。 + - 在解析address的值的时候,函数会确定网络服务的 IP 地址、端口号等必 要信息,并在需要时访问 DNS 服务。 + - 如果解析出的 IP 地址有多个,那么函数会串行或并发地尝试建立连接。但无论用什 么样的方式尝试,函数总会以最先建立成功的那个连接为准。 + + +**怎样在net.Conn类型的值上 正确地设定针对读操作和写操作的超时时间?** +>net.Conn接口提供了SetDeadline, SetReadDeadline, SetWriteDeadline;调用 SetDeadline方法等于同时调用了后两个方法,因为其最总调用的setDeadlineImpl(fd, t, 'r'+'w') 对读和写都设置了超时时间。 + + +### 47. 基于HTTP协议的网络服务 + +如果我们只是访问基于 HTTP 协议的网络服务的话,那么使用net/http代码包中的程序实体来做,显然会更加便捷。 + +- http.Get函数 + - 传入一个URL就可以了,返回两个结果值。 + - 第一个结果值的类型是*http.Response,它是网络 服务给我们传回来的响应内容的结构化表示。 + - 第二个结果值是error类型的,它代表了在创建和发送 HTTP 请求,以及接收和解析 HTTP 响应的过程中可能发生的错误。 + - 在内部使用缺省的 HTTP 客户端,并且调用它的Get方法以完成功能。 + - 这个缺省的 HTTP 客户端是由net/http包中的公开变量DefaultClient代表的,其类型是 `*http.Client`。 + + +**http.Client类型中的Transport字段代表着什么?** + +- 代表着:向网络服务发送 HTTP 请求,并从网络 服务接收 HTTP 响应的操作过程。 +- 也就是说,该字段的方法RoundTrip应该实现单次 HTTP 事务(或者说基于 HTTP 协议的单次交互)需要的所有步骤。 +- 这个字段是http.RoundTripper接口类型的,它有一个由http.DefaultTransport变量代表的缺省值(以下简称DefaultTransport)。当我们在初始化一个http.Client 类型的值(以下简称Client值)的时候,**如果没有显式地为该字段赋值,那么这个 Client值就会直接使用DefaultTransport**。 默认值怎么联系上的? +- http.Client类型的**Timeout字段**,代表的正是前面所说的单次 HTTP 事务的超时时间,它是time.Duration类型的。它的零值是可用的,用于表示没有设置超时时间。 + + +**http.Transport类型** + +- 在内部使用一个net.Dialer类型的值(以下简称Dialer 值),并且,它会把该值的Timeout字段的值,设定为30秒。如果在 30 秒内还没有建立好网络连接,那么就会被判定为操作超时。 + - 字段KeepAlive: 它的背后是一种针对网络连接(更确切地说,是 TCP 连接)的存活探测机制。它的值用于表示每间隔多长时间发送一次探测包。当该值不大于0时,则表示不开启这种机制。 DefaultTransport会把这个字段的值设定为30秒。 +- IdleConnTimeout:含义是空闲的连接在多久之后就应该被关闭。DefaultTransport默认设为90秒。如果该值为0,那么就表示不关闭空 闲的连接。 +- ResponseHeaderTimeout:含义是,从客户端把请求完全递交给操作系统到从操作系 统那里接收到响应报文头的最大时长。DefaultTransport并没有设定该字段的值。 +- ExpectContinueTimeout:含义是,在客户端递交了请求报文头之后,等待接收第一 个响应报文头的最长时间。 +- TLSHandshakeTimeout: 这个字段代表了基于 TLS 协议的连接在被建立时的握手阶段的超时时间。若 该值为0,则表示对这个时间不设限。DefaultTransport把该字段的值设定为了10 秒。 +- MaxIdleConns; 只会对空闲连接的总数做出限定 +- MaxIdleConnsPerHost: 该Transport值访问的每一个网络服务的最大空闲连接数,不论这些连接是否是空闲的。 没有缺省值,零值表示不做限制。 +- MaxConnsPerHost: 缺省值由http.DefaultMaxIdleConnsPerHost变量代表,值为2。也就是说,在默认情况下,对于某一个Transport值访问的每一个网络 服务,它的空闲连接数都最多只能有两个。 + + +**http.Server类型的ListenAndServe方法都做了哪些事情?** + +>功能: 监听一个基于 TCP 协议的网络地址,并对接收到的 HTTP 请求进行处理。这个方法会默认开启针对网络连接的存活探测机制,以保证连接是持久的。 + +```go +func (srv *Server) ListenAndServe() error { + if srv.shuttingDown() { + return ErrServerClosed + } + addr := srv.Addr + if addr == "" { + //表示使用任何可以代表本机的域名和 IP 地址,并 且端口号为80。 + addr = ":http" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + //通过调用当前值的Serve方法准备接受和处理将要到来的 HTTP 请求。 + return srv.Serve(ln) +} +``` +1. net.Listen函数都做了哪些事情? + 1. 解析参数值中包含的网络地址隐含的 IP 地址和端口号; + 2. 根据给定的网络协议,确定监听的方法,并开始进行监听。 +2. http.Server类型的Serve方法是怎样接受和处理 HTTP 请求的。 + 1. 在一个for循环中,网络监听器的Accept方法会被不断地调用,该方法会返回两个结果值; + 1. net.Conn 包含了新到来的 HTTP 请求的网络连接; + 2. error, 如果不为nil,直接返回错误。循环终止,除非是暂时性错误会在一段时间后再次执行下次循环。 + 2. 把它的第一个结 果值包装成一个*http.conn类型的值(以下简称conn值),然后通过在新的 goroutine 中调用这个conn值的serve方法,来对当前的 HTTP 请求进行处理。 + + + + + + +### 48-49. 程序性能分析基础 +go性能分析API: +1. runtime/pprof; +2. net/http/pprof; +3. runtime/trace; + + +标准工具,主要有go tool pprof和go tool trace这两个。它们可以解析概要文 件中的信息,并以人类易读的方式把这些信息展示出来。 + + +在 Go 语言中,用于分析程序性能的概要文件有三种,分别是: +- CPU 概要文件(CPU Profile) +- 内存概要文件(Mem Profile) +- 阻塞概要文件(Block Profile) +>这些概要文件中包含的都是:在某一段时间内,对 Go 程序的相关指标进行多次采样后得到的概要信息。 这些概要信息以二进制的形式展现。 +- 查看二进制概要信息:` go tool pprof cpuprofile.out` + + +**怎样让程序对 CPU 概要信息进行采样?** +- runtime/pprof包中的 API + - 开始采样,调用StartCPUProfile函数 + - 先会去设定 CPU 概要信息的采样频率,并会在单独的 goroutine 中进行 CPU 概要信息的收集和输出。 + - StartCPUProfile函数设定的采样频率总是固定的,即:100赫兹。也就是说,每 秒采样100次,或者说每10毫秒采样一次。 + - runtime包中SetCPUProfileRate函数在被调用的时候,会保证采样频率不超过 1MHz(兆赫) + - 停止采样,调用StopCPUProfile函数 + - StopCPUProfile函数也会调用runtime.SetCPUProfileRate函数,并把参数值(也 就是采样频率)设为0。这会让针对 CPU 概要信息的采样工作停止。 + - 同时,它也会给负责收集 CPU 概要信息的代码一个“信号”,以告知收集工作也需要停止 了。 + + +**1. 怎么设定内存概要信息采样频率?** + +- **设置**:为runtime.MemProfileRate变量赋值即可。 + - 变量含义:平均每分配多少个字节,就对堆内存的使用情况进行一次采样 + - 变量的值设为0,那么,Go语言运行时系统就会完全停止对内存概要信息的采样。该变量的缺省值是512 KB,也就是512千字节。 +- **查看获取**:调用runtime/pprof包中的 `WriteHeapProfile`函数。 + - 该函数会把收集好的内存概要信息,写到我们指定的写入器中。 + - 通过WriteHeapProfile函数得到的内存概要信息并**不是实时的**,它是一个快 照,是在最近一次的内存垃圾收集工作完成时产生的。 + - 若想要实时信息,通过调用`runtime.ReadMemStats`函数。不过这个函数会引起Go语言调度器的短暂停顿。 + + +**2. 怎样获取到阻塞概要信息?** + +- **设置**:调用runtime包中的`SetBlockProfileRate函数`,即可对阻塞概要信息的采样频率进行设定。 + - 该函数有一个名叫`rate`的参数,它是int类型的。 + - 该参数含义是: 只要发现一个阻塞事件的持续时间达到了多少个纳秒,就可以对其进行采样。 + - 如果这个参数的值小于或等于0,那么就意味着 Go 语言运行时系统将会完全停止对阻塞概要信息的采样。 + - 还有一个名叫`blockprofilerate`的包级私有变量,它是uint64类型 的。这个变量的含义是,只要发现一个阻塞事件的持续时间跨越了多少个 CPU 时钟周期, 就可以对其进行采样。 + - 这两个参数只是单位不一样,实际上最终都是使用后者,runtime.SetBlockProfileRate函数会先对参数rate的值进行单位换算和必要的类型转换,然后,它会把换算结果用原子操作赋给blockprofilerate变量。 + - 此变量的**缺省值是0**,所以 Go 语言运行时系统在默认情况下并**不会记录任何在程序中发生的阻塞事件**。 +- **查看获取**: + - 先调用runtime/pprof包中的`Lookup函数`并传入参数值`"block"`,从而得到一个`*runtime/pprof.Profile`类型的值。 + - 在这之后,我们还需要调用这个Profile值的`WriteTo方法`,以驱使它把概要信息写进我们指定的写入器中。 + - WriteTo方法有两个参数,一个参数就是我们刚刚提到的写入器,它是io.Writer类型的。而另一个参数则是代表了**概要信息详细程度的int类型**参数debug。 + - debug参数主要的可选值有两个: 0和1. + - 0代表通过WriteTo方法写 进写入器的概要信息仅会包含go tool pprof工具所需的内存地址,这些内存地址会以十六进制的形式展现出来。概要信息会经由 protocol buffers 转换为字节流。 + - 1代表相应的包名、函数名、源码文件路径、代码行号等信息就都会作为注释被加入进去。概要信息是可以读懂的普通文本。 + - 2代表被输出的概要信息也会是普通的文本,并且通常会包含更多的细节。至于**这些细节都包含了哪些内容**,那就要看我们调用 runtime/pprof.Lookup函数的时候传入的是什么样的参数值了。 + + +**runtime/pprof.Lookup函数的正确调用方式是什么?** + +**作用**:提供与给定的名称相对应的概要信息。预先定义了 6 个概要名称:goroutine、heap、 allocs、threadcreate、block和mutex。 +- "goroutine": 当我们把"goroutine"传入Lookup函数的时候,该函数会利用相应的方法,收集到当前正在使用的所有 goroutine 的堆栈跟踪信息。 + >注意,这样的收集会引起 Go 语言调度器的 短暂停顿。 + - 当调用该函数返回的Profile值的WriteTo方法时,如果参数debug的值大于或等于2, 那么该方法就会输出所有 goroutine 的堆栈跟踪信息。 +- "heap": 收集与堆内存的分配和释放有关的采样信息。(就是内存概要信息)。“allocs”:基本同“heap”。 + - 区别在于在这两种 Profile值的WriteTo方法被调用时,它们输出的概要信息会有细微的差别,而且这仅仅体现在参数debug等于0的时候。 + - "heap"会使得被输出的内存概要信息默认以“在用空间”(inuse_space)的视角呈现。(已经被分配但还未被释放的内存空间。go tool pprof工具会忽略已释放空间相关的那部分信息) + - "allocs"对应的默认视角则是“已分配空间”(alloc_space)。 (所有的内存分配信息都会被展现出来,无论这些内存空间在采样时是否已被释放。) +- "threadcreate": 会使Lookup函数去收集一些堆栈跟踪信息。 + - 这些堆栈跟踪信息中的每一个都会描绘出一个**代码调用链**,这些调用链上的代码都**导致新的操作系统线程产生**。 +- "block"代表:因争用同步原语而被阻塞的那些代码的堆栈跟踪信息。也就是**阻塞概要信息**。 +- "mutex"代表: 曾经作为同步原语持有者的那些代码,它们的堆栈跟踪信息。 + - **同步原语**,指的是存在于 Go 语言运行时系统内部的一种底层的同步工具,或者说一种同步机制。 + - 它是直接面向内存地址的,并以异步信号量和原子操作作为实现手段。 + - 我们已经熟知的通 道、互斥锁、条件变量、”WaitGroup“,以及 Go 语言运行时系统本身,都会利用它来实现自己的功能。 + + +**如何为基于 HTTP 协议的网络服务添加性能分析接口?** + +- 在程序中导入`import _ "net/http/pprof"`, +- 然后启动网络服务并开始监听: `log.Println(http.ListenAndServe("localhost:8082", nil))` +- 在运行这个程序之后,我们就可以通过在网络浏览器中访问 http://localhost:8082/debug/pprof这个地址看到一个简约的网页。 + - 这个URL下还有很多可用的子路径,像allocs、block、goroutine、heap、mutex、threadcreate 这 6 个子路径 + - 在底层其实都是通过Lookup函数来处理的 + - 这些子路径都可以接受查询参数debug。它用于控制概要信息的格式和详细程度。 + - 还有一个名叫**gc**的查询参数。它用于控制是否在获取概要信息之前强制地执行一次垃圾回收。只要它的值大于0,就会垃圾回收,仅在`/debug/pprof/heap`路径下有效。 + - `/debug/pprof/heap`路径被访问,程序会去执行对CPU概要信息的采样 + - 它接受一个名为seconds的查询参数。该参数的含义是,采样工作需要持续多少秒。 如果这个参数未被显式地指定,那么采样工作会持续30秒。 + - 注意,在这个路径下,程序只会响应经 protocol buffers 转换的字节流。我们可以通过go tool pprof工具直接读取这样的 HTTP 响应,例如:`go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60` + - `/debug/pprof/trace`:在这个路径下, 程序主要会利用runtime/trace代码包中的 API 来处理我们的请求。 + - 程序会先调用trace.Start函数,然后在查询参数seconds指定的持续时间之后再调用trace.Stop函数。这里的seconds的缺省值是1秒。 +- 针对这写URL路径做对应的操作,支持自定义: +```go + +``` + - 使用第三方的网络服务开发框架时尤其有用。 + + +## 尾声和思考 diff --git "a/Java\345\237\272\347\241\200/\345\217\215\345\260\204&\346\263\233\345\236\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\203\347\274\226\347\250\213\343\200\213\350\257\273\344\271\246\347\254\224\350\256\260.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\345\217\215\345\260\204&\346\263\233\345\236\213.md" rename to "Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\203\347\274\226\347\250\213\343\200\213\350\257\273\344\271\246\347\254\224\350\256\260.md" diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\350\277\233\351\230\266\345\255\246\344\271\240\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\350\277\233\351\230\266\345\255\246\344\271\240\343\200\213.md" new file mode 100644 index 0000000..c152505 --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\350\277\233\351\230\266\345\255\246\344\271\240\343\200\213.md" @@ -0,0 +1,6 @@ + + +# 单元测试 + + +## 如何使用接口提高代码的可测试性 diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\350\277\233\351\230\266\347\237\245\350\257\206\347\202\271\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\350\277\233\351\230\266\347\237\245\350\257\206\347\202\271\343\200\213.md" new file mode 100644 index 0000000..a6d744d --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\350\277\233\351\230\266\347\237\245\350\257\206\347\202\271\343\200\213.md" @@ -0,0 +1,61 @@ + +# 进阶入门 + +## go程序是如何跑起来的? + +## 一些go独有的特点 + +1. 函数可以返回任意数量的返回值 +```go +package main + +import "fmt" + +func swap(x, y string) (string, string, string) { + return y, x, x + y +} + +func main() { + a, b, c := swap("hello", "world") + fmt.Println(a, b, c) +} +``` + +2. 短变量声明:在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。 + +函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用 + +```go +package main + +import "fmt" + +func main() { + var i, j int = 1, 2 + k := 3 + c, python, java := true, false, "no!" + + fmt.Println(i, j, k, c, python, java) +} +``` + +3. 可见性 +1)声明在函数内部,是函数的本地值,类似private +2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect +3)声明在函数外部且首字母大写是所有包可见的全局值,类似public. +>如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。 + + + +# 类型系统 + + + +# 高级结构 + + + + +# 并发 + + diff --git "a/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\351\241\271\347\233\256\345\274\200\345\217\221\345\256\236\346\210\230\343\200\213.md" "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\351\241\271\347\233\256\345\274\200\345\217\221\345\256\236\346\210\230\343\200\213.md" new file mode 100644 index 0000000..99ab11f --- /dev/null +++ "b/Go\350\257\255\350\250\200/\343\200\212Go\350\257\255\350\250\200\351\241\271\347\233\256\345\274\200\345\217\221\345\256\236\346\210\230\343\200\213.md" @@ -0,0 +1,537 @@ + +[TOC] + +# 一、环境准备 +>如何准备开发环境、制作 CA 证书,安装和配置用到的数据库、应用,以及 Shell 脚本编写技巧 + + +**项目背景**: 实现一个IAM(Identity and Access Management,身份识别与访问管理)系统。 +- 为了保障 Go 应用的安全,我们需要对访问进行认证,对资源进行授权。 + + +如何实现访问认证和资源授权呢? +- 认证功能不复杂,我们可以通过 JWT (JSON Web Token)认证来实现。 +- 授权功能的复杂性使得它可以囊括很多 Go 开发技能点。 本专栏学习就是将这两种功能实现升级为IAM系统,讲解它的构建过程。 + + +**创建数据库** + +```shell +sudo tee /etc/yum.repos.d/mongodb-org-4.4.repo<<'EOF' +[mongodb-org-4.4] +name=MongoDB Repository +baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.4/x86_64 +gpgcheck=1 +enabled=1 +gpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc +EOF +``` + +**创建CA证书** + + +```shell +tee ca-config.json << EOF +{ + "signing": { + "default": { + "expiry": "87600h" + }, + "profiles": { + "iam": { + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ], + "expiry": "876000h" + } + } + } +} +EOF +``` + +```shell +$ tee ca-csr.json << EOF +{ + "CN": "iam-ca", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names":[ + { + "C": "CN", + "ST": "BeiJing", + "L": "BeiJing", + "O": "marmotedu", + "OU": "iam" + } + ], + "ca": { + "expiry": "876000h" + } +} +EOF + +``` + +```shell +tee iam-apiserver-csr.json <目录规范、日志规范、错误码规范、Commit规范 + +规范设计 + +一个项目的规范设计主要包括编码类和非编码类这两类规范。 + +## 非编码类规范 + +1. 新开发的项目最好按照开源标准来规范,以驱动其成为一个高质量的项目。 +2. 开发之前,最好提前规范好文档目录,并选择一种合适的方式来编写 API 文档。 +3. 版本规范 + + +**API文档规范** + + +一个规范的 API 接口文档,通常需要包含一个完整的 API 接口介绍文档、API 接口变更 历史文档、通用说明、数据结构说明、错误码描述和 API 接口使用文档。 + + +**版本号规范** + +- 到底该如何确定版本号呢? + * 第一,在实际开发的时候,我建议你使用 0.1.0 作为第一个开发版本号,并在后续的每次 发行时递增次版本号。 + * 第二,当我们的版本是一个稳定的版本,并且第一次对外发布时,版本号可以定为 1.0.0。 + * 第三,当我们严格按照 Angular commit message 规范提交代码时,版本号可以这么来确定: + * fix 类型的 commit 可以将修订号 +1。 + * feat 类型的 commit 可以将次版本号 +1。 + * 带有 BREAKING CHANGE 的 commit 可以将主版本号 +1。 + + +**Commit 规范** + + +- Commit Message 包含三个部分,分别是 Header、Body 和Footer,格式如下: +```c + [optional scope]: + // 空行 + [optional body] + // 空行 + [optional footer(s)] +``` +- 每行50/72/100字比较合理。 +- Type分为两大类 + - Production: 修改会影响最终用户和生成环境的代码,这类改动要做好充分的测试。 + - feat: 新增功能 + - fix: Bug修复 + - perf: 提高代码性能的变更 + - refactor: 其他代码类变更, 不属于feat、fix、perf、style的。 + - Development: 一些项目管理类的变更,不影响生产环境,可以免测发布,比如CI流程、构建方式 + - style: 代码格式类变更。 比如删除空行、gofmt格式化代码。 + - docs: 文档的更新 + - test: 新增测试用例或者更新现有测试用例 + - chore: 其他不影响生产环境的变更。 比如 构建流程、依赖管理或者辅助工具的变更。 + + +**如何归类?** +- 如果**变更了应用代码**: 在代码类中,有 4 种具有明确变更意图的类型:feat、fix、perf 和 style;如果我们的代码变更不 属于这 4 类,那就全都归为 refactor 类,也就是优化代码。 +- 如果我们**变更了非应用代码**: 例如更改了文档,那它属于非代码类。在非代码类中,有 3 种具有明确变更意图的类型:test、ci、docs;如果我们的非代码变更不属于这 3 类,那 就全部归入到 chore 类。 + + +- **scope**: 说明 commit 的影响范围的,它必须是名词。 + - 主要是根据组件名和功能来设置的。例如,支持 apiserver、 authzserver、user 这些 scope。 + - 不适合设置太具体的值。 +- **subject**: 简短描述 + - 必须以动词开头,使用现在时。 + - subject 的 结尾不能加英文句号。 +- **body**: 更详细的变更说明,可选 + - 动词开头,必须包括修改的动机,以及和上一个版本的改动点。 + >例如: The body is mandatory for all commits except for those of scope "docs". When t。。。 +- **footer**: 说明本次commit导致的后果。 + - 在实际应用中,Footer 通常用来说明不兼容的改动和关闭的 Issue 列表 + - 不兼容的改动: 如果当前代码跟上一个版本不兼容,需要在 Footer 部分,以 `BREAKING CHANG`: 开头,后面跟上不兼容改动的摘要。 + - 关闭的 Issue 列表:关闭的 Bug 需要在 Footer 部分新建一行,并以 Closes 开头列 出,例如:Closes #123。关闭多个用逗号分隔。 +- 还原: revert: 开头,后跟还原的 commit 的 Header。 + - 而且,在 Body 中必须写成 This reverts commit ,其中 hash 是 要还原的 commit 的 SHA 标识。 + +>为了更好地遵循 Angular 规范,建议你在提交代码时养成不用 git commit -m,即不用 -m 选项的习惯,而是直接用 git commit 或者 git commit -a 进入交互界面编辑 Commit Message。 + + + **提交频率** + + +>随意 commit 不仅会让 Commit Message 变得难以理解,还会让其他研发同事觉 得你不专业 + - 开发完一个完整的功能,测试通过后就提交. + - 规定一个时间,定期提交。这里我建议代码下班前固定提交一次,并且要确保本地未提交的代码,延期不超过 1 天。 + + + **rebase** + + +- 可以在最后合并代码或者提交 Pull Request 前,执行 git rebase -i 合并之前的所有 commit。 + - 如何操作?用rebase。 + - git rebase 的最大作用是它可以重写历史。 + - 通常会通过 git rebase -i 使用 git rebase 命令,-i 参数表示交 互(interactive),该命令会进入到一个交互界面中,其实就是 Vim 编辑器。 + - 首先列出给定之前(不包括,越下面越新)的所有 commit,每个 commit 前面有一个操作命令,默认是 pick。 + - 可以选择不同的 commit,并修改 commit 前面的命令,来对该 commit 执行不同的变更操作。 + - git rebase支持的变更操作: + - p,pick 不对该commit做任何处理 + - r,reword 保留该commit,但是修改提交信息 + - edit 保留该commit,但是rebase时会暂停,允许你修改这个commit + - squash 保留该commit,将当前commit与上一个commit合并 + - fixup 与squash相同,但是不会保存当前commit的提交信息 + - exec 执行其他shell命令 + - drop 删除该commit + - squash 和 fixup 可以用来合并 commit。 + >例如用 squash 来合并, 我们只需要把要合并的 commit 前面的动词,改成 squash(或者 s)即可。 + +```s + pick 07c5abd Introduce OpenPGP and teach basic usage 2 s de9b1eb Fix PostChecker::Post#urls + s 3e7ee36 Hey kids, stop all the highlighting + pick fa20af3 git interactive rebase, squash, amend + + //合并成2条commit + # This is a combination of 3 commits. + # The first commit's message is: + Introduce OpenPGP and teach basic usage + + # This is the 2ndCommit Message: + Fix PostChecker::Post#urls + + # This is the 3rdCommit Message: + Hey kids, stop all the highlighting + +``` +**注意事项**: +* 删除某个 commit 行,则该 commit 会丢失掉。 +* 删除所有的 commit 行,则 rebase 会被终止掉。 +* 可以对 commits 进行排序,git 会从上到下进行合并。 + + +**步骤**: +1. `git rebase -i ` +2. 编辑交互界面,执行squash 操作,在每个提交前面增加 s。 +3. 看一下是否合并成功:`git log --oneline` +4. `git checkout master` +5. `git merge feature/user` 可以将 feature 分支 feature/user 的改动合并到主干分支,从而完成新 功能的开发。 +6. `git log --oneline` + + +如果有太多commit需要合并,可以不合并,撤销之前n次,然后再建一个新的。 +```s + $ git reset HEAD~3 + $gitadd. + $ git commit -am "feat(user): add user resource" +``` +>除了 commit 实在太多的时候,一般情况下我不建议用这种方法,有点粗 暴,而且之前提交的 Commit Message 都要重新整理一遍。 + + +**修改 Commit Message** + + +>遇到提交的 Commit Message 不 符合规范的情况,这个时候就需要我们能够修改之前某次 commit 的 Commit Message。 +有两种修改方法,分别对应两种不同情况: +1. git commit --amend:修改最近一次 commit 的 message; + 1. 在当前 Git 仓库下执行命令:git commit --amend,后会进入一个交互界面,在交互界 面中,修改最近一次的 Commit Message, +2. git rebase -i:修改某次 commit 的 message。 + 1. 先看当前分支日志记录:`git log --oneline` + 2. 指定想修改的记录上一条commit的 id 的 message。`git rebase -i 55892fa`, + 3. 使用 reword 或者 r,保留倒 数第二次的变更信息,但是修改其 message + > git rebase -i 会变更父 commit ID 之后所有提交的 commit ID。 + +>如果当前分支有未 commit 的代码,需要先执行 git stash 将工作状态进行暂存,当 修改完成后再执行 git stash pop 恢复之前的工作状态。 + + +### 目录结构设计:如何组织一个可维护、可扩展的代码目录? + +**如何规范目录?** + +>目录规范,通常是指我们的项目由哪些目录组成,每个目录下存放什么文件、实现什么功能,以及各个目录间的依赖关系是什么等。 + +- 命名清晰: 不长不短,最好用单数。 +- 功能明确: 当需要新增一个功能时,能够清楚知道放哪个目录。 +- 全面性: 尽可能全面地包含研发过程中需要的功能,例如文档、脚本、源码管理、API实现、工具、第三方包、测试、编译产物。 +- 可预测性: 能够在项目变大时,仍然保持之前的目录结构。 +- 可扩展性: 存同类功能,项目变大时,还可以存更多? **莫名其妙**,感觉像是再说,子目录不要取名字太宽泛,避免目录太深。 + + +根据功能,我们可以将目录结构分为结构化目录结构和平铺式目录结构两种。 +- 结构化目录一般用在Go应用中,相对复杂。 +- 平铺式目录一般用在Go包中,相对简单。 + - 引用路径长度明显减少 + + +应用目录结构分为3大部分:Go 应用 、项目管理和文档。 +- Go应用主要存放前后端代码 + - /web 前端代码,静态资源、服务端模板、单页应用 + - /cmd 组件 + - 每个组件的目录名应该跟你期望的可执行文件名是一致的。 + - 这里要保证 /cmd/<组件名> 目 录下不要存放太多的代码。 + - cmd//main.go + - /internal 私有应用和库代码,不希望在其他应用和库中被导入的代码。 + - 可以通过 Go 语言本身的机制来约束其他项目 import 项目内部的包。 + - /internal/apiserver: 该目录中存放真实的应用代码。 + - /internal/pkg : 存放项目内可共享,项目外不共享的包。 校验代码、code码 + - 建议:一开始将所有的共享代码存放在 /internal/pkg 目录下,当该共享代码做 好了对外开发的准备后,再转存到/pkg目录下。 + - /pkg : 存放可以被外部应用使用的代码库,其他项目可以直接通过 import 导入这里的代码。 + - /vendor 项目依赖,可通过 go mod vendor 创建。需要注意的是,如果是一个 Go 库,不要提交 vendor 依赖包。 + - /third_party 外部帮助工具,分支代码或其他第三方应用(例如 Swagger UI) + - 比如我们 fork 了一个 第三方 go 包,并做了一些小的改动,我们可以放在目录 /third_party/forked 下。 + - /test 用于存放其他外部测试应用和测试数据。 + - Go 也会忽略以“.”或 “_” 开头的目录或文件。这样在命名测试数据目 录方面,可以具有更大的灵活性。 + - /configs 配置文件模板或默认配置,敏感信息用占位符取代,不要放在配置代码中。 + - /deployments 用来存放 Iaas、PaaS 系统和容器编排部署配置和模板 + - /init 存放初始化系统(systemd,upstart,sysv)和进程管理配置文件(runit, supervisord)。比如 sysemd 的 unit 文件。这类文件,在非容器化部署的项目中会用到。 +- 项目管理类 + - /Makefile 一个很老的项目管理工具,通 常用来执行静态代码检查、单元测试、编译等功能。 + - 执行顺序建议: 首先生成代码 gen -> format -> lint -> test -> build。 + - 在实际开发中,我们可以将一些重复性的工作自动化,并添加到 Makefile 文件中统一管 理。 + - /script 存放脚本文件,实现构建、安装、分析等不同功能。 + - /scripts/make-rules:用来存放 makefile 文件,实现 /Makefile 文件中的各个功能。 Makefile 有很多功能. + - /scripts/lib:shell 库,用来存放 shell 脚本。 + - shell 脚本中的函数名,建议采用语义化的命名方式,例如 iam::log::info 这种 语义化的命名方式,可以使调用者轻松的辨别出函数的功能类别,便于函数的管理和引 用。 + - /scripts/install:如果项目支持自动化部署,可以将自动化部署脚本放在此目录下。 + - /build 存放安装包和持续集成相关的文件 + - /build/package:存放容器(Docker)、系统(deb, rpm, pkg)的包配置和脚本。 + - /build/ci:存放 CI(travis,circle,drone)的配置文件和脚本。 + - /build/docker:存放子项目各个组件的 Dockerfile 文件。 + - /tools 存放这个项目的支持工具。这些工具可导入来自 /pkg 和 /internal 目录的代码. + - /assets 其他资源 (图片、CSS、JavaScript 等)。 + - /website 如果你不使用 GitHub 页面,那么可以在这里放置项目网站相关的数据。 +- 文档 + - /README.md 包含了项目的介绍、功能、快速安装和使用指引、详细的文档链 接以及开发指引等。 + - 过长需要跳转,需要 添加 markdown toc 索引,可以借助工具  tocenize 来完成索引的添加。 + - /docs 存放设计文档、开发文档和用户文档等(除了 godoc 生成的文档)。 + - /docs/devel/{en-US,zh-CN}:存放开发文档、hack 文档等。 + - /docs/guide/{en-US,zh-CN}: 存放用户手册,安装、quickstart、产品文档等,分为中 文文档和英文文档。 + - /docs/images:存放图片文件。 + - /api 目录中存放的是当前项目对外提供的各种不同类型的 API 接口定义文件. + - 其中可能包含类似 /api/protobuf-spec、/api/thrift-spec、/api/http-spec、 openapi、swagger 的目录. + - /CONTRIBUTING.md 开源贡献说明 + - /LICENSE + - 常用的开源协议有:Apache 2.0、MIT、 BSD、GPL、Mozilla、LGPL。 + - 可自动化生成,推荐工具:  addlicense 。 + - /CHANGELOG 为了方便了解当前版本的更新内容或者历史更新内容,需要将更新记录 存放到 CHANGELOG 目录 + - /examples 存放应用程序或者公共包的示例代码。 + + +**一些建议** +- 对于小型项目, 可以考虑先包含 cmd、pkg、internal 3 个目录,其他目录后面按需创建。 +- 空目录无法提交到git,可以加一个 .keep 文件 +- utils, common这类目录不建议用,在Go项目中,每个包名字应该唯一、功能单一、明确。这 类目录存放了很杂的功能,后期维护、查找都很麻烦。 + +- GO 基于功能划分目录, DDD 基于模型划分目录,如何理解? + + +### 工作流设计: **如何设计合理的多人开发模式?** + +在使用 Git 开发时,有 4 种常用的工作流,也叫开发模式,按演进顺序分为集中式工作流、功能分支工作流、Git Flow 工作流和 Forking 工作流。 +- 集中式工作流: + - 都在主干开发。 + - 适合用在团队 人数少、开发不频繁、不需要同时维护多个版本的小项目中 +- 分支工作流: + - 在功能分支上进行开发,开发完再合并到主干。 不是版本号递增那种,而是自己取名字。 git checkout -b feature/rate-limiting + - `git merge --no-ff` : feature 分支上所有的 commit 都会加到 master 分支上,并且会生成一个 merge commit。 + - `git merge --squash` : 使该 pull request 上的所有 commit 都合并成一个 commit ,然后加到 master 分支上,但**原来的 commit 历史会丢失**。如果开发人员在 feature 分支上提交 的 commit 非常随意,没有规范,那么我们可以选择这种方法来丢弃无意义的 commit。 + - `git rebase`: 将 pull request 上的所有提交历史按照原有顺序依次添加到 master 分支的头部(HEAD)。不熟悉别用。 +- Git Flow 工作流 + - Git Flow 中定义了 5 种分支,分别是 master、develop、feature、release 和 hotfix。 其中,master 和 develop 为常驻分支,其他为非常驻分支,不同的研发阶段会用到不同 的分支。 + - 上手难度大,Git Flow 工作流的 每个分支分工明确,这可以最大程度减少它们之间的相互影响。。 + - 比较适合开发团队相对固定,规模较大的项目。 +- Forking 工作流 + - 适用于:开源项目、开发者有衍生出自己的衍生版的需求、开发者不固定。 + - fork到自己账号下,clone到本地,创建功能分支,开发commit,合并commit:git rebase -i origin/master,git rebase -i --autosquash 自动合并commit,push到主干。 + - 在个人远程仓库页面创建 pull request。创建 pull request 时,base 通常选择目标 远程仓库的 master 分支。 + +### 研发流程设计 + +#### 如何设计 Go 项目的开发流程? +待看,暂时不需要 + + +#### 如何管理应用的生命周期? +待看 + + +## 编码规范 + +### 设计方法:怎么写出优雅的 Go 项目? + +1. 为什么是 Go 项目,而不是 Go 应用? +>Go 项目是一个偏工程化的概念,不仅包含了 Go 应用,还包含了项目 管理和项目文档: + + +2. 一个优雅的 Go 项目具有哪些特点? +>不仅要求我们的 Go 应用是优雅的,还要确保我们的项目管理和文档也是优雅的。 +* 符合 Go 编码规范和最佳实践; +* 易阅读、易理解,易维护; +* 易测试、易扩展; +* 代码质量高。 + + +**编写高质量的 Go 应用** + + +做好5个方面: 代码结构、代码规范、代码质量、编程哲学和软件设计方法. + + +- **代码结构** + - 组织一个好的目录结构,看前面那讲。 + - 选择一个好的模块拆分方法。目的就是模块职责分明,高内聚低耦合 + - 按层拆分,比如MVC结构。 + - 问题:相同功能可能在不同层被使用到,而这些功能又分散在不同的层中,很容易造成循环引用。 + - 按功能拆分,比如把user、order、billing拆分为三个模块。 + - 好处1:不同模块,功能单一,可以实现高内聚低耦合的设计哲学。 + - 好处2:因为所有的功能只需要实现一次,引用逻辑清晰,会大大减少出现循环引用的概率。 +- **代码规范** + - 编码规范: 《Uber Go 语言编码规范》比较受欢迎 + - 静态代码检查工具: golangci-lint + - 最佳实践文章: + - 《Effective Go》: 高效 Go 编程,由 Golang 官方编写。 + - 《Go Code Review Comments》:Golang 官方编写的 Go 最佳实践,作为 Effective Go 的补充。 + - Style guideline for Go packages:包含了如何组织 Go 包、如何命名 Go 包、如何 写 Go 包文档的一些建议。 +- **代码质量** + - 写**单测**,mock + - 为了**提高代码的可测性**,降低单元测试的复杂度,对 function 和 mock 的要求是: + - 要尽可能减少 function 中的依赖,让 function 只依赖必要的模块。编写一个功能单 一、职责分明的函数,会有利于减少依赖。 + - 依赖模块应该是易 Mock 的。 + - 常用mock工具 + - golang/mock,是官方提供的 Mock 框架。它实现了基于 interface 的 Mock 功 能,能够与 Golang 内置的 testing 包做很好的集成 + - sqlmock,可以用来模拟数据库连接。 + - httpmock,可以用来 Mock HTTP 请求。 + - bouk/monkey,猴子补丁,能够通过替换函数指针的方式来修改任意函数的实现。 猴子补丁提供了单元测试 Mock 依赖的最终解决方案。 + - 定期检查单元测试覆盖率 + - `go test -race -cover -coverprofile=./coverage.out -timeout=10m -short -v ./...` + - `go tool cover -func ./coverage.out` + - 提高我们的单元测试覆盖率 + - 使用 gotests 工具自动生成单元测试代码,减少编写单元测试用例的工作量,将你从重 复的劳动中解放出来。 + + - **Code Review** + +编写高质量 Go 代码的- +- 外功: 组织一个合理的代码结构、编写符合 Go 代码规范的代码、保 证代码质量,在我看来都是。 +- 内功: 编程哲学和软件设计方法。 + + +**编程哲学** + +>面向接口编 程和面向“对象”编程。 + +Go 接口是一组方法的集合。 +任何类型,只要实现了该接口中的方法集,那么就属于这个类型,也称为实现了该接口。 +接口的作用,其实就是为不同层级的模块提供一个定义好的中间层。-这样,上游不再需要 依赖下游的具体实现,充分地对上下游进行了解耦。 + + +### Go常用设计模式概述 + + + +# 三、基础功能设计或开发 +>开发基础功能,如日志包、错误包、错误码 + +## API 风格 + +### 如何设计RESTful API? + + +### RPC API介绍 + + +## 项目管理:如何编写高质量的Makefile? + + +## 研发流程实战:IAM项目是如何进行研发流程管理的? + + +## 代码检查:如何进行静态代码检查? + +## API 文档:如何生成 Swagger API 文档 ? + + +## 错误处理 + +### 如何设计一套科学的错误码? + + +### 如何设计错误包? + + +## 日志处理 + +### 如何设计日志包并记录日志? + + +### 手把手教你从 0 编写一个日志包 + + +## 应用构建三剑客:Pflag、Viper、Cobra 核心功能介绍 + + +## 应用构建实战:如何构建一个优秀的企业应用框架? + +# 四、服务开发 +>解析一个企业级的 Go 项目代码,让你学会如何开发 Go 应用. 怎么设计和开发 API 服务、Go SDK、客户端工具 + + +# 五、服务测试 +>讲解单元测试、功能测试、性能分析和 性能调优的方法,最终让你交付一个性能和稳定性都经过充分测试的、生产级可用的服 务。 + + +# 六、服务部署 +>如何部署一个高可用、安 全、具备容灾能力,又可以轻松水平扩展的企业应用。 传统部署和容器化部署。 diff --git a/Java/.DS_Store b/Java/.DS_Store new file mode 100644 index 0000000..02142e9 Binary files /dev/null and b/Java/.DS_Store differ diff --git "a/Java/JVM/GC\346\227\245\345\277\227\350\247\202\346\221\251.md" "b/Java/JVM/GC\346\227\245\345\277\227\350\247\202\346\221\251.md" new file mode 100644 index 0000000..532fb36 --- /dev/null +++ "b/Java/JVM/GC\346\227\245\345\277\227\350\247\202\346\221\251.md" @@ -0,0 +1,128 @@ +# 3月14日 + +## 案例一: gc日志跟业务日志停止时间不匹配? 看gc日志的方法: 22764450 + + +gc日志不太对呀,gc时间跟业务日志停止时间段不太一样 +先看业务日志: + +```java + +[WARN ] 2022-03-14 14:53:23,305 [RMI TCP Connection(382177)-10.164.34.27] from ip:[10.164.34.27],RemoteInvocation: method name 'getCpcIdeaListByCpcGrpIdListAndStatusForSpecial'; parameter types [java.lang.Long, java.util.List, java.util.List, java.util.List],target:[com.sun.proxy.$Proxy343],loginUser null! +[INFO ] 2022-03-14 14:53:45,037 [org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean#0-3] Timer task, the program is running..... +[INFO ] 2022-03-14 14:53:45,039 [Thread-920] Clear timed cache +[INFO ] 2022-03-14 14:53:45,044 [resin-23-SendThread(10.160.14.154:2181)] Client session timed out, have not heard from server in 21824ms for sessionid 0x17ddcb5bc628138, closing socket connection and attempting reconnect +[INFO ] 2022-03-14 14:53:45,044 [resin-23-SendThread(10.160.14.154:2181)] Client session timed out, have not heard from server in 23539ms for sessionid 0x17ddcb5bc628134, closing socket connection and attempting reconnect +[INFO ] 2022-03-14 14:53:45,044 [resin-23-SendThread(10.160.14.166:2181)] Client session timed out, have not heard from server in 23558ms for sessionid 0x27ddcb5bc726940, closing socket connection and attempting reconnect +[WARN ] 2022-03-14 14:53:45,048 [RMI TCP Connection(381748)-10.160.185.53] from ip:[10.160.185.53],RemoteInvocation: method name 'getPlanById'; parameter types [java.lang.Long, java.lang.Long],target:[com.sun.proxy.$Proxy330],loginUser null! +[WARN ] 2022-03-14 14:53:45,057 [RMI TCP Connection(381793)-10.143.185.26] from ip:[10.143.185.26],RemoteInvocation: method name 'getAccountLevelDtoByAccountId'; parameter types [java.lang.Long],target:[com.sun.proxy.$Proxy333],loginUser null! +``` +可以看到,2022-03-14 14:53:23 ~ 2022-03-14 14:53:45 之间是没有业务日志的,也就是说属于STW时间 +再看gc日志: + +账号ID 22764450 部分full gc +apollo 10.164.128.210 2022-03-14 14:53:35 977 2022-03-14 14:54:03 493 + +```java +2022-03-14T14:53:15.236+0800: 318865.535: [GC (Allocation Failure) +2022-03-14T14:53:15.237+0800: 318865.536: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 6639472 bytes, 6639472 total +- age 2: 10362208 bytes, 17001680 total +- age 3: 7893584 bytes, 24895264 total +: 11630323K->189685K(12268352K), 5.3521316 secs] 19190776K->12536392K(40579904K), 5.3548955 secs] [Times: user=85.02 sys=0.00, real=5.36 secs] + +2022-03-14T14:53:23.324+0800: 318873.623: [GC (Allocation Failure) +2022-03-14T144:53:23.325+0800: 318873.624: [ParNew (promotion failed): 10753166K->10693429K(122 +268352K), 3.2176676 secs] +2022-03-14T14:53:26.543+0800: 318876.842: [CMS: 14130222 +6K->3745616K(28311552K), 18.4838208 secs] 23099874K->3745616K(40579904K), [Metass +pace: 139290K->139290K(167936K)], 21.7104200 secs] [Times: user=66.03 sys=0.00, +real=21.71 secs] + +2022-03-14T14:53:59.444+0800: 318909.743: [GC (Allocation Failure) +2022-03-14T144:53:59.445+0800: 318909.744: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 290037208 bytes, 290037208 total +: 10905216K->323809K(12268352K), 1.6613804 secs] 14650832K->5852947K(40579904K),, + 1.6645653 secs] [Times: user=25.89 sys=0.00, real=1.66 secs] + +2022-03-14T14:54:45.030+0800: 318955.329: [GC (Allocation Failure) +2022-03-14T144:54:45.034+0800: 318955.333: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 263349136 bytes, 263349136 total +- age 2: 206822944 bytes, 470172080 total +: 11229025K->559302K(12268352K), 0.3198510 secs] 16758163K->6088440K(40579904K), 0.3250050 secs] [Times: user=4.83 sys=0.07, real=0.32 secs] +``` + + + +## 案例二 业务日志跟gc日志也有误差,但时间类似。 耗时还有其他原因: 23222668 + +从下面业务日志看,耗时确实这么长 + +- 查库? + +- 看业务日志 14:17:36 直接跳到 14:17:44, 垃圾回收了8s? + +```java +[INFO ] 2022-03-14 14:17:47,705 [RMI TCP Connection(374801)-10.143.189.65] StopWatch 'getCpcKeyListPageByQueryAndAdType': running time (millis) = 23582 +----------------------------------------- +ms % Task name +----------------------------------------- +22045 093% endQueryCpcList +01529 006% QueryReport +00008 000% HandlePageInfo + +[INFO ] 2022-03-14 14:17:47,705 [RMI TCP Connection(374801)-10.143.189.65] 23222668 requestId=0 [Outer Call] com.sogou.bizdev.cpc.key.provider.CpcKeyProvider.getCpcListByQuery,args:[ Long:23222668 KeyQueryDto:{"accountId":23222668,"colorTags":[],"cpcGrpId":0,"cpcIds":[],"cpcPlanId":0,"filterStatus":0,"keyMatchTypes":[],"keyStatusList":[],"maxMaxPrice":0,"minMaxPrice":0,"mobileQualityDegrees":[],"orderBy":21,"pagedBean":{"curPageNum":1,"dataList":[],"isFirstPage":false,"isLastPage":false,"nextPage":0,"pageSize":50,"prePage":0,"startSerial":0,"totalPages":0,"totalRecNum":0},"pcQualityDegrees":[],"queryMatchType":0,"queryWord":"","statisticDataQueryDto":{"endDate":{"date":13,"day":0,"hours":0,"minutes":0,"month":2,"seconds":0,"time":1647100800000,"timezoneOffset":-480,"year":122},"materialType":0,"startDate":{"date":13,"day":0,"hours":0,"minutes":0,"month":2,"seconds":0,"time":1647100800000,"timezoneOffset":-480,"year":122},"timeType":6}} SearchAdUser:com.sogou.bizdev.bizlog.dto.SearchAdUser@3706962d],processing time: 23805 +``` + +- gc日志 + +```java +apollo 10.160.78.59 2022-03-14 14:17:23 659 + +```java +2022-03-14T14:17:04.176+0800: 316556.392: [GC (Allocation Failure) +2022-03-14T14:17:04.177+0800: 316556.392: [ParNew +Desired survivor size 697925632 bytes, new threshold 1 (max 6) +- age 1: 1146176680 bytes, 1146176680 total +: 12158181K->1151620K(12268352K), 1.5177358 secs] 27777349K->18933725K(40579904K), 1.5199294 secs] [Times: user=23.09 sys=0.81, real=1.52 secs] +2022-03-14T14:17:05.711+0800: 316557.926: [GC (CMS Initial Mark) [1 CMS-initial-mark: 17782104K(28311552K)] 18933935K(40579904K), 0.0123023 secs] [Times: user=0.06 sys=0.01, real=0.02 secs] +2022-03-14T14:17:05.725+0800: 316557.940: [CMS-concurrent-mark-start] +2022-03-14T14:17:06.225+0800: 316558.440: [CMS-concurrent-mark: 0.485/0.500 secs] [Times: user=5.19 sys=0.37, real=0.50 secs] +2022-03-14T14:17:06.226+0800: 316558.441: [CMS-concurrent-preclean-start] +2022-03-14T14:17:06.273+0800: 316558.489: [CMS-concurrent-preclean: 0.047/0.047 secs] [Times: user=0.31 sys=0.04, real=0.05 secs] +2022-03-14T14:17:06.274+0800: 316558.489: [CMS-concurrent-abortable-preclean-start] + CMS: abort preclean due to time 2022-03-14T14:17:11.288+0800: 316563.504: [CMS-concurrent-abortable-preclean: 4.983/5.014 secs] [Times: user=26.29 sys=1.40, real=5.01 secs] +2022-03-14T14:17:11.303+0800: 316563.518: [GC (CMS Final Remark) [YG occupancy: 6327134 K (12268352 K)]2022-03-14T14:17:11.303+0800: 316563.518: [GC (CMS Final Remark) 2022-03-14T14:17:11.304+0800: 316563.519: [ParNew (promotion failed): 6327134K->5311815K(12268352K), 1.0276246 secs] 24109239K->23094088K(40579904K), 1.0297185 secs] [Times: user=2.12 sys=0.12, real=1.03 secs] +2022-03-14T14:17:12.333+0800: 316564.549: [Rescan (parallel) , 1.5637997 secs]2022-03-14T14:17:13.897+0800: 316566.113: [weak refs processing, 0.0079816 secs]2022-03-14T14:17:13.905+0800: 316566.121: [class unloading, 0.0930767 secs]2022-03-14T14:17:13.998+0800: 316566.214: [scrub symbol table, 0.0218572 secs]2022-03-14T14:17:14.020+0800: 316566.236: [scrub string table, 0.0022893 secs][1 CMS-remark: 17782272K(28311552K)] 23094088K(40579904K), 2.7495797 secs] [Times: user=26.68 sys=0.37, real=2.75 secs] +2022-03-14T14:17:14.054+0800: 316566.270: [CMS-concurrent-sweep-start] +2022-03-14T14:17:36.993+0800: 316589.209: [CMS-concurrent-sweep: 17.895/22.939 secs] [Times: user=83.75 sys=17.18, real=22.94 secs] +2022-03-14T14:17:37.040+0800: 316589.255: [GC (Allocation Failure) 2022-03-14T14:17:37.041+0800: 316589.257: [ParNew: 11041517K->11041517K(12268352K), 0.0000416 secs]2022-03-14T14:17:37.041+0800: 316589.257: [CMS: 2561685K->2062560K(28311552K), 7.8205440 secs] 13603202K->2062560K(40579904K), [Metaspace: 135050K->135050K(180224K)], 7.8238814 secs] [Times: user=6.35 sys=1.49, real=7.83 secs] +2022-03-14T14:17:49.565+0800: 316601.780: [GC (Allocation Failure) 2022-03-14T14:17:49.566+0800: 316601.781: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 340847128 bytes, 340847128 total +: 10905216K->366409K(12268352K), 0.1260961 secs] 12967776K->2428970K(40579904K), 0.1287021 secs] [Times: user=1.88 sys=0.11, real=0.13 secs] +``` + + +## kuaitou-api 老年代剩余空间还有很多,但是却进行了一次CMS + +```log +2022-04-07T10:09:14.849+0800: 45507.697: [GC (Allocation Failure) 2022-04-07T10:09:14.849+0800: 45507.697: [ParNew: 7746217K->4979K(8709120K), 0.1099053 secs] 8049995K->308967K(24837120K), 0.1102290 secs] [Times: user=0.29 sys=0.03, real=0.11 secs] +2022-04-07T10:09:32.954+0800: 45525.802: [GC (CMS Initial Mark) [1 CMS-initial-mark: 303987K(16128000K)] 4357022K(24837120K), 0.1989679 secs] [Times: user=0.39 sys=0.01, real=0.20 secs] +2022-04-07T10:09:33.153+0800: 45526.001: [CMS-concurrent-mark-start] +2022-04-07T10:09:33.364+0800: 45526.212: [CMS-concurrent-mark: 0.211/0.211 secs] [Times: user=0.52 sys=0.00, real=0.21 secs] +2022-04-07T10:09:33.364+0800: 45526.212: [CMS-concurrent-preclean-start] +2022-04-07T10:09:33.402+0800: 45526.250: [CMS-concurrent-preclean: 0.038/0.038 secs] [Times: user=0.03 sys=0.00, real=0.04 secs] +2022-04-07T10:09:33.402+0800: 45526.250: [CMS-concurrent-abortable-preclean-start] + CMS: abort preclean due to time 2022-04-07T10:09:38.490+0800: 45531.338: [CMS-concurrent-abortable-preclean: 2.040/5.088 secs] [Times: user=2.06 sys=0.01, real=5.09 secs] +2022-04-07T10:09:38.491+0800: 45531.339: [GC (CMS Final Remark) [YG occupancy: 4055296 K (8709120 K)]2022-04-07T10:09:38.491+0800: 45531.339: [Rescan (parallel) , 0.1663928 secs]2022-04-07T10:09:38.658+0800: 45531.506: [weak refs processing, 0.1447850 secs]2022-04-07T10:09:38.802+0800: 45531.650: [class unloading, 0.0843129 secs]2022-04-07T10:09:38.887+0800: 45531.735: [scrub symbol table, 0.0155863 secs]2022-04-07T10:09:38.902+0800: 45531.750: [scrub string table, 0.0028043 secs][1 CMS-remark: 303987K(16128000K)] 4359283K(24837120K), 0.4435910 secs] [Times: user=0.70 sys=0.03, real=0.44 secs] +2022-04-07T10:09:38.935+0800: 45531.783: [CMS-concurrent-sweep-start] +2022-04-07T10:09:39.099+0800: 45531.947: [CMS-concurrent-sweep: 0.142/0.164 secs] [Times: user=0.18 sys=0.00, real=0.17 secs] +2022-04-07T10:09:39.099+0800: 45531.947: [CMS-concurrent-reset-start] +2022-04-07T10:09:39.138+0800: 45531.986: [CMS-concurrent-reset: 0.039/0.039 secs] [Times: user=0.04 sys=0.00, real=0.04 secs] +2022-04-07T10:09:59.501+0800: 45552.349: [GC (Allocation Failure) 2022-04-07T10:09:59.501+0800: 45552.349: [ParNew: 7746419K->6175K(8709120K), 0.0638041 secs] 8005409K->265480K(24837120K), 0.0641397 secs] [Times: user=0.32 sys=0.01, real=0.06 secs] +2022-04-07T10:10:37.603+0800: 45590.451: [GC (Allocation Failure) 2022-04-07T10:10:37.604+0800: 45590.452: [ParNew: 7747615K->5611K(8709120K), 0.0607111 secs] 8006920K->265024K(24837120K), 0.0610551 secs] [Times: user=0.30 sys=0.01, real=0.06 secs] +``` \ No newline at end of file diff --git "a/Java/JVM/GC\347\232\204\344\272\214\344\270\211\344\272\213.md" "b/Java/JVM/GC\347\232\204\344\272\214\344\270\211\344\272\213.md" new file mode 100644 index 0000000..0b2fb62 --- /dev/null +++ "b/Java/JVM/GC\347\232\204\344\272\214\344\270\211\344\272\213.md" @@ -0,0 +1,344 @@ + +# 收集器 + +## CMS +### 遇到问题:CMS收集过程和日志分析 +垃圾回收新生代和老年代的垃圾收集器组合: ParNew and CMS + +mark-sweep分为多个阶段,其中一大部分阶段GC的工作是和Application threads的工作同时进行的(当然,gc线程会和用户线程竞争CPU的时间),默认的GC的工作线程为你服务器物理CPU核数的1/4; + +>当你的服务器是多核同时你的目标是低延时,那该GC的搭配则是你的不二选择。 + +#### 什么是CMS +"Concurrent Mark and Sweep" 是CMS的全称,官方给予的名称是:“Mostly Concurrent Mark and Sweep Garbage Collector”; +年轻代:采用 stop-the-world [mark-copy](https://plumbr.eu/handbook/garbage-collection-algorithms/removing-unused-objects/copy) 算法; +年老代:采用 Mostly Concurrent [mark-sweep](https://plumbr.eu/handbook/garbage-collection-algorithms/removing-unused-objects/sweep) 算法; +设计目标:年老代收集的时候避免长时间的暂停; + +CMS:在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。是HotSpot虚拟机中第一款真正意义上 +CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作(一个面向高吞吐,一个面向低延时,而且G1这些不在是分代设计),所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它 + +#### Par New +自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加SerialOld以及Serial加CMS这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了-XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部分 + +#### 日志初体验 + +##### Minor GC + +```log +2022-03-07T11:41:14.315+0800: 37.690: [GC (Allocation Failure) +2022-03-07T11:41:14.315+0800: 37.691: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 115229256 bytes, 115229256 total +- age 2: 47712552 bytes, 162941808 total +- age 3: 64836536 bytes, 227778344 total +: 11387196K->434763K(12268352K), 0.1768324 secs] 11387196K->434763K(40579904K), 0.1777042 secs] [Times: user=1.60 sys=0.28, real=0.18 secs] + +``` +* 2022-03-07T11:41:14.315+0800 – GC发生的时间; +* 37.690 – GC开始,相对JVM启动的相对时间,单位是秒; +* GC – 区别MinorGC和FullGC的标识,这次代表的是MinorGC; +* Allocation Failure – **MinorGC的原因**,在这个case里边,**由于年轻代不满足申请的空间,**因此触发了MinorGC; +* ParNew – 收集器的名称,它预示了年轻代使用一个**并行的** mark-copy stop-the-world 垃圾收集器; +* Desired survivor size 697925632 bytes, new threshold 6 (max 6) - +* age 1: 115229256 bytes, 115229256 total +* 11387196K->434763K – 收集前后**年轻代的**使用情况; +* (12268352K) – **整个年轻代的容量**; +* 0.1768324 secs – Duration for the collection w/o final cleanup. +* 11387196K->434763K – 收集前后整个**堆**的使用情况; +* (40579904K) – **整个堆的容量**; +* 0.1777042 secs – ParNew**收集器标记**和**复制年轻代活着的对象**所花费的时间(包括和老年代通信的开销、对象晋升到老年代时间、垃圾收集周期结束一些最后的清理对象等的花销); +* [Times: user=1.60 sys=0.28, real=0.18 secs] – GC事件在不同维度的耗时,具体的用英文解释起来更加合理: + * user – Total CPU time that was consumed by Garbage Collector threads during this collection + * sys – Time spent in OS calls or waiting for system event + * real – Clock time for which **your application was stopped**. With **Parallel GC** this number should be close to (**user time + system time) divided by the number of threads** used by the Garbage Collector. In this particular case 8 threads were used. Note that due to some activities not being parallelizable, it always exceeds the ratio by a certain amount. + +**分析一下对象晋级问题**: +收集前: 整个堆使用:11387196K, 年轻代使用:11387196K, 老年代使用:整个堆11387196K - 年轻代11387196K = 0? + - 整个老年代容量:(40579904K - 12268352K) 比率是 Eden 1: Survivor 2: Old 7 +收集后: 整个堆使用:434763K, 年轻代使用:434763K, 老年代使用:0 +收集清理了: 年轻代使用:11387196K - 年轻代使用434763K = + - 说明这次垃圾回收没有晋升到老年代的: 而且有434763K 对象的age + 1。 + + +#### Full GC? CMS不属于Full GC, 只收集老年代 + +```log +2022-03-07T11:41:38.496+0800: 61.871: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(28311552K)] 8020070K(40579904K), 0.1120113 secs] [Times: user=1.27 sys=0.00, real=0.11 secs] +2022-03-07T11:41:38.609+0800: 61.985: [CMS-concurrent-mark-start] +2022-03-07T11:41:38.618+0800: 61.994: [CMS-concurrent-mark: 0.009/0.009 secs] [Times: user=0.07 sys=0.02, real=0.01 secs] +2022-03-07T11:41:38.619+0800: 61.994: [CMS-concurrent-preclean-start] +2022-03-07T11:41:38.674+0800: 62.050: [CMS-concurrent-preclean: 0.055/0.055 secs] [Times: user=0.18 sys=0.03, real=0.05 secs] +2022-03-07T11:41:38.679+0800: 62.054: [CMS-concurrent-abortable-preclean-start] + CMS: abort preclean due to time +2022-03-07T11:41:43.723+0800: 67.098: [CMS-concurrent-abortable-preclean: 4.580/5.044 secs] [Times: user=10.26 sys=2.00, real=5.04 secs] +2022-03-07T11:41:43.730+0800: 67.105: [GC (CMS Final Remark) [YG occupancy: 9955998 K (12268352 K)] +2022-03-07T11:41:43.730+0800: 67.106: [GC (CMS Final Remark) +2022-03-07T11:41:43.731+0800: 67.107: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 108073056 bytes, 108073056 total +- age 2: 93901384 bytes, 201974440 total +- age 3: 47244104 bytes, 249218544 total +- age 4: 64773416 bytes, 313991960 total +: 9955998K->404612K(12268352K), 0.0912009 secs] 9955998K->404612K(40579904K), 0.0931721 secs] [Times: user=1.24 sys=0.01, real=0.09 secs] +2022-03-07T11:41:43.824+0800: 67.199: [Rescan (parallel) , 0.0196641 secs] +2022-03-07T11:41:43.844+0800: 67.219: [weak refs processing, 0.0081472 secs] +2022-03-07T11:41:43.852+0800: 67.227: [class unloading, 0.0277837 secs] +2022-03-07T11:41:43.880+0800: 67.255: [scrub symbol table, 0.0171561 secs] +2022-03-07T11:41:43.897+0800: 67.272: [scrub string table, 0.0013598 secs][1 CMS-remark: 0K(28311552K)] 404612K(40579904K), 0.1760652 secs] [Times: user=1.59 sys=0.02, real=0.17 secs] +2022-03-07T11:41:43.908+0800: 67.283: [CMS-concurrent-sweep-start] +2022-03-07T11:41:43.913+0800: 67.288: [CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] +2022-03-07T11:41:43.913+0800: 67.289: [CMS-concurrent-reset-start] +2022-03-07T11:41:44.055+0800: 67.430: [CMS-concurrent-reset: 0.125/0.141 secs] [Times: user=0.30 sys=0.01, real=0.14 secs] +``` + +**具体步骤**: +1. Initial Mark: **stop-the-world:需要暂停用户进程,需尽量避免、缩短,简称STW** + - 目标 + - 标记老年代中所有的GC Root + - 标记被年轻代中活着的对象引用的对象 + - 日志分析: + - 日志:`2022-03-07T11:41:38.496+0800: 61.871: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(28311552K)] 8020070K(40579904K), 0.1120113 secs] [Times: user=1.27 sys=0.00, real=0.11 secs]` + - GC时间开始时间: 相对于JVM启动时间的相对时间: + - [收集阶段, 开始收集所有的GC Roots和直接引用到的对象] + - 0K(28311552K) : 老年代当前使用情况(老年代容量) + - 8020070K(40579904K) : 整个堆当前使用情况(整个堆的容量) + - 0.1120113 secs : 时间? + - [Times: user=1.27 sys=0.00, real=0.11 secs] 同上解释过了 + + +2. Concurrent Mark + - 目标: + - 遍历整个老年代并且标记所有存活对象, 从上一步找到的GC Roots开始。 + - **并发标记**的特点是和应用程序线程同时运行 + - 并不是老年代的多有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等 + - 日志分析: + - 日志: + ```log + 2022-03-07T11:41:38.609+0800: 61.985: [CMS-concurrent-mark-start] + 2022-03-07T11:41:38.618+0800: 61.994: [CMS-concurrent-mark: 0.009/0.009 secs] [Times: user=0.07 sys=0.02, real=0.01 secs] + ``` + - 开始, 该阶段会遍历整个老年代并且标记活着的对象。 + - 该阶段持续的时间 + + +3. Concurrent Preclean + - 目标:前一个阶段在并行运行时,一些对象的引用已经发生了变化,当这些应用发生辩护的时候,JVM会标记堆的这个区域为 Dirty Card(包含被标记且改变了的对象,卡表?) + - 此阶段那些能够从Dirty Card对象到达的对象也会被标记,标记完后dirty标记就会被清楚。 + - 日志分析: + - 日志: + ```log + 2022-03-07T11:41:38.619+0800: 61.994: [CMS-concurrent-preclean-start] + 2022-03-07T11:41:38.674+0800: 62.050: [CMS-concurrent-preclean: 0.055/0.055 secs] [Times: user=0.18 sys=0.03, real=0.05 secs] + ``` + - 这个阶段:负责标记前一个阶段标记后又发送改变的对象。 + - 其他同上 + + +4. Concurrent Abortable Preclean 可终止的并发预清理 + - 目标: + - 尝试着去承担STW的Final Remark阶段足够多的工作。 + - 此阶段持续的时间依赖很多因素:由于这个阶段是重复的做相同的事情,知道发生aboart的条件之一才会停止(比如:重复次数、多少工作量、持续的时间等条件)。 + - 日志分析: + - 日志: + ```log + 2022-03-07T11:41:38.679+0800: 62.054: [CMS-concurrent-abortable-preclean-start]CMS: abort preclean due to time + 2022-03-07T11:41:43.723+0800: 67.098: [CMS-concurrent-abortable-preclean: 4.580/5.044 secs] [Times: user=10.26 sys=2.00, real=5.04 secs] secs] + ``` + - 可终止的并发预清理,主要目的是试图尽可能缩短下一步Final Remark的时间(需要STW) + - 4.580/5.044 secs 默认情况下,此阶段最长可持续5s + - 其他同上 + + +5. Final Remark: **需要STW** + - 目标: + - 标记整个老年代的所有存活对象 + - 由于之前的预处理是并发的,它可能跟不上应用程序改变的速度,这个时候就需要STW来完成最后的校准工作。 + - 通常CMS尽量在年轻代足够干净的时候进入Final Remark阶段,目的是消除紧接着的连续多个STW阶段? + - 日志分析: + - 日志: + ```log + 2022-03-07T11:41:43.730+0800: 67.105: [GC (CMS Final Remark) [YG occupancy: 9955998 K (12268352 K)] + 2022-03-07T11:41:43.730+0800: 67.106: [GC (CMS Final Remark) + + 2022-03-07T11:41:43.731+0800: 67.107: [ParNew + Desired survivor size 697925632 bytes, new threshold 6 (max 6) + - age 1: 108073056 bytes, 108073056 total + - age 2: 93901384 bytes, 201974440 total + - age 3: 47244104 bytes, 249218544 total + - age 4: 64773416 bytes, 313991960 total + : 9955998K->404612K(12268352K), 0.0912009 secs] 9955998K->404612K(40579904K), 0.0931721 secs] [Times: user=1.24 sys=0.01, real=0.09 secs] + 2022-03-07T11:41:43.824+0800: 67.199: [Rescan (parallel) , 0.0196641 secs] + 2022-03-07T11:41:43.844+0800: 67.219: [weak refs processing, 0.0081472 secs] + 2022-03-07T11:41:43.852+0800: 67.227: [class unloading, 0.0277837 secs] + 2022-03-07T11:41:43.880+0800: 67.255: [scrub symbol table, 0.0171561 secs] + 2022-03-07T11:41:43.897+0800: 67.272: [scrub string table, 0.0013598 secs][1 CMS-remark: 0K(28311552K)] 404612K(40579904K), 0.1760652 secs] [Times: user=1.59 sys=0.02, real=0.17 secs] + ``` + - 收集阶段,这个阶段会标记老年代所有存活对象,包括那些在并发标记阶段更改的或者新建的引用对象 + - [YG occupancy: 9955998 K (12268352 K)] : 年轻代当前占用情况和容量 + - 在停止应用之前,先清理一下年轻代,因为配了: XX:+CMSScavengeBeforeRemark + - [Rescan (parallel) , 0.0196641 secs] : 这个阶段在**应用停止的阶段**完成存活对象的标记工作,这一步会扫描年轻代 + - [weak refs processing, 0.0081472 secs] : 子阶段一, 处理弱引用 + - [class unloading, 0.0277837 secs] : 子阶段二, 卸载那些不适用的类 + - [scrub symbol table, 0.0171561 secs] : 子阶段三, 清理symbol table + - [scrub string table, 0.0013598 secs] : 子阶段四,that is cleaning up symbol and string tables which hold class-level metadata and internalized string respectively + - [1 CMS-remark: 0K(28311552K)] : 此阶段后 老年代使用内存大小和容量 + - 404612K(40579904K), 0.1760652 secs] : 此阶段后整个堆使用内存大小和容量 + - 其他时间同上 + + +>通过以上五个阶段的标记,老年代所有存活的对象已经被标记,并且现在要通过垃圾回收采用清扫的方式,回收哪些不在使用的对象了。 + + +6. Concurrent Sweep + - 目标: + - 并发移除那些不用的对象,回收他们占用的空间供未来使用。 + - 日志分析: + - 日志: + ```log + 2022-03-07T11:41:43.908+0800: 67.283: [CMS-concurrent-sweep-start] + 2022-03-07T11:41:43.913+0800: 67.288: [CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] + ``` + - 同上 + + +7. Concurrent Reset + - 目标: + - 重新设置CMS算法内部的数据结构,准备下一个CMS生命周期使用 + - 日志分析: + - 日志: + ```log + 2022-03-07T11:41:43.913+0800: 67.289: [CMS-concurrent-reset-start] + 2022-03-07T11:41:44.055+0800: 67.430: [CMS-concurrent-reset: 0.125/0.141 secs] [Times: user=0.30 sys=0.01, real=0.14 secs] + ``` + - 同上 + +### CMS优化建议 + +1. 一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:`-XX:+CMSScavengeBeforeRemark` + - 用来开启或关闭在 CMS-remark 阶段之前的清除(Young GC)尝试。如果开启,在CMS开始前,会进行一次年轻代的清理,也就是为啥我看很多CMS的remark前都有一次ParNew(并行清理年轻代垃圾的收集器) + - 为啥提前清理年轻代,可以减少CMS的remark阶段? + >由于 YoungGen 存在引用 OldGen 对象的情况,因此 CMS-remark 阶段会将 YoungGen 作为 OldGen 的 “GC ROOTS” 进行扫描,防止回收了不该回收的对象。而配置 -XX:+CMSScavengeBeforeRemark 参数,在 CMS GC 的 CMS-remark 阶段开始前先进行一次 Young GC,有利于减少 Young Gen 对 Old Gen 的无效引用,降低 CMS-remark 阶段的时间开销。 + + +2. CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,**会造成内存碎片**,这时候我们需要用到这个参数:`XX:CMSFullGCsBeforeCompaction=n` + - CMS GC要决定是否在full GC时做压缩,会依赖几个条件,下面三种条件的**任意一种**成立都会让CMS决定这次做full GC时**要做压缩**。 + 1. UseCMSCompactAtFullCollection 与 `CMSFullGCsBeforeCompaction` 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。 + 2. 用户调用了System.gc(),而且DisableExplicitGC没有开启。 + 3. young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。 + + +3. 执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足。 + - 产生可能原因: + - CMS还没有机会回收老年带产生的, + - 或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生concurrent mode failure。 + -  确定发生concurrent mode failure的原因是因为碎片造成的,还是Eden区有大对象直接晋升老年代造成的? + - **优化方法**:一般有大量的对象晋升老年代容易导致这个错,有优化空间,要保证大部分对象尽肯能的再新生代gc掉。 + - **经验判断**:在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于**老年代有足够的空闲空间,但是由于碎片较多**,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的promotion failed。 + + +4. 过早提升与提升失败 + +- 过早提升(Premature Promotion):在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代。 + - 这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 + - **早提升的原因**: + 1. Survivor空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的full gc; + 2. 对象太大,Survivor和Eden没有足够大的空间来存放这些大象; +- 提升失败(Promotion Failure):如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆。 + - **提升失败原因** + - 当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。为什么是没有足够的连续空间而不是空闲空间呢?老年代容纳不下提升的对象有两种情况,多数情况是后者: + 1. 老年代空闲空间不够用了; + 2. 老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象; + - 解决方法 + 1. 如果是因为内存碎片导致的大对象提升失败,cms需要进行空间整理压缩; + 2. 如果是因为提升过快导致的,说明Survivor空闲空间不足,那么可以尝试调大Survivor; + 3. 如果是因为**老年代空间不够导致的,尝试将CMS触发的阈值调低**; + + +5. 导致回收停顿时间变长原因 + +>linux使用了swap,内存换入换出(vmstat),尤其是开启了大内存页的时候,因为swap只支持4k的内存页,大内存页的大小为2M,大内存页在swap的交换的时候需要先将swap中4k内存页合并成一个大内存页再放入内存或将大内存页切分为4k的内存页放入swap,合并和切分的操作会导致操作系统占用cup飙高,用户cpu占用反而很低;除了swap交换外,网络io(netstat)、磁盘I/O (iostat)在 GC 过程中发生会使 GC 时间变长。 + +如果是以上原因,就要去查看gc日志中的Times耗时: + +`[Times: user=0.00 sys=0.00, real=0.00 secs]` + +- user是用户线程占用的时间,sys是系统线程占用的时间,如果是io导致的问题,会有两种情况 + + 1. user与sys时间都非常小,但是real却很长,如下:`[ Times: user=0.51 sys=0.10, real=5.00 secs ]` + >user+sys的时间远远小于real的值,这种情况说明停顿的时间并不是消耗在cup执行上了,不是cup肯定就是io导致的了,所以这时候要去检查系统的io情况。 + 2. sys时间很长,user时间很短,real几乎等于sys的时间,如下:`[ Times: user=0.11 sys=31.10, real=33.12 secs ]` + >这时候其中一种原因是开启了大内存页,还开启了swap,大内存进行swap交换时会有这种现象; + + +6. 增加线程数 + +- CMS默认启动的**回收线程数目**是 (ParallelGCThreads + 3)/4) ,这里的ParallelGCThreads是**年轻代的并行收集**线程数; + + - 年轻代的并行收集线程数默认是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8); + - 如果要直接设定CMS回收线程数,可以通过`-XX:ParallelCMSThreads=n`,注意这个**n不能超过cpu线程数**,需要注意的是增加gc线程数,就会和应用争抢资源 + + +CMS并发GC不是“full GC”。 + +HotSpot VM里对concurrent collection和full collection有明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关的,**cms收集算法只是清理老年代**。 + + +#### 参考文章 + +- [CMS日志分析](https://www.cnblogs.com/zhangxiaoguang/p/5792468.html) +- 《深入理解Java虚拟机-JVM高级特性与最佳实践》第三版 +- [简书-爱吃糖果的:CMS日志分析](https://www.jianshu.com/p/03fac9502311) + + +# GC算法 + +## 基础常识 + +### 如何判断哪些对象是需要回收的 + +#### 引用计数法 + +现在已经不用了 + +#### 可达性分析法 +通过GC Roots的对象作为起始点,向下搜索,无法到达的就认为这些对象不可用。 + +**什么是GC Roots**: +- 一般都是哪些**堆外指向对内**的引用 + - JVM栈中引用的对象 + - 方法去中静态属性引用的对象 + - 方法区中常量引用的对象 + - 本地方法栈中引用的对象 + + +所有引用类型,都是抽象类 `java.lang.ref.Reference` 的子类,你可能注意到它提供了 get() 方法: + + +除了幻象引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法 获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态! +>对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以保证处于弱引用状态的对象,没有改变为强引用。 + + +如果我们错误的保持了强引用(比如,赋值给了 static 变量),那么对象可能就没 有机会变回类似弱引用的可达性状态了,就会产生内存泄漏。 + +#### 强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么? +不同的引用类型,主要体现的是对象不同的**可达性**(reachable)状态和**对垃圾收集的影响**。 + +**强引用**:就是我们**最常见的普通对象引用**,对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋 值为 null,就是可以被垃圾收集的了。 + + +**软引用**:相对强引用弱一点的引用,可以让对象豁免一些垃圾 收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。 + + +**弱引用**:并不能使对象豁免垃圾收集,仅仅是**提供一种访问**在弱引用状态下对象的**途径**,可以用来构建一种没有特定约束的关系。 + >比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多**缓存实现的选择**。 + + +**虚引用(幻像引用)**:**不能**通过它访问对象,仅仅是提供了一种**确保**对象被`finalize`以后,**做某些事情的机制**. + >通常用来做所谓的 Post-Mortem 清理机制,Java平台自身Cleaner机制等,也有人利用幻象引用监控对象的创建和销毁。 + + +**了解这个的作用**: +- 很少直接操作各种引用,考察对基础概念的理解,也考察对底层对象生命周期、垃圾回收机制的掌握。 +- 对于设计可靠缓存框架或者诊断应用OOM等问题,也会有帮助。 +>比如,诊断 MySQL connector-j 驱动在特定模式下(useCompression=true) 的内存泄漏问题,就需要我们理解怎么排查幻象引用的堆积问题 \ No newline at end of file diff --git "a/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.html" "b/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.html" new file mode 100644 index 0000000..24b87c6 --- /dev/null +++ "b/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.html" @@ -0,0 +1,320 @@ + + GC监控设计 + + + + + + + + + + + + + + + + +
+

GC监控指标(主要关注STW的几个时间)

+
    +
  • ParNew:出现ParNew后,紧跟着的Time里的real时间。 如[Times: user=1.24 sys=0.01, real=0.09 secs] 的0.09s。
  • +
+
2022-03-07T11:41:43.731+0800: 67.107: [ParNew
+Desired survivor size 697925632 bytes, new threshold 6 (max 6)
+- age   1:  108073056 bytes,  108073056 total
+- age   2:   93901384 bytes,  201974440 total
+- age   3:   47244104 bytes,  249218544 total
+- age   4:   64773416 bytes,  313991960 total
+: 9955998K->404612K(12268352K), 0.0912009 secs] 9955998K->404612K(40579904K), 0.0931721 secs] [Times: user=1.24 sys=0.01, real=0.09 secs]
+
    +
  • CMS 关注两个STW的步骤 +
      +
    • +

      CMS Initial Mark: 一般比较短,主要识别CMS Initial Mark,同上,也是找紧跟其后的 real时间

      +
      [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(16128000K)] 2111086K(24837120K), 0.2589686 secs] [Times: user=0.33 sys=0.12, real=0.26 secs]
      +
    • +
    • +

      CMS Final Remark: 也是获取紧跟其后的real时间, 目前发现两种情况,一种简单的(如下第一条)。一种中间会包含一次ParNew(这里各记录各的,一个时间寄到两个类型就行。)

      +
      2022-03-29T19:11:11.228+0800: 17.707: [GC (CMS Final Remark) [YG occupancy: 6657732 K (8709120 K)]2022-03-29T19:11:11.228+0800: 17.707: [Rescan (parallel) , 0.3269928 secs]2022-03-29T19:11:11.555+0800: 18.034: [weak refs processing, 0.0004249 secs]2022-03-29T19:11:11.556+0800: 18.035: [class unloading, 0.0064031 secs]2022-03-29T19:11:11.562+0800: 18.041: [scrub symbol table, 0.0612921 secs]2022-03-29T19:11:11.624+0800: 18.102: [scrub string table, 0.0007930 secs][1 CMS-remark: 0K(16128000K)] 6657732K(24837120K), 0.3975450 secs] [Times: user=0.76 sys=0.01, real=0.40 secs]
      +
      +
      +2022-03-14T14:17:11.303+0800: 316563.518: [GC (CMS Final Remark) [YG occupancy: 6327134 K (12268352 K)]
      +2022-03-14T14:17:11.303+0800: 316563.518: [GC (CMS Final Remark) 2022-03-14T14:17:11.304+0800: 316563.519: [ParNew (promotion failed): 6327134K->5311815K(12268352K), 1.0276246 secs] 24109239K->23094088K(40579904K), 1.0297185 secs] [Times: user=2.12 sys=0.12, real=1.03 secs]
      +
    • +
    +
  • +
+

上面说的总共记录成2个指标: ParNew、CMS(两类加起来), 如果这两个指标在一分钟内持续超过3s就报警(暂定这个时间,后续可能调整)

+

下面比较严重的GC(包含full GC),只要出现,不管耗时长短(一般都不会太短),都报警。

+
    +
  • Full GC : 出现[Full GC 这个词就记录后面的real时间。
  • +
  • promotion failed: 一般形如[ParNew (promotion failed) ,出现关键词 promotion failed 就记录后面的real,记录成此类型。(ParNew也可能重复记录,先不用去重)
  • +
  • concurrent mode failure: 出现关键词 concurrent mode failure 就记录后面的real。
  • +
+
+

解释后面两种情况:

+
+
    +
  • young gc的时候,把eden和survivor里的都还存活的对象,统一移到另一个survivor区中时,发现装不下了,就需要把部分对象,放到老年代中去,结果老年代空间也不足,这种场景呢,叫做promotion failed
  • +
  • promotion failed的前提下,老年代恰好还正在full gc,那么就会有图1红框5中的字样提示,concurrent mode failure
  • +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git "a/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.md" "b/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.md" new file mode 100644 index 0000000..f802a87 --- /dev/null +++ "b/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.md" @@ -0,0 +1,50 @@ + + +## GC监控指标(主要关注STW的几个时间) + +### ParNew: + +出现ParNew后,紧跟着的Time里的real时间。 如`[Times: user=1.24 sys=0.01, real=0.09 secs]` 的0.09s。 +```java +2022-03-07T11:41:43.731+0800: 67.107: [ParNew +Desired survivor size 697925632 bytes, new threshold 6 (max 6) +- age 1: 108073056 bytes, 108073056 total +- age 2: 93901384 bytes, 201974440 total +- age 3: 47244104 bytes, 249218544 total +- age 4: 64773416 bytes, 313991960 total +: 9955998K->404612K(12268352K), 0.0912009 secs] 9955998K->404612K(40579904K), 0.0931721 secs] [Times: user=1.24 sys=0.01, real=0.09 secs] +``` + +### CMS 关注两个STW的步骤 +- **CMS Initial Mark**: 一般比较短,主要识别`CMS Initial Mark`,同上,也是找紧跟其后的 real时间 +```java +[GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(16128000K)] 2111086K(24837120K), 0.2589686 secs] [Times: user=0.33 sys=0.12, real=0.26 secs] +``` + +- **CMS Final Remark**: 也是获取紧跟其后的real时间, 目前发现两种情况,一种简单的(如下第一条)。一种中间会包含一次ParNew(这里各记录各的,一个时间寄到两个类型就行。) +```java +2022-03-29T19:11:11.228+0800: 17.707: [GC (CMS Final Remark) [YG occupancy: 6657732 K (8709120 K)]2022-03-29T19:11:11.228+0800: 17.707: [Rescan (parallel) , 0.3269928 secs]2022-03-29T19:11:11.555+0800: 18.034: [weak refs processing, 0.0004249 secs]2022-03-29T19:11:11.556+0800: 18.035: [class unloading, 0.0064031 secs]2022-03-29T19:11:11.562+0800: 18.041: [scrub symbol table, 0.0612921 secs]2022-03-29T19:11:11.624+0800: 18.102: [scrub string table, 0.0007930 secs][1 CMS-remark: 0K(16128000K)] 6657732K(24837120K), 0.3975450 secs] [Times: user=0.76 sys=0.01, real=0.40 secs] + + +2022-03-14T14:17:11.303+0800: 316563.518: [GC (CMS Final Remark) [YG occupancy: 6327134 K (12268352 K)] +2022-03-14T14:17:11.303+0800: 316563.518: [GC (CMS Final Remark) 2022-03-14T14:17:11.304+0800: 316563.519: [ParNew (promotion failed): 6327134K->5311815K(12268352K), 1.0276246 secs] 24109239K->23094088K(40579904K), 1.0297185 secs] [Times: user=2.12 sys=0.12, real=1.03 secs] +``` + +上面说的总共记录成2个指标: ParNew、CMS(两类加起来) + +> 如果这两个指标在**一分钟内持续>3s**就报警(暂定这个时间,后续可能调整) + + +## 比较严重的GC(包含full GC) +>下面集中GC情况,只要出现,**不管耗时**长短(一般都不会太短),**都报警**。 + + - **Full GC** : 出现[Full GC 这个词就记录后面的real时间。 + -**promotion failed**: 一般形如`[ParNew (promotion failed)` ,出现关键词 **promotion failed** 就记录后面的real,记录成此类型。(ParNew也可能重复记录,先不用去重) + - **concurrent mode failure**: 出现关键词 **concurrent mode failure** 就记录后面的real时间。 + + + +**解释**后面两种情况: +- 当**young gc**的时候,把eden和survivor里的都还存活的对象,统一移到另一个survivor区中时,发现装不下了,就需要把部分对象,放到老年代中去,结果**老年代空间也不足**,这种场景呢,叫做**promotion failed**。 +- 在**promotion failed**的前提下,**老年代恰好还正在full gc**,那么就会有图1红框5中的字样提示,**concurrent mode failure**。 + diff --git "a/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.pdf" "b/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.pdf" new file mode 100644 index 0000000..dc3a6ee Binary files /dev/null and "b/Java/JVM/GC\347\233\221\346\216\247\350\256\276\350\256\241.pdf" differ diff --git "a/Java/JVM/GC\351\227\256\351\242\230\346\216\222\346\237\245\346\214\207\345\215\227.md" "b/Java/JVM/GC\351\227\256\351\242\230\346\216\222\346\237\245\346\214\207\345\215\227.md" new file mode 100644 index 0000000..c9a54c2 --- /dev/null +++ "b/Java/JVM/GC\351\227\256\351\242\230\346\216\222\346\237\245\346\214\207\345\215\227.md" @@ -0,0 +1,46 @@ + + +## 命令篇 + + +打印线程的栈信息,制作线程Dump。 + +jstack <进程ID> >> <输出文件> + +jstack 2316 >> c:\thread.txt + + +**dump内存情况** + + +- 打印存活的对象大小和个数 +```java +jmap -histo:live + +jmap -histo:live 64421 > live.log +``` + +- 二进制方式存储堆文件 +>注意要在进程用户下,或者有权限用户。 +>然后命令找不到可以试试java按照目录全路径 +```java +//jmap -dump:format=b,file=文件名.hprof <进程ID> +jmap -dump:format=b,file=/opt/wkt/wkt1.hprof 64421 +``` + + + +## 工具篇 + + + + + +## 理论篇 + + + + + +## 实战篇 + diff --git "a/Java\345\237\272\347\241\200/JVM\345\255\246\344\271\240.md" "b/Java/JVM/JVM\345\255\246\344\271\240.md" similarity index 100% rename from "Java\345\237\272\347\241\200/JVM\345\255\246\344\271\240.md" rename to "Java/JVM/JVM\345\255\246\344\271\240.md" diff --git "a/Java\345\237\272\347\241\200/JVM\346\216\242\347\247\230.md" "b/Java/JVM/JVM\346\216\242\347\247\230.md" similarity index 68% rename from "Java\345\237\272\347\241\200/JVM\346\216\242\347\247\230.md" rename to "Java/JVM/JVM\346\216\242\347\247\230.md" index 1538832..c43a096 100644 --- "a/Java\345\237\272\347\241\200/JVM\346\216\242\347\247\230.md" +++ "b/Java/JVM/JVM\346\216\242\347\247\230.md" @@ -15,6 +15,30 @@ 2. JIT编译执行 3. JIT编译与解释混合执行。 +### JVM优化Java代码时都做了什么? +**从JVM的角度给出的回答** + +JVM 在对代码执行的优化可分为: +- 运行时(runtime)优化: + - 主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配 机制(如 TLAB)等 + - 专门用于优化解释执行效率的,比如说模版解释 器、内联缓存(inline cache,用于优化虚方法调用的动态绑定) +- 即时编译器(JIT)优化: + - 是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上 + - 它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行 profile 的投机性优化(speculative/optimistic optimization) + + +**从 Java 工程师日常的角度出发** +- 侧重于 + - 从整体去了解 Java 代码编译、执行的过程,目的是对基本机制和流程有个直观的认识, 以保证能够理解调优选择背后的逻辑 + - 如何将JIT的知识落实到实际工作中的可能思路。这里包括两部 分:如何收集 JIT 相关的信息,以及具体的调优手段。 + + +通常所说的**编译期**,是指 javac 等编译器或者相关 API 等将源码转换成为字节码的过程, 这个阶段也会进行少量类似常量折叠之类的优化。 + + +**JVM 运行时的优化**: + + # 类加载 冯诺依曼:任何程序都需要加载到内存才能与CPU进行交流。 diff --git "a/Java\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213/\345\271\266\345\217\221\344\270\216\345\244\232\347\272\277\347\250\213.md" "b/Java/JVM/Java\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213/\345\271\266\345\217\221\344\270\216\345\244\232\347\272\277\347\250\213.md" rename to "Java/JVM/Java\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" diff --git "a/Spring\347\263\273\345\210\227/AOP.md" "b/Java/Spring\347\263\273\345\210\227/AOP.md" similarity index 100% rename from "Spring\347\263\273\345\210\227/AOP.md" rename to "Java/Spring\347\263\273\345\210\227/AOP.md" diff --git "a/Spring\347\263\273\345\210\227/Spring Boot.md" "b/Java/Spring\347\263\273\345\210\227/Spring Boot.md" similarity index 100% rename from "Spring\347\263\273\345\210\227/Spring Boot.md" rename to "Java/Spring\347\263\273\345\210\227/Spring Boot.md" diff --git "a/Spring\347\263\273\345\210\227/Spring Cloud.md" "b/Java/Spring\347\263\273\345\210\227/Spring Cloud.md" similarity index 100% rename from "Spring\347\263\273\345\210\227/Spring Cloud.md" rename to "Java/Spring\347\263\273\345\210\227/Spring Cloud.md" diff --git "a/Java/Spring\347\263\273\345\210\227/Spring Framwork\345\205\245\351\227\250\345\255\246\344\271\240.md" "b/Java/Spring\347\263\273\345\210\227/Spring Framwork\345\205\245\351\227\250\345\255\246\344\271\240.md" new file mode 100644 index 0000000..676d16f --- /dev/null +++ "b/Java/Spring\347\263\273\345\210\227/Spring Framwork\345\205\245\351\227\250\345\255\246\344\271\240.md" @@ -0,0 +1,791 @@ + +# Spring核心特性学习笔记 + +# 一、 框架总览 + +**课前准备**: +1. 心态:戒躁戒躁、谨慎豁达、如履薄冰 +2. 方法: + 1. 基础:夯实基础,了解动态 + 2. 思考:保持怀疑,验证一切 + 3. 分析: 不拘小节,观其大意 + 4. 实践:思辨结合,学以致用 +3. 工具 + 1. JDK: Oracle JDK 8 + 2. Spring FrameWork 5.2.2。RELEASE + 3. IDE + 4. Maven 3.2+ + +## 总览 + +**核心特性**: +- IoC容器 +- Spring事件 Events +- 资源管理(Resources) +- 国际化(i18n) +- 校验(Validation) BeanValidation +- 数据绑定 Data Binding +- 类型转换 Type Conversion +- Spring表达式 Spring Express Language +- 面向切面编程 AOP + +**数据存储**: +- JDBC +- 事务抽象 Transactions +- DAO支持 DAO Support +- O/R映射 Mapping +- XML编列 Marshlling + +**Web技术** +- Web Servlet技术栈 + - Spring MVC + - WebSocket + - SockJS +- Web Reactive技术栈 + - Spring WebFlux + - WebClient + - WebSocket + +**技术整合** +- 远程调用 Remoting +- Java消息服务 JMS +- Java连接架构 JCA 可忽略 +- Java管理扩展 JMX +- 本地服务 Tasks +- 本地调度 Scheduling +- 缓存抽象 Caching +- Spring测试 Testing + - 模拟对象 Mock Objects + - TestContect框架 + - Spring Mvc测试 + - Web测试客户端 + + +## 版本特性 +Soring 4.x -- Java 6+ :springboot1.0 +Spring 5.x -- Java标准版 8+ -- Java企业版 Java EE 7 -- springboot2 + +## 模块化设计 +大概分为了 **20**多个模块: +- aop 切面 +- aspects 切面 +- beans 与context一起组成ioc +- context-indexer +- contect-support +- context +- core +- expression 表达式语言 EL +- instrument java装配 +- jcl 日志框架 +- jdbc +- jms 消息服务 +- messaging 统一消息服务 +- orm +- oxm xml编列 +- test +- tx 事务 +- web 想统一,所以分为多个模块 +- webflux +- webmvc +- websocket + + +## 技术整合 + +### Java语言特性运用 + +Java 语言变化: +- 5: 枚举、泛型、注解、封箱(解箱) spring1.2 第一版开始支持 2004年 + - 注解、枚举 spring1.2+支持 + - for-each、自动转拆箱、泛型 spring3.0+支持 +- 6: @Override spring4.0 +- 7: Diamond语法(泛型<>不需要写)、多Catch、Try语法糖(Resource close)。 spring5+ +- 8: Lambda语法、可重复注解(一个方法可以加多个注解)、类型注解 +- 9:模块化、接口私有方法 +- 10:局部变量类型推断 + + +### JDK API实践 +- < Java5: 反射 Reflection、Java Beans、动态代理 +- Java5: 并发框架(J.U.C)、格式化(Formatter)、Java管理扩展(JMX)、Instrumentation、XML处理(DOM、SAX、XPath、XSTL) +- Java6: JDBC 4.0(JSR 221)、JAXB 2.0(JSR 222)、可插拔注解处理API(JSR 269)、Common Annotations(JSR 250)、Java Compiler API(JSR 199)、Scripting in JVM(JSR 223) +- Java7: NIO 2(JSR 203)(PathResource)、Fork/Join框架(JSR 116)、invokedynamic 字节码 +- Java8: Stream API、CompletableFuture(J.U.C)、Annotation on Java Types、Date and Time API、可重复Annotations 、JavaScript运行时 +- Java9:Reactive Stream Flow API、Process API Updates、Variable Handlers、Method Handles、Spin-Wait Hints、Stack-Walking API + +>JSR: Java Specification Requests + +### JavaEE API整合 + +**Java EE Web技术相关**: +- Servlet + JSP--- DispatcherServlet +- JSTL -- JstlView +- JSF(JavaServer Faces) -- +- Portlet +- SOAP 简单对象访问协议 +- WebServices +- WebSocket + +**数据存储相关**: +- JDO -- JdoTemplate 5.0之后不支持了 +- JTA 事务 +- JPA 注解方式支持 +- Java Caching API + +**Bean技术相关** +- JMS -- JmsTemplate +- EJB +- Dependency Injection For Java 依赖注入 spring2.5+ +- Bean Validation 3.0+ + +**相关资源** +-[小马哥JSR收藏](https://github.com/mercyblitz/jsr) +- [JSR官方网站](https://jcp/org) + + +## 编程模型 + +### 面向对象编程 + +- 契约接口 +- 设计模式: + - 观察者模式 EventObject + - 组合模式 Composite + - 模板模式 Template JdbcTemplate + - 对象继承 Abstract Application... + +### 面向切面编程 + +#### 动态代理 + +JdkDynamic +CglibAopProxy + +#### 字节码提升 + +### 面向元编程 + +#### 配置元信息 +Environment.java +PropertySource.java + +#### 注解 +模式注解 +- @Component +- @Repository +- @Service + +#### 泛型 + +GenericTypeResolver +TypeResolver +ResolvableType + +### 面向模块编程 + +#### Maven Artifacts + +#### Java 9 Automatic Modules + +#### Spring @Enable*注解 +@EnableCache +@EnableMvc +@Enable 激活 + +### 面向函编程 + +- FunctionalInterface.java +- 函数接口:ApplicationEventPublisher +- Reactive: Spring WebFlux + +#### Lambada + +#### Reactive +异步非阻塞 + +## Spring 核心价值 + +- 生态系统 + - Spring Boot + - Spring Cloud + - Spring Security + - Spring Data + - 其他 +- API抽象 + - AOP抽象 + - 事务抽象 + - Environment抽象 + - 生命周期 +- 编程模型 + - 面向对象 + - 契约接口 + - 面向切面 + - 动态代理 + - 字节码提升 + - 面向元 + - 配置元信息 + - 注解 + - 面向函数 + - Lambda + - Reactive + - 面向模块 +- 设计思想 + - OOP + - IoC/DI + - DDD + - TDD + - EDP + - FP +- 设计模式 + - 专属模式 + - 前缀模式 + - Enable模式 + - Configurable + - 后缀模式 + - 处理模式 + - Processor + - Resolver + - Handler + - 意识模式 + - Aware + - 配置器模式 + - Configuror + - 选择器模式 + - ImprotSelector + - 传统GoF23 + - 创建模式 + - 结构模式 + - 行为模式 + - 责任链 +- 用户基础 + - Spring用户 + - Spring Framework + - Spring Boot + - Spring CLoud + - 传统用户 + - Java SE + - Java EE + +## 面试题 + +- 沙雕面试题 + - 什么是Spring Framework + - 企业应用 + - 易用 + - 提供很多好特性 +- 996面试题 + - Spring有哪些重要的模块 + - core:资源管理、泛型处理 + - beans: 依赖查找、依赖注入 + - aop: 动态代理、AOP字节码提升 + - context: 事件驱动、注解驱动、模块驱动 + - expression: 表达式语言模块 +- 劝退题 + - Spring Framework的优势和不足是什么? 贯穿整个系列,慢慢补充 + +--- + +# 二、 IoC容器 + +## 重新认识IoC + +### 1. IoC发展简介 +- 1983年 好莱坞原则: 演员不用去找导演,导演会联系演员 +- 1988 控制反转 +- 1996 控制反转命名为好莱坞原则 +- 2004 Martin Fowler 提出了自己对IoC和DI的一些理解 +- 2005 Martin Fowler 对IoC做出进一步说明 + +### 2. IoC主要实现策略 +- service locator pattern +- 依赖查找 lookup +- 依赖注入 injection + - 构造器注入 + - 参数注入 + - Setter injection + - 接口注入 +- contextualized lookup +- 模板方法设计模式 template +- 策略模式 + + +### 3. IoC容器的职责 +IoC遵循下面几个原则: +- 实现与任务运行之间解耦 +- 关注设计目标模块而不是实现 +- To prevent side effects when replacing a module. +- To free modules from assumptions about how other systems do what they do and instead rely on contracts. +>"好莱坞原则": 不要打给我们,我们会打给你。 我们:需要的东西和资源 你:系统、模块 + +**职责**: +- 通用职责 +- 依赖处理 + - 依赖查找 + - 依赖注入 +- 生命周期管理 + - 容器 + - 托管资源(Java Beans 或其他资源) +- 配置 + - 容器 + - 外部化配置 + - 托管的资源Java Beans 或其他资源) + +### 4. IoC容器的实现 +- Java SE + - Java Beans + - Java ServiceLoader SPI + - JNDI(Java Naming and Directory Interface) +- Java EE + - EJB(Enterprise Java Beans) + - Servlet +- 开源 + - Apache Avalon + - PicoContainer + - Googel Guice + - Spring Framework + + +### 5. 传统IoC容器实现 +- Java Beans 作为IoC容器 +- 特性 + - 依赖查找 + - 声明周期管理 + - 配置元信息 + - 事件 + - 自定义 + - 资源管理 + - 持久化 +- 规范 + - JavaBeans: https://www.oracle.com/technetwork/java/javase/tech。。。 + - BeanContext: + +**什么是JavaBeans**: +PropertyEditor + + +### 6. 轻量级IoC容器 +- 管理到我的应用代码,控制启停生命周期 +- 快速启动 +- 不需要一些特殊配置, 不像EJB需要大量XML +- 轻量级内存占用,少量的API。 EJB需要大量的API +- 提供管控的渠道 + + +**好处**: +- 执行层面和实现的解耦 +- 最大化代码复用 +- 更大化的面向对象 +- 更大化的产品化 +- 更好的可测性 + +### 7. 依赖查找 VS 依赖注入 +**优劣对比**: + +类型 | 依赖处理 | 实现便利性 | 代码入侵性 | API依赖性 | 可读性 +---------|----------|---------|---------|----------|--------- + 依赖查找 | 主动获取 | 相对繁琐 | 侵入业务逻辑 | 依赖容器API | 良好 + 依赖注入 | 被动提供 | 相对便利 | 低侵入性 | 不依赖容器API | 一般 + + +### 8. 构造器注入 VS Setter注入 +- 鼓励构造器注入。 不变的对象,确保对象不为空。 +- Setter注入可选。 让对象更可配 + +Setter注入优点: +- JavaBeans properties 更好的支持 IDEs +- JavaBeans 属性是自文档的 + +setter缺点: +- Setter没有顺序 +- 不是所有的setter方法都是必须的 + +构造器优势: +- 字段赋值,鼓励对象是不变的,final修饰。 + +**参考书**: 《Expert One-On-One J2EE Development without EJB》倾向于setter,spring官方文档倾向于 构造器注入 + +### 9. 面试题精选 +- 沙雕面试题: 什么是IoC? +> 反转控制,类似于好莱坞原则,主要有依赖查找,依赖注入(构造器、setter)。 推的模式。 + +- 996面试题: **依赖查找和依赖注入**的**区别**? +> 依赖查找是主动或手动的依赖查找方式,通常需要依赖容器或标准API实现。 而依赖注入则是手动或自动依赖绑定的方式,无需依赖特性的容器和API。 + +- 劝退面试题:** Spring作为IoC容器**有什么**优势**? + - 典型的IoC管理,依赖查找,依赖注入 + - AOP抽象 + - 事务抽象 + - 事件机制 + - SPI扩展 + - 强大的第三方整合 + - 易测试性 + - 更好的面向对象 + + +## Spring IoC容器概述 + + +### 1. Spring IoC依赖查找 +- 根据Bean名称查找 + - 实时查找 + - 延迟查找: ObjectBean +- 根据Bean类型查找 +- 单个Bean对象 + +### 2. Spring IoC依赖注入 + + +### Spring IoC依赖来源 +- 自定义的Bean: UserRepository +- 容器内建Bean对象: Environment +- 容器内建依赖 : BeanFacotory + +### 3. Spring IoC配置元信息 +- Bean定义配置 + - 基于XML文件 + - 基于Properties文件 + - 基于Java注解 + - 基于Java API +- IoC容器配置 + - 基于XML文件 + - 基于Java注解 + - 基于Java API +- 外部化属性配置 + - 基于Java注解 @Value + +### 4. Spring IoC容器 底层 +**BeanFactory 和ApplicationContext谁才是Spring IoC容器?** +- BeanFactory是底层的IoC容器。 +- ApplicationContext通过组合方式引入了BeanFactory的实现,提供更多企业级特性(更好的跟AOP集成,消息资源处理、事件发布),是超集。 + +### 5. Spring应用上下文 +ApplicationContext除了IoC容器角色,还提供: +- AOP +- 配置元信息(Configuration Metadata) +- 资源管理(Resources) +- 事件(Event) +- 国际化 +- 注解 +- Environment抽象 + +### 6. 使用Spring IoC容器 +- BeanFactory是Spring底层IoC容器 +- ApplicationContext是具备应用特性的BeanFactory的超集 + +### 7. Spring IoC容器生命周期 +- 启动 :refresh() + - prepareRefresh + - prepareBeanFactory +- 运行 +- 停止 + +### 8. 面试题 +- 沙雕面试题: 什么是Spring IoC容器? + - DI是IoC实现的一种,原则。依赖查找已经移除了 + - 伴随很多依赖 +- 996面试:BeanFactory 和 FactoryBean + - BeanFactory是IoC底层容器 + - FactoryBean是创建Bean的一种方式,帮助实现复杂的初始化逻辑 +- 劝退: Spring IoC容器启动时做了哪些准备? + - AbstractApplicationContext.java + - IoC配置元信息读取和解析 + - IoC容器声明周期 + - Spring事件发布 + - 国际化 + + +## Spring IoC依赖查找 + +### 1. 依赖查找的前世今生 + +### 2. 单一类型依赖查找 + +### 3. 集合类型依赖查找 + +### 4. 层次性依赖查找 + +### 5. 延迟性依赖查找 + +### 6. 安全依赖查找 +不太理解安全性的意思: +- 根据类型查找,如果有多个同类型的Bean,会报错,这就是不安全?? +- + +### 7. 内建可查找的依赖 + + +Bean名称 | Bean实例 | 使用场景 +---------|----------|--------- + environment | Environment对象 | 外部化配置以及Profiles + systemProperties | java.util.Properties对象 | Java系统属性 + systemEnvironment | B3 | C3 + systemEnvironment | B3 | C3 +systemEnvironment | B3 | C3 + systemEnvironment | B3 | C3 + + **注解驱动Spring应用上下文内建可查找的依赖** + +### 8. 依赖查找中的经典异常 +**BeanException**子类型 + +异常类型 | 触发条件 | 场景距离 +---------|----------|--------- +NoSuchBeanDefinitionException | B1 | C1 +NoUniqueBeanDefinitionException | B2 | C2 +BeanInstantiationException | B3 | C3 +BeanCreateException | B3 | C3 + BeanDefinitionStoreException | B3 | C3 + +### 9. 面试题精选 +- 沙雕面试题:ObjectFactory与BeanFactory的区别? + - 两者都提供依赖查找的能力 + - 不过ObjectFactory仅关注一个或者一种类型的Bean依赖查找,并且自身不具备依赖查找的能力,能力则由BeanFactory输出 + - BeanFactory则提供了单一类型、集合类型以及层次性等多种依赖查找方式 +- 996面试题:BeanFactory.getBeab操作是否线程安全? + - 是线程安全的。举例用DefaultListableBeanFactory +- 劝退面试题:Spring依赖查找与注入在来源上的区别? + +## 依赖注入 + +### 1. 依赖注入的模式和类型 +**手动模式**:配置或者编程的方式,提前安排注入规则 +- XML 资源配置元信息 +- Java注解配置元信息 +- API配置元信息 + +**自动模式**:实现方式提供依赖自动关联的方式,按照内建的注入规则 +- Autowiring + +**依赖注入类型**: +- Setter方法: +- 构造器: +- 字段: `@Autorwire User user` +- 方法: `@Autorwire public void user(User user){...}` +- 接口回调: `class MyBean implements BeanFactoryAware{...}` + +### 2. 自动绑定(AutoWiring) +**优点**: +- 减少属性、构造器参数的配置 +- 更新绑定,引用传递 + + +### 3. 自动绑定模式 +模式分类: +- **no**: 默认值,未激活Autowiring, 需要手动指定依赖注入对象 +- **byName**: 根据被注入属性的名称作为Bean名称进行依赖查找,并将对象设置到该属性 +- **ByType**: 根据被注入属性的类型作为依赖类型查找,并将对象设置到该属性 +- **constructor**: 特殊byType类型,用于构造器参数 + +### 4. 自动绑定限制和不足 +看官方文档: +- 精确依赖会覆盖自动 +- 不能绑定一些简单的类型,原生类型。可以用@Value +- +- 不唯一就会报异常,可以加 primary + +### 5. Setter方法依赖注入 +实现方法: +- 手动模式 + - xml + - 注解 + - api +- 自动模式 + - byType + - byName + +### 6. 构造器依赖注入 +- 手动模式 + - xml: + - 注解: @Bean new User(xxx) + - api: 是有顺序的 +- 自动模式 + - autowire="constructor" + + +### 7. 字段注入 +- 手动模式 + - Java注解配置元信息 + - @Autowire + - 会忽略掉静态字段,不会注入 + - @Resource + - @Inject(可选) + +### 8. 方法注入 +不是注入方法。。。 +- 手动模式 + - Java注解配置元信息 + - @Autowire + - 会忽略掉静态字段,不会注入 + - @Resource + - @Inject(可选) + - @Bean + +### 9. 回调注入 + +### 10. 依赖注入类型选择 + +### 11. 基础类型注入 +- 原生类型:int +- 标量类型:enum +- 常规类型:Object、String +- Spring类型: + +### 12. 集合类型注入 +- 数组类型(Array):原生类型、标量类型、常规类型、Spring类型 +- 集合类型(collection): + - Collection:List、Set(SortedSet、NavigableSet、EnumSet) + - Map:Properties + +### 13. 限定注入 +- `@Qualifier` +- + +### 14. 延迟依赖注入 + +### 15. 依赖处理注入 + +### 16. @Autowire注入原理 + +### 17. JSR-330@Inject注入原理 + +### 18. Java通用注解注入原理 + +### 19. 自定义依赖注入注解 + +### 20. 面试题精选 + +# 三、 Bean + +## Spring Bean基础 + +### 1. 定义Spring Bean +BeanDefinition: 是Spring Framwork 中定义Bean的 **配置元信息接口**,包含: +- Bean的类名 +- Bean行为配置元素,如作用域、自动绑定的模式、生命周期回调等 +- 其他Bean引用,又可称为合作者(Collaborators)或者依赖(Dependencies) +- 配置设置,比如Bean配置(Properties) + +### 2. BeanDefinition元信息 +- Class: Bean全类名,必须是具体类, 不能用抽象类或接口 +- Name: Bean的名称或者ID +- Scope: Bean的作用域(singleton、prototype) +- Constructor arguments:构造器参数, 用于依赖注入 +- Properties: 属性设置,用于依赖注入 +- AUtoWiring mode: 自动绑定模式,byName byType +- Lazy initialization mode: 延迟初始化模式 +- Initialization method: 初始化回调方法名称 +- Destruction method: 销毁回调方法名称 + + +BeanDefinition如何构建? +- 通过BeanDefinitionBuilder +- 通过AbstractBeanDefinition以及派生类 + + +### 3. 命名Spring Bean +Bean的名称: +- 允许出现特殊字符,比如. +- 如果想要引入别名,可以在name属性使用英文逗号或者分号来间隔 +- id非必填,留空的话,容器会自动生成唯一的名称 + + +Bean的名称生成器,因为id、name非必填。 注解式的一般都不命名,使用默认生成的。 XML一般都命名居多。 + +### 4. Spring Bean的别名 +别名的价值场景: +- 在不同的系统叫不同的名称 + +### 5. 注册Spring Bean +- XML配置元信息 + - +- Java注解配置元信息 + - @Bean + - @Component + - @Import() +- Java API配置元信息 + - 命名方式:BeanDefinitionRegistry#registerBeanDefinition(String, BeanDefinition) + - 非命名方式: BeanDefinitionReaderUtil#registerWithGeneratedName(AbstractBeanDefinition, BeanDifinitionRegistey) + - 配置类方式: AnnotatedBeanDefinitionReader#register(Class...) + +### 6. 实例化Spring Bean +- 常规方式 + - 通过构造器 (配置元信息:XML、Java注解和Java API) + - 静态工厂方法(配置元信息:XML、Java API) + - Bean工厂方法(配置元信息:XML、Java API) + - FactoryBean(配置元信息:XML、Java API) +- 特殊方式 + - 通过ServiceLoaderFactoryBean(配置元信息:XML、Java注解和Java API) + - 通过AutowireCapableBeanFactory#createBean(Class, int, boolean) + - 通过BeanDefinitionRegistry#registerBeanDefinition(String, BeanDefinition) + +### 7. 初始化Spring Bean +Initialization +- @PostConstruct标注方法 +- 实现Initialization接口的afterPropertiesSet()方法 +- 自定义初始化方法 + - XML配置: + - Java注解:@Bean(initMethod="init) + - Java API:AbstractBeanDefinition#setInitMethodName(String) + +>如果同时出现,那顺序是 PostConstruct > Initialization > 自定义 + +### 8. 延迟初始化Spring Bean +Lazy Initialization +- XML配置: +- Java注解: @Lazy(true) + +>当某个Bean定义为延迟初始化,那么。Spring容器返回的对象与非延迟的对象存在怎样的差异? 延迟加载是在上下文初始化之后进行加载的, 非延迟加载是在上下文初始化之前加载 + +### 9. 销毁Spring Bean +Destroy +- @PreDestory标注方法 +- 实现DisposableBean接口的destroy()方法 +- 自定义初始化方法 + - XML配置: + - Java注解:@Bean(destroy="destroy") + - Java API:AbstractBeanDefinition#setDestroyMethodName(String) + +>如果同时出现,那顺序是 @PreDestory > DisposableBean > 自定义 + +### 10. 垃圾回收Spring Bean +1. 关闭Spring容器(应用上下文) +2. 执行GC +3. Spring Bean覆盖finalize()方法回调 + +### 11. 面试题精选 +1. 沙雕面试题:如何注册一个Spring Bean + 1. 通过BeanDefinition和外部单体对象注册 +2. 996面试题:什么是Spring BeanDefinition? + 1. 有很多属性, scope、role、primary、各种元信息接口 +3. 劝退面试题:Spring容器是怎样管理注册Bean? + 1. IoC配置、依赖注册、依赖查找、生命周期等 + +## Bean实例 + +## Bean作用域 + +## Bean生命周期 + +# 四、 元信息 + +## 注解 + +## 配置源信息 + +## 外部化属性 + +# 五、 基础设施 + +## 资源管理 + +## 类型转换 + +## 数据绑定 + +## 校验 + +## 国际化 + +## 事件 + +## 泛型处理 \ No newline at end of file diff --git "a/Spring\347\263\273\345\210\227/Spring MVC\345\205\245\351\227\250\346\261\207\346\200\273.md" "b/Java/Spring\347\263\273\345\210\227/Spring MVC\345\205\245\351\227\250\346\261\207\346\200\273.md" similarity index 100% rename from "Spring\347\263\273\345\210\227/Spring MVC\345\205\245\351\227\250\346\261\207\346\200\273.md" rename to "Java/Spring\347\263\273\345\210\227/Spring MVC\345\205\245\351\227\250\346\261\207\346\200\273.md" diff --git "a/Spring\347\263\273\345\210\227/Spring\345\205\245\351\227\250.md" "b/Java/Spring\347\263\273\345\210\227/Spring\345\205\245\351\227\250.md" similarity index 100% rename from "Spring\347\263\273\345\210\227/Spring\345\205\245\351\227\250.md" rename to "Java/Spring\347\263\273\345\210\227/Spring\345\205\245\351\227\250.md" diff --git "a/Java/\345\237\272\347\241\200/Java\345\217\215\345\260\204\346\234\272\345\210\266\345\222\214\345\212\250\346\200\201\344\273\243\347\220\206.md" "b/Java/\345\237\272\347\241\200/Java\345\217\215\345\260\204\346\234\272\345\210\266\345\222\214\345\212\250\346\200\201\344\273\243\347\220\206.md" new file mode 100644 index 0000000..05514db --- /dev/null +++ "b/Java/\345\237\272\347\241\200/Java\345\217\215\345\260\204\346\234\272\345\210\266\345\222\214\345\212\250\346\200\201\344\273\243\347\220\206.md" @@ -0,0 +1,14 @@ + + +# 谈谈 Java 反射机制,动态代理是基于什么原理? + +- 动态类型和静态类型就是其中一种分类角度,简单区分就是 +- 语言类型信息是在**运行时检查,动态类型**。 +- 还是**编译期检查**,静态代理。 + + +类似 根据是否需要显示地进行类型转换分为: +- 强类型 +- 弱类型 + +Java 是**静态的强类型**语言,但是因为提供了类似反射等机制,也具备了部分动态类型语言的能力。 \ No newline at end of file diff --git "a/Java/\345\237\272\347\241\200/Java\345\237\272\347\241\200\345\244\247\346\235\202\347\203\251.md" "b/Java/\345\237\272\347\241\200/Java\345\237\272\347\241\200\345\244\247\346\235\202\347\203\251.md" new file mode 100644 index 0000000..e11bb66 --- /dev/null +++ "b/Java/\345\237\272\347\241\200/Java\345\237\272\347\241\200\345\244\247\346\235\202\347\203\251.md" @@ -0,0 +1,380 @@ + + +# 你对Java平台的理解 + +## 特点、特性 + +### 第一印象 + +>思维深入,且系统化 +- Write Once,run anywhere,跨平台。 因为字节码-虚拟机 +- 垃圾回收GC,自动内存分配和回收。 +- JRE:Java运行环境,包含JVM和Java类库,以及一些模块。 +- JDK: JRE的一个超集,还提供编译器、各类诊断工具。 +- Java是大部分解释执行,但是JIT即时编译技术,热点代码提前编译成机器码-这属于编译执行。 + + +**很多点** +- 语言特性:泛型、Lamabda等 +- 基础类库: + - 集合 + - IO/NIO、网络、utils + - 并发、安全 +- JVM + - 类加载机制、常用JDK版本特点区别 + - 垃圾回收基本原理,常见垃圾收集器:SerialGC、Parallel GC、CMS、G1 + - 工具:编译器、运行时环境、安全工具、诊断、监控工具。 + - 辅助工具,如jlink、jar、jdeps + - 编译器,javac、sjavac + - 诊断工具:jmap、jstack、jconsole、jhsdb、jcmd + - 解释和编译混合(mixed): + - C1对应client模式(适用于启动速度敏感的应用,比如普通Java桌面应用) + - C2对应server模式(适用于长时间运行的服务端应用) + + + +## 多态&父子类 +protected 需要从以下两个点来分析说明: + +子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问; + +子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。 + +protected 可以修饰数据成员,构造方法,方法成员,不能修饰类(内部类除外)。 + + +## 理解 Java 的字符串,String、StringBuffer、StringBuilder 有什么区别? +**String** +- 它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。 +- 由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响 + + +**StringBuffer**:为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。 +- 本质是一个**线程安全**的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销。 +- 通过把各种修改数据的方法都加上`Syncronized`关键字实现的。 + + +**StringBuilder** 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去 掉了线程安全的部分,有效减小了开销。字符串拼接优先选它。 + + +**考点**: +* 通过 String 和相关类,考察基本的线程安全设计与实现,各种基础编程实践。 考察 JVM 对象缓存机制的理解以及如何良好地使用。 +* 考察 JVM 优化 Java 代码的一些技巧。 +* String 相关类的演进,比如 Java 9 中实现的巨大变化。 + + +1. **字符串设计和实现考量** + 1. 因为是不可变的,所以String本身是线程安全的,拷贝函数也不需要额外复制数据。 + 2. StringBuffer 和 StringBuilder 底层都是利用可修改的 (char,JDK 9 以后是 byte)**数组**,二者都继承了 AbstractStringBuilder,里面包含了基 本操作,区别仅在于最终的方法是否加了 synchronize。 + 3. 这个内部数组初始字符串长度为+16(如果没有构建对象时输入最初的字符串,那么初始值就是 16)。确定这个长度不够的话,建议给个初始值。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组, 还要进行 arraycopy + 4. 非静态的拼接逻辑在 JDK 8 中会自动被 javac 转换为 StringBuilder 操作。其实就是下面这种: `String str = "aa" + "bb" + "cc" + "dd" ;`会被优化成StringBuilder。 + >在日常编程中,保证程序的可读性、可维护性,往往比所谓的最优性能更重要。 + >String myStr = "aa" +"bb" + "cc" +"dd";反编译后并不会用到StringBuilder,老师反编 译结果中出现StringBuilder是因为输出中拼接了字符串System.out.println("My String:" + myStr); + + +2. **字符串缓存** + - 经过粗略统计,将常见应用进行Dump Heap后进行对象组成分析,发现平均25%的对象是字符串,并且其中50%都是重复的。 **如何避免创建重复字符串,可以有效降低内存消耗和对象创建开销**。 + - String 在 Java 6 以后提供了**intern()**方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。但真实情况是Java6版本是不推荐这么用的!如果使用不当,会导致OOM。 + >被缓存的字符串是存在所谓 PermGen 里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到. 后续版本把这个缓存放在了堆中,而Java 8用元空间代替了。 + - Intern 是一种**显式地排重机制**,需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的,另外也无法确定字符串重复情况,效率优化效果不稳定。 + - 在java8后续版本中是支持JVM自动优化, G1 GC 下的字符串排重。 它是通过将相同数据的字符串指向同一份数据来做到的。 默认是关闭的, XX:+UseStringDeduplication,需要指定为G1 GC + + +3. String自身的演化。 + 1. 在 Java 9 中,我们引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。 将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并 且将相关字符串操作类都进行了修改。 + 2. 好处: 更小的内存占用,更快的操作速度 + 3. 坏处: 由于一个byte数组只有之前char数组的一半,字符串最大长度自然也少了一般,但一般用不到这么长。 + + +**getBytes()等方法最好是指定编码:**, +- 如果不指定则看看JVM 参数里有没有指定file.encoding参数, +- 如果JVM没有指定,那使用的默认编码就是运行的 操作系统环境的编码了, +- 那这个编码就变得不确定了。常见的编码iso8859-1是单字节编 码,UTF-8是变长的编码。 + +**String s = new String(“abc”) 创建了几个对象?** + + +String是immutable,在security, Cache,Thread Safe等方面都有很好的体现。 +- Security: 传参的时候我们很多地方使用String参数,可以保证参数不会被改变,比如数据 库连接参数url等,从而保证数据库连接安全。 +- Cache: 因为创建String前先去Constant Pool里面查看是否已经存在此字符串,如果已经 存在,就把该字符串的地址引用赋给字符变量;如果没有,则在Constant Pool创建字符 + + +1.通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否 存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池 中生成一个字符串,再将栈中的引用指向该字符串。 +2.JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常 量池中的引用,这一点与1.7之前没有区别,区别在于,如果在常量池找不到对应的字符 + + + +## transient修饰符 +transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 + +因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。 + +比如银行卡密码 + + +## OOM 你遇到过嘛? + +除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。 + +1. Java Heap 溢出: 一般的异常信息:`java.lang.OutOfMemoryError:Java heap spacess`。 +>java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来 避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。 + +- 对dump出来的 堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。 + - 如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是 通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 + - 如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。 + + +2. 虚拟机栈和本地方法栈溢出 + +- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出`StackOverflowError`异常。 +- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常. + + +3. 运行时常量池溢出 异常信息:`java.lang.OutOfMemoryError:PermGenspace` + +- 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。 + - 该方法 的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象; + - 否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。 + - 由于**常量池分配在方法区**内,我们可以通过`-XX:PermSize`和`-XX:MaxPermSize`限制方法区的大小,从而间接限制其中常量池的容量。 + + +4. 方法区溢出 + +- 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。 +- 也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。 +- 异常信息:`java.lang.OutOfMemoryError:PermGenspace` + +- 方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常**动态生成大量**Class的应用中,要特别注意这点。 比如for循环里生成对象。 + + +**SOF(堆栈溢出StackOverflow):** 当应用程序递归太深而发生堆栈溢出时,抛出该错误。 + +- 因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容 量超过1m而导致溢出。 +- 栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。 + + +## IO流 + +Java 中 IO 流分为几种? + * 按照流的流向分,可以分为输入流和输出流; + * 按照操作单元划分,可以划分为字节流和字符流; + * 按照流的角色划分为节点流和处理流。 + +Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 + +- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + + +## java反射的作用于原理 + +**1、定义:** + +反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象, 都能够调用它的任意一个方法。 在java中,只要给定类的名字,就可以通过反射机制来获得类的所 有信息。 + +>这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。 + + +**2、哪里会用到反射机制?** + +- jdbc就是典型的反射: +```java +Class.forName('com.mysql.jdbc.Driver.class');`//加载MySQL的驱动类 +``` +这就是反射。如hibernate,struts等框架使用反射实现的。 + + +**3、反射的实现方式:** + +第一步:获取Class对象,有4中方法: + +1)Class.forName(“类的路径”); +2)类名.class +3)对象 名.getClass() +4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象 + + +**4、实现Java反射的类:** + +1)Class:表示正在运行的Java应用程序中的类和接口 注意: 所有获取对象的信息都需要Class类 来实现。 + +2)Field:提供有关类和接口的属性信息,以及对它的动态访问权限。 + +3)Constructor: 提供关于类的单个构造方法的信息以及它的访问权限 + +4)Method:提供类或接口中某个方法的信息 + + +**5、反射机制的优缺点:** + +**优点**: +1)能够运行时动态获取类的实例,提高灵活性; +2)与动态编译结合 + +**缺点**: +1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。 + 解决方案: + 1、通过setAccessible(true) 关闭JDK的安全检查来提升反射速度; + 2、多次创建一个类的实例时,有缓存会快很多 + 3、 ReflectASM工具类,通过字节码生成的方式加快反射速度 + +2)相对不安全,破坏了封装性(因为通 过反射可以获得私有方法和属性) + + +## Object的方法 + +- notify/notifyAll 和wait(带超时和不带超时) + +wait: + +调用该方法后当前线程进入睡眠状态,直到以下事件发生。 +1. 其他线程调用了该对象的 notify 方法; +2. 其他线程调用了该对象的 notifyAll 方法; +3. 其他线程调用了 interrupt 中断该线程; +4. 时间间隔到了。 +5. +>此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。 + +- equals 方法 和 hashcode 方法 + - 重写了 equals 方法一般都要重写 hashCode 方法。 + - equals相等 ,hashcode肯定一样,反之不一定 + + +- finalize : 该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。 + +- clone: 保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable + +- toString 和 getClass 方法 + + + +## 集合 + +### ArrayList + +Java 集合框架中的一种存放相同类型的元素数据,是一种变长的集合类,基于定长数组实现,当加 入数据达到一定程度后,会实行自动扩容,即扩大数组大小。 + + +底层是**使用数组实现**,添加元素。 + +- 如果 add(o),**添加到的是数组的尾部**,如果要增加的数据量很大,应该使用 ensureCapacity() 方法,该方法的作用是预先设置 ArrayList 的大小,这样可以大大提高初始化速度。 + +- 如果使用 add(int,o),添加到某个位置,那么可能**会挪动大量的数组元素**,并且可能会触发扩容机制。 + +- 高并发的情况下,线程不安全。多个线程同时操作 ArrayList,会引发不可预知的异常或错误。 + +- ArrayList 里面的 clone() 复制其实是浅复制。 + +- 数组是定死的数组(初始化必须指定大小,或者有具体的元素),ArrayList 却是动态数组。 + + +### 说说什么是 fail-fast? + +fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行 操作时,就可能会产生 fail-fast 事件。 + +例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变 了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事 件。 这里的操作主要是指 add、remove 和 clear,**对集合元素个数进行修改。** + +>在遍历之前,把 modCount 记下来 expectModCount,后面 expectModCount 去 和 modCount 进行比较,如果不相等了,证明已并发了,被修改了, + +解决办法:建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。 + + +### HashMap 跟 HashTable的区别 + +1. HashMap 的 key 和 value 都可以为 null。在计算 hash 值的时候,有判断,如果key==null ,则其 hash=0 ;至于 value 是否为 null,根本没有判断过。 + + +2. Hashtable 直接使用对象的 hash 值。 + 1. hash 值是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。然后再使用**除留余数法来获得最终的位置**。然而除法运算是非常耗费时间的,效率很低。 + 2. HashMap 为了提高计算效率,**将哈希表的大小固定为了 2 的幂**,这样在取模预算时,不需要做除法,**只需要做位运算**。位运算比除法的效率要高很多。 + + +### HashMap 与 ConcurrentHashMap 的异同 + +1. 都是key-value,HashMap可以有一个key为null,后者不行。 HashMap可以有很多value为null,后者一个都不行? +2. ConcurrentHashMap结构。 + 1. 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry, Segment 数组大小默认是 16,2 的 n 次方; + 2. JDK 1.8 之后,采用 Node + CAS + Synchronized 来保证并发安全进行实现。 + + +**红黑树的特征** + +- 节点只有两种颜色可选 +- 根节点黑色 +- 叶子节点黑色 +- 父红子必黑 +- 从节点到该节点的子孙节点的所有路径,包含相同数目的黑色节点。 + + +### HashMap + +**为啥length按照2的n次方扩容?** + +为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。 + +想到通过取模%来实现。 + +>取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说 `hash % length == hash &(length - 1)` 的前提是 **length 是 2 的 n 次方**)。并且,采用二进制位操作 & ,相对于 % 能够提高运算效率。 + + +**底层数据结构变化** + +* HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。 +* JDK 1.8 之后是数组 + 链表 + 红黑 树。 +* 当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快; + + +# JVM + + +其中内存模型,类加载机制,GC是重点方面. + +性能调优部分更偏向应用,重点突出实践能力. + +编译器优化 和执行模式部分偏向于理论基础,重点掌握知识点. + +## 内存模型 + + + +## 类加载机制 + +**加载过程** + + +其中验证,准备,解析合称链接。 + +- 加载 通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象. + +- 验证 确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全. + +- 准备进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态变 量,因为final变量在编译时分配. + +- 解析将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量 等. + +- 初始化主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用 时才会初始化. + + +触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时 候,或者某个子类初始化的时候. + + +Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸. + + +**双亲委派模式** + + + - 加载器加载类时先把请求委托**给自己的父类加载器执行,直到顶层的启动类加载器.** + - 父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载. + + + +## GC + +CMS GC时出现promotion failed和concurrent mode failure 对于采用CMS进行旧生代GC的 程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当 这两种状况出现时可能会触发Full GC。 +- promotionfailed是在进行Minor GC时,survivor space放 不下、对象只能放入旧生代,而此时旧生代也放不下造成的; + +- concurrent mode failure是在执行 CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 + +- 应对措施为: + - 增大 survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由 于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置`- XX:CMSMaxAbortablePrecleanTime=5`(单位为ms)来避免。 \ No newline at end of file diff --git "a/Java/\345\237\272\347\241\200/Java\351\235\242\350\257\225\345\205\253\350\202\241\346\226\207.md" "b/Java/\345\237\272\347\241\200/Java\351\235\242\350\257\225\345\205\253\350\202\241\346\226\207.md" new file mode 100644 index 0000000..9a4ba96 --- /dev/null +++ "b/Java/\345\237\272\347\241\200/Java\351\235\242\350\257\225\345\205\253\350\202\241\346\226\207.md" @@ -0,0 +1,22 @@ + +# Java面试指南 + +# 常见面试题 + +1. Java的第一印象、特点、区别于其他语言的? 见[《Java基础大杂烩》](./Java基础大杂烩.md) +2. 异常:Error、Exception、Throwable?见[《异常处理》](./异常处理.md) +3. 谈谈 final、finally、 finalize 有什么不同? + - final:是可以用来修饰类、方法、变量。明确语义和意图,不可修改,也保证安全。减少同步开销,省去一些防御性拷贝的必要。 + - 类: 不可继承扩展 + - 在java.lang 包下面的很多类,相当 一部分都被声明成为 final class?在第三方类库的一些基础类中同样如此,这可以有效避免 API 使用者更改基础功能 + - 变量: 不可修改。 + - final 字段对性能的影 响,大部分情况下,并没有考虑的必要。 + - final 不是 immutable!: + - 方法: 不可重写(override) + - finally:是Java 保证重点代码一定要被执行的一种机制。 + - 经常用在try-catch-finally中类似JDBC关闭连接、保证unlock锁等动作,不过关闭资源推荐使用try-with-resources语句。 + - finalize: 是基础类java.lang.Object的一个方法,设计目的是保证对象在被垃圾收集前完成特定资源的回收。(不在推荐使用) + - 不推荐原因:你无法保证 finalize 什么时候执行,执行的是否符合预期。使用不当会 影响性能,导致程序死锁、挂起等。 + >面试官还可以考察你对性能、并发、对象生命周期或垃圾 收集基本过程等方面的理解. + + diff --git "a/Java/\345\237\272\347\241\200/stringDemo.java" "b/Java/\345\237\272\347\241\200/stringDemo.java" new file mode 100644 index 0000000..5a15230 --- /dev/null +++ "b/Java/\345\237\272\347\241\200/stringDemo.java" @@ -0,0 +1,40 @@ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello World!"); + } + + public List> getFull(int[] arr){ + List> res = new ArrayList<>(); + int len = arr.length; + if(len == 0) { + return res; + } + //默认值false + boolean[] visited = new boolean[len]; + back(res, new ArrayList<>(), arr, visited); + return res; + } + + public List> back(List> res, List ans, int[] source, boolean[] visited){ + if(ans.size() == source.length) { + res.add(ans); + } + for (int i = 0; i < source.length; i++) { + if(visited[i]){ + continue; + } + ans.add(source[i]); + visited[i] = true; + back(res, ans, source, visited); + visited[i] = false; + ans.remove(ans.size() - 1); + } + } +} diff --git "a/Java/\345\237\272\347\241\200/\343\200\212Java\346\240\270\345\277\203\346\212\200\346\234\257\351\235\242\350\257\225\347\262\276\350\256\262\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/Java/\345\237\272\347\241\200/\343\200\212Java\346\240\270\345\277\203\346\212\200\346\234\257\351\235\242\350\257\225\347\262\276\350\256\262\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..d5a52e8 --- /dev/null +++ "b/Java/\345\237\272\347\241\200/\343\200\212Java\346\240\270\345\277\203\346\212\200\346\234\257\351\235\242\350\257\225\347\262\276\350\256\262\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,971 @@ +## 强引用、软引用、弱引用、幻象引用有什么区别? + + +# 集合 + +## ArrayList、Vector、LinkedList有何区别? + +- Vector 是 Java 早期提供的**线程安全**的动态数组,如果不需要线程安全,并不建议选择, 毕竟同步是有额外开销的。扩容会创建新的数组,并拷贝原来的数组数据。 扩容1倍。 +- ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。扩容50%。 +- LinkedList 是双向链表,不需要调整容量,**不是线程安全**。 + + +如果事先可以估计到,应用操作是偏向于插入、删除,还是随机访问较多,就可以针对性的进行选择。 + +* **TreeSet** 支持自然顺序访问,但是添加、删除、包含等操作要相对低效(log(n) 时 间)。 +* **HashSet** 则是利用哈希算法,理想情况下,如果哈希散列正常,可以提供常数时间的添 加、删除、包含等操作,但是它不保证有序。 +* **LinkedHashSet**,内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序 遍历的能力,与此同时,也保证了常数时间的添加、删除、包含等操作,这些操作性能略 低于 HashSet,因为需要维护链表的开销。 + * 在遍历元素时,HashSet 性能受自身容量影响,所以初始化时,除非有必要,不然不要 将其背后的 HashMap 容量设置过大。 + * 而对于 LinkedHashSet,由于其内部链表提供的 方便,遍历性能只和元素多少有关系。 + + +**集合排序** + + Java 提供的默认排序算法,具体是什么排序方 式以及设计思路等。 + +这个问题本身就是有点陷阱的意味,因为需要区分是 Arrays.sort() 还是 Collections.sort() (底层是调用 Arrays.sort());什么数据类型;多大的数据集(太小的数据集,复杂排序 是没必要的,Java 会直接进行二分插入排序)等。 +对于原始数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一 种改进的快速排序算法,早期版本是相对传统的快速排序,你可以阅读源码。 + + +而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序 (binarySort)结合的优化排序算法。TimSort 并不是 Java 的独创,简单说它的思路是 查找数据集中已经排好序的分区(这里叫 run),然后合并这些分区来达到排序的目的。 + + +另外,Java 8 引入了并行排序算法(直接使用 parallelSort 方法),这是为了充分利用现 代多核处理器的计算能力,底层实现基于 fork-join 框架(专栏后面会对 fork-join 进行相 对详细的介绍),当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是, 当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境。 + + +## Hashtable、HashMap、TreeMap 有什么不同? + +- Hashtable: 同步,不支持null键和值。 +- HashMap: 不同步,支持 null 键和值。 存取时间接近常数。 +- TreeMap 则是基于红黑树的一种提供**顺序**访问的 Map,它的 get、 put、remove 之类操作都是 O(log(n))的时间复杂度 + + +**Map** + +**继承关系** +- Dictionary:HashTable:Properties +- AbstractMap: HashMap:LinkedHashMap、TreeMap、EnumMap +- EnumMap、HashMap、SortedMap + + +HashMap 的性能表现非常依赖于哈希码的有 效性,请务必掌握 **hashCode 和 equals **的一些基本约定,比如: + +- equals 相等,hashCode 一定要相等。 +- 重写了 hashCode 也要重写 equals。 +- hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致。 +- equals 的对称、反射、传递等特性。 + + +LinkedHashMap 和 TreeMap 都可以保证某种**顺序**,但二者还是非常不同的。 + +- LinkedHashMap 通常提供的是**遍历顺序符合插入顺序**,它的实现是通过为条目(键值 对)维护一个双向链表。注意,通过特定构造函数,我们可以创建反映访问顺序的实例, 所谓的 put、get、compute 等,都算作“访问”。 **插入顺序就是遍历读取的顺序? get操作也算是访问,如果get过会被放在前面** + - 用处:我们构建一个空间占用敏感的资源池,希望可以自动将最不常被访问的对象释放掉,这就可以利用 LinkedHashMap 提供的机制来实现。 + - 构建一个具有优先级的调度系统的问题,其本质就是个 典型的优先队列场景,Java 标准库提供了基于二叉堆实现的 PriorityQueue,它们都是依赖于**同一种排序机制**,当然也包括 TreeMap 的马甲 TreeSet。 + + +### HashMap 源码分析 + +**HashMap 内部实现基本点分析** + +1. HashMap内部结构可以看作是数组(Node[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了 键值对在这个数组的寻址; +2. 哈希值相同的键值对,以链表形式存储,链表大小超过(8),就会被改造为树形结构。 + + +3. putVal 方法本身逻辑非常集中,从初始化、扩容到树化,全部都和它有关。 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbent, boolean evit) { + Node[] tab; Node p; int , i; + if ((tab = table) == null || (n = tab.length) = 0) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == ull) + tab[i] = newNode(hash, key, value, nll); + else { + // ... + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for first + treeifyBin(tab, hash); + // ... +} } +``` + +- 如果表格是 null,resize 方法会负责初始化它,这从 tab = resize() 可以看出。 + + +4. **resize方法**兼顾两个职责: + - 创建**初始存储表格**(如上一段代码) + - 在容量不满足需求的时候(如下一段代码),进行扩容(resize)。 + + +- `threshold=newCap * loadFator;` 如果构建HashMap时没指定就用默认常量值。 +- threshold通常以倍数进行调整(newThr = oldThr << 1)。 + +```java +if (++size > threshold) resize(); +``` + +- 具体键值对在哈希表中的位置(数组 index)取决于下面的位运算: `i = (n - 1) & hash`。 + +- **hash值** + - 为什么这里需要将高位数据移位到低位进行异或运算呢? + >这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希 寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。 +```java +static final int hash(Object kye) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16; +} +``` + +- 扩容后,需要将老的数组中的元素重新放置到新的数组。这是扩容的主要开销来源。 + + + +**容量(capacity)和负载系数(load factor)** + + +容量和负载系数**决定了可用的桶的数量**,空桶太多会浪费空间,如果使用的太满则 +会严重影响操作的性能。极端情况下,假设只有一个桶,那么它就退化成了链表。 + + +既然容量和负载因子这么重要,我们**在实践中应该如何选择**呢? + +- 预先设置的容量需要满足,大于“预估元素数量 / 负载因子”,同时它是 2 的幂 数,结论已经非常清晰了。 + +- 如果没有特别需求,不要轻易进行更改,因为 JDK 自身的默认负载因子是非常符合通用 场景的需求的。 +- +- 如果确实需要调整,建议不要设置超过 0.75 的数值,因为会显著增加冲突,降低 HashMap 的性能。 +- 如果使用太小的负载因子,按照上面的公式,预设容量值也进行调整,否则可能会导致更加频繁的扩容,增加无谓的开销,本身访问性能也会受影响。 + + + + +**树化** + +对应逻辑主要在 putVal 和 treeifyBin 中: + + +```java + final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + else if ((e = tab[index - (n - 1) & hash]) != null){ + //树化逻辑 + } + +``` + +当 bin 的数量大于 TREEIFY_THRESHOLD 时: +- 如果容量小于 MIN_TREEIFY_CAPACITY,只会进行简单的扩容。 +- 如果容量大于 MIN_TREEIFY_CAPACITY ,则会进行树化改造。 + + +## 如何保证容器是线程安全的?ConcurrentHashMap 如何实现高 效地线程安全? + + +### ConcurrentHashMap 分析 + +**演化的过程** + + +#### 1. 早期Java7: 分段、分离锁 +- 分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。 +- HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。 +- **Segment 的数量**由所谓的 concurrentcyLevel 决定,默认是 16,也可以 在相应构造函数直接指定。注意,Java 需要它是 2 的幂数值 + +- get 操作:需要保证的是可见性, 所以并没有什么同步逻辑。 + - 计算hash,找到位置 + - Unsafe直接进行volatile access +- put 操作: 首先是通过二次哈希避免哈希冲突,然后以 Unsafe 调用方式,直接获 取相应的 Segment,然后进行线程安全的 put 操作。 + - put要先获取锁,锁的是segment。 + - ConcurrentHashMap 会获取再入锁,以保证数据一致性,Segment 本身就是基于 ReentrantLock 的扩展实现,所以,在并发修改期间,相应 Segment 是被锁定的。 + - 在最初阶段,进行重复性的扫描,以确定相应 key 值是否已经在数组里面,进而决定是 更新还是放置操作。重复扫描、检测冲突是 ConcurrentHashMap 的常见技巧。 + - 扩容是单独对Segment扩容。 +- **分离锁副作用**: size 方法 计算 和初始化操作耗时。如果不进行同步,简单的计算所有 Segment 的总值,可能会因为并发 put,导致结 果不准确,但是直接锁定所有 Segment 进行计算,就会变得非常昂贵。 + - ConcurrentHashMap 的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重 试次数 2),来试图获得可靠值。 + - 如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。 + + +#### 2. Java8 的一些变化:去掉分段,CAS、volatile、Unsafe + +1. 其内部仍然有 Segment 定义,但仅仅是**为了保证序列化时的兼容性**而已,**不再有任何结构上的用处**。 +2. 因为不再使用 Segment,**初始化操作大大简化**,修改为 **lazy-load** 形式,这样可以有效 避免初始开销。 +3. 数据存储利用 volatile 来保证可见性。 +4. 使用 CAS 等操作,在特定场景进行无锁并发操作。 +5. 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。 +6. 1.8以后的锁的颗粒度,是加在链表头上的 + + +具体看数据存储内部实现: +- key是final的,因为在生命周期中,一个条目的 Key不可能变化; +- val声明为volatile,以保证其可见性。 + + +**put方法的实现** + +补充源码 + + +- 在同步逻辑上,它使用的是 synchronized。 + >synchronized相比于 ReentrantLock,它可以减少内存消耗,这是个 非常大的优势。 + +- size计算: + - 真正的逻辑是在 sumCount 方法中。 思路仍然和以前类似,都是分而治之的进行计数,然后求和处理,但实现却 基于一个奇怪的 CounterCell。 + - 对于 CounterCell 的操作,是基于 java.util.concurrent.atomic.LongAdder 进行 的,是一种 JVM 利用空间换取更高效率的方法,利用了Striped64内部的复杂逻辑。 + + + +## 设计模式 + +- 希望你写一个典型的设计模式实现。这虽然看似简单,但即使是最简单的单例,也能够综合考察代码基本功。 + + +- 考察典型的设计模式使用,尤其是**结合标准库或者主流开源框架**,考察你对业界良好实践的掌握程度。 + + +### 写个单例模式 + + + +2. 利用内部类持有静态对象的方式实现,其理论依据是对象初始化过程 中隐含的初始化锁。 +```java +public class Singleton { + private Singleton() {} + private static Singleton getSingleton(){ + return Holder.singletion; + } + + private static class Holder { + private static Singleton single = new Singletion(); + } +} + +``` + + +3. 其实实践中未必需要如此复杂,如果我们看 Java 核心类库自己的 单例实现,比如java.lang.Runtime,你会发现: **它并没使用复杂的双检锁之类。** + 1. 静态实例被声明为 final,这是被通常实践忽略的,一定程度保证了实例不被篡改 +```java +public class Runtime { + private static final Runtime currentRuntime = new Runtime(); + private static Version version; + //.. + public static Runtime getRuntime(){ + return currentRuntime; + } + + private Runtime() {} +} + +``` + + +### Spring用了哪些设计模式? + +- BeanFactory和ApplicationContext应用了工厂模式。 +- 在 Bean 的创建中,Spring 也为不同 scope 定义的对象,提供了单例和原型等模式实现。 +- AOP 领域则是使用了代理模式、装饰器模式、适配器模式等。 +- 各种事件监听器,是观察者模式的典型应用。 +- 类似 JdbcTemplate 等则是应用了模板模式。 + + +### 11. Java提供了哪些IO方式? NIO如何实现多路复用? + +1. 传统的 java.io 包,它基于**流模型**实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。 + 1. 交互方式是同步、阻塞的方式。 读取写入流在读写动作完成之前,线程会一直阻塞在那里,他们之间调用是可靠的线性顺序。 + 2. 好处是代码比较简单直观,缺点是IO效率和扩展性存在局限。 +2. Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、 Buffer 等新的抽象,可以构建**多路复用的、同步非阻塞** IO 程序,同时提供了更接近操作系 统底层的高性能数据操作方式。 +3. 在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了**异步非阻塞** IO 方 式,也有很多人叫它 AIO(Asynchronous IO)。 + 1. 异步 IO 操作**基于事件和回调**机制 + 2. 可以理解为:应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相 应线程进行后续工作。 + + + +#### BIO、NIO、NIO 2(AIO) + + +**基础 API 功能与设计, InputStream/OutputStream 和 Reader/Writer 的关系和区别。** + +- 输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作 图片文件。 +- 而 Reader/Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中 读取或者写入文本信息。 +- Reader/Writer 相当于构建了应用逻辑和原始数据之间的桥梁。 +- BufferedOutputStream 等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高 IO 处 理效率。 +- 很多 IO 工具类都实现了 Closeable 接口,因为需要进行资源的释 放。比如,打开 FileInputStream,它就会获取相应的文件描述符(FileDescriptor), 需要利用 try-with-resources、 try-finally 等机制保证 FileInputStream 被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。 +- File +- RandomAccessFile +- InputStream + - FilterInputStream --> BufferedInputStream + - BytesArrayInputStream、ObjectInputStream、PipeInputStream +- OutputStream + - FilterOutputStream --> BufferedOutputStream + - BytesArrayOutputStream、ObjectOutputStream、PipeOutputStream +- Reader + - InputStreamReader-->FileReader + - BufferedReader、PipeReader +- Writer + - OutputStreamWriter-->FileWriter + - BufferedWriter/PipeWriter + + +**NIO、NIO 2 的基本组成。** + +1.Java NIO 概览 + +- 首先,熟悉一下 NIO 的主要组成部分: + - **Buffer**,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。 + - **Channel**,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量 式 IO 操作的一种抽象。File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel 则是更加操作系统底层 的一种抽象 + - **Selector**,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对 多 Channel 的高效管理。 + + + +**给定场景,分别用不同模型实现,分析 BIO、NIO 等模式的设计和实现原理。** + +2.NIO 能解决什么问题? + + NIO 多路复用: + - 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。 + - 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。 + - Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。 + - NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,**仅仅 select 阶段是阻塞的**,可以有效避免大量客户端连接时,频繁线程切换带来 的问题。 + +> **局限性**: +> - 当每个channel所进行的都是耗 时操作时,由于是同步操作,就会积压很多channel任务,从而影响性能。 +> - 如果回调时客户端做了重操作,就会影响调度,导致后续的client回调缓慢。 + + +AIO 实现异步IO,利用事件和回调处理 Accept、Read 等操作。: +- Future、CompletionHandler +- Reactor、 Proactor 模式 +- 业务逻辑的关键在于,通过指定 CompletionHandler 回调接口,在 accept/read/write 等关键节点,通过事件机制调用。 + + +**NIO 提供的高性能数据操作方式是基于什么原理,如何使用?** + +- **Linux 上依赖于 epoll**。 +- Windows 上 NIO2(AIO)模式则是依赖于 iocp。 + + +## Java 有几种文件拷贝方式?哪一种最高效? + +1. 利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建 一个 FileOutputStream,完成写入工作。 +2. 利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。 +3. Java 标准库也提供了文件拷贝方法 (java.nio.file.Files.copy) + + +>对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO **transferTo/From 的方式可能更快**,因为它更能利用现代操作系统底层机制,避免不必要 拷贝和上下文切换。 + + + +- [ ] 不同的 copy 方式,底层机制有什么区别? +- [ ] 为什么零拷贝(zero-copy)可能有性能优势? +- [ ] Buffer 分类与使用。 +- [ ] Direct Buffer 对垃圾收集等方面的影响与实践选择。 + + +1. 拷贝**实现机制**分析 + +- **用户态空**间(User Space) : 操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权 + +- **内核态**空间(Kernel Space) : 给普通应用和服务使用 + + +当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据 +时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写操作也类似。 +>所以,这种方式会带来一定的额外开销,可能会降低 IO 效率。 + + +基于 NIO transferTo 的实现方式,在 Linux 和 Unix 上,则会使用到**零拷贝**技术,数据传输并**不需要用户态参与**,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。 + + +2.Java **IO/NIO 源码结构** + + +Java 标准库也提供了文件拷贝方法 (java.nio.file.Files.copy) + - 有几种不太的copy方法,可以自己看源码。可以看到,copy 不仅仅是支持文件之间操作,后面两种 copy 实现,能够在方法实现里直接看到使用的是 + - InputStream.transferTo(),你可以直接看源码,其内部实现其实是 stream 在用户态的读写; + - NIO 部分代码甚至是定义为模板而不是 Java 源文件,在 build 过程自 动生成源码。原来文件系统实际逻辑存在于 JDK 内部实现里,公共 API 其实是通过 ServiceLoader 机 制加载一系列文件系统实现,然后提供服务。 + +**如何提高类似拷贝等IO操作的性能**,有一些宽泛的原则: + +- 在程序中,使用缓存等机制,合理减少 IO 次数(在网络通信中,如 TCP 传输,window 大小也可以看作是类似思路)。 +- 使用 transferTo 等机制,减少上下文切换和额外 IO 操作。 +- 尽量减少不必要的转换过程,比如编解码;对象序列化和反序列化,比如操作文本文件或者网络通信,如果不是过程中需要使用文本信息,可以考虑不要将二进制信息转换成字符串,直接传输二进制信息。 + + +3. 掌握 **NIO Buffer** + +Java 为每种原始数据类型都提供了相应的Buffer 实现(布尔除外),所以掌握和使用 Buffer 是十分必要的,尤其是涉及 Direct Buffer 等使用,因为其在垃圾收集等方面的特殊性,更要重点掌握。 + + +Buffer 有几个基本属性: + +* capcity,它反映这个 Buffer 到底有多大,也就是数组的长度。 +* position,要操作的数据起始位置。 +* limit,相当于操作的限额。在读取或者写入时,limit 的意义很明显是不一样的。 + * 读取操作时,很可能将 limit 设置到所容纳数据的上限; + * 而在写入时,则会设置容量或容 量以下的可写限度。 + * mark,记录上一次 postion 的位置,默认是 0,算是一个便利性的考虑,往往不是必须 的。 + + +4.**Direct Buffer 和垃圾收集** + +- Direct Buffer:如果我们看 Buffer 的方法定义,你会发现它定义了 isDirect() 方法,返回当前 Buffer 是否是 Direct 类型。这是因为 Java 提供了堆内和堆外(Direct) Buffer,我们可以以它的 allocate 或者 allocateDirect 方法直接创建。 +- MappedByteBuffer:它将文件按照指定大小直接映射为内存区域,当程序访问这个内存 区域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。我们可以使用FileChannel.map创建 MappedByteBuffer,它本质上也是种 Direct Buffer。 + + +Java 会尽量对 Direct Buffer 仅做本地 IO 操作,对于很多大数据量的 IO 密集操作,可能会带来非常大的性能优势,因为: + +- Direct Buffer **生命周期内内存地址都不会再发生更改**,进而内核可以安全地对其进行访问,很多 IO 操作会很高效。 +- 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高。 +- Direct Buffer 创建和销毁过程中,都会比一般的堆内 Buffer 增加部分开销, 所以通常都建议用于**长期使用、数据较大**的场景。 +- 使用 Direct Buffer不在堆上, 所以 Xmx 之类参数,其实并不能影响 Direct Buffer 等堆外成员所使用的内存额度,我们可以使用下面参数设置大小:`-XX:MaxDirectMemorySize=512M` + - 这意味着我们在计算 Java 可以使用的内存大小的时 候,不能只考虑堆的需要,还有 Direct Buffer 等一系列堆外因素。如果出现内存不足,堆 外内存占用也是一种可能性。 + - 大多数垃圾收集过程中,都不会主动收集 Direct Buffer,它的垃圾收集过程是基于Cleaner(一个内部实现)和幻象引用 (PhantomReference)机制。其本身不是 public 类型,内部实现了一个 Deallocator 负 责销毁的逻辑。对它的销毁往往要拖到 full GC 的时候,所以使用不当很容易导致 OutOfMemoryError。 + + +对于 Direct Buffer 的回收,我有几个建议: +- 在应用程序中,显式地调用 System.gc() 来强制触发。 +- 另外一种思路是,在大量使用 Direct Buffer 的部分框架中,框架会自己在程序中调用释放方法,Netty 就是这么做的。 +- 重复使用 Direct Buffer。 + + +5. 跟踪和诊断 Direct Buffer 内存占用? + +- 在 JDK 8 之后的版本,我们可以方便地使用 Native Memory Tracking(NMT)特性来进行诊断。启动参数:`-XX:NativeMemoryTracking={summary|detail}` +- 注意,激活 NMT 通常都会导致 JVM 出现 5%~10% 的性能下降,请谨慎考虑。 + +## 谈谈接口和抽象类有什么区别? + +**接口** + +- 接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。 +- 接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义; +- 、没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。 +- Java 标准类库中,定义了非常多的接口,比如 java.util.List。 + + +**抽象类** + +- 抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。 +- 可以有一个或者多个抽象方法,也可以没有抽象方法。 +- 抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量, 然后通过继承的方式达到代码复用的目的。 +- Java 标准库中,比如 collection 框架,很多通 用部分就被抽取成为抽象类,例如 java.util.AbstractList。 + + +```java +public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable +``` + +**深入理解,考点** + + +1. 对于 Java 的基本元素的语法是否理解准确。能否定义出语法基本正确的接口、抽象类或 者相关继承实现,涉及重载(Overload)、重写(Override)更是有各种不同的题目。 +2. 在软件设计开发中妥善地使用接口和抽象类。你至少知道典型应用场景,掌握基础类库重 要接口的使用;掌握设计方法,能够在 review 代码的时候看出明显的不利于未来维护的设计。 +3. 掌握 Java 语言特性演进。现在非常多的框架已经是基于 Java 8,并逐渐支持更新版本, 掌握相关语法,理解设计目的是很有必要的。 + + + +**Java 不支持多继承。** + +- 这种限制,在规范了代码实现的同时,也产生了一些局限性,影响着程序设计结构。 Java 类可以实现多个接口,因为接口是抽象方法的集合,所以这是声明性的,但不能通过扩展多个抽象类来重用逻辑。 +- 在一些情况下存在特定场景,需要抽象出与具体实现、实例化无关的通用逻辑,或者纯调用 关系的逻辑,但是使用传统的抽象类会陷入到单继承的窘境。以往常见的做法是,实现由静 态方法组成的工具类(Utils),比如 java.util.Collections。 + - 为接口添加任何抽象方法,相应的所有实现了这个接口的类,也必须实现新增方法,否则会出现编译错误。 + - 对于抽象类,如果我们添加非抽象方法,其子类只会享受到能力扩展,而不用担心编译出问题。 +- 有一类**没有任何方法的接口**,通常叫作 Marker Interface,顾名思义,它的目的就是为了声明某些东西,比如我 们熟知的 Cloneable、Serializable 等。 类似Annotation,不过后者因为其可以指定参数和值,在表达能力上要更强大一些。 + + +**Java 8 以后,接口也是可以有方法实现的!** +- 对 default method 的支持。 +- Default method 提供了一种二进制兼容的扩展已有接口的 办法。比如,我们熟知的 java.util.Collection,它是 collection 体系的 root interface, 在 Java 8 中添加了一系列 default method,主要是增加 Lambda、Stream 相关的功 能。 + + +**面向对象设计** + +1. 基本要素:封装、继承、多态。 + +- **封装** + - 目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。 + - 封装提供了合理的边 界,避免外部调用者接触到内部的细节。 + - 避免太多无意义的细节浪费调用者的精力 +- **继承** + - 是代码复用的基础机制 + - 但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。 + - 过度滥用继承会起到反作用 +- **多态** + - 重写是父子类中相同名字和参数的方法,不同的实现; + - 重载则是相同名字的方法,但是不同的 参数,本质上这些方法签名是不一样的。 + - 向上转型 + + +2. 基本设计原则:SOLID + +- **单一职责**(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如 果发现某个类承担着多种义务,可以考虑进行拆分。 +- **开关原则**(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。 程序设计应保证平滑的扩展性,尽量避免因为新增同类 功能而修改已有实现,这样可以少产出些回归(regression)问题。 +- **里氏替换**(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,**凡是可以用父类或者基类的地方,都可以用子类替换**。 +- **接口分离**(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里 定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏 了程序的内聚性。 +- **依赖反转**(Dependency Inversion),实体应该依赖于抽象而不是实现。 + + +# 并发 + +## synchronized和ReentrantLock有什么区别呢? + +synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了 互斥的语义和可见性。 + +synchronized 可以用来修饰方法,也可以使用在特定的代码块。 + + +ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现。 +- 再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵 活。 +- 必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。 + + +### 理解什么是线程安全 + +> 推荐看Brain Goetz 等专家撰写的《Java 并发编程实战》 + +线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境 下共享的、可修改的状态的正确性,这里的状态反映在程序中其实可以看作是数据。 + + +换个角度来看,如果**状态不是共享的**,或者**不是可修改的**,也就不存在线程安全问题,进而可以推理出保证线程安全的两个办法: + +- 封装: 通过封装,我们可以将对象内部状态隐藏、保护起来。 +- 不可变: final 和 immutable。 Java 语言目前还没有真正意义上的原生不可变,但是未来也许会引入。 + + +**线程安全**需要保证几个**基本特性**: + +- **原子性**,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。 +- **可见性**,是一个线程修改了某个共享变量,其状态能够**立即被其他线程知晓**,通常被解释为将线程本地状态**反映到主内存**上,`volatile`就是负责保证可见性的。 +- 有序性,是保证线程内串行语义,避免指令重排等。 + + +### synchronized、ReentrantLock 等机制的基本使用与案例。 + +原子性: 加synchronized保护起来,使用this作为互斥单元。。。 +- 如果用 javap 反编译,可以看到类似片段,利用 `monitorenter/monitorexit` 对实现了同 步的语义。 + +```java +synchronized (this) { + int former = sharedState ++; + int latter = sharedState; + //... +} + +synchronized (ClassName.class) {} +``` + +**ReentrantLock** + +- ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。 +- Lock使用起来比较灵活,但是必须有释放锁的配合动作 + + +什么是再入? +>它是表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位而不是基于调用次数。 + +Java 锁实现强调再入性是为了和 pthread 的行为进行区分。 + +再入锁可以设置公平性(fairness),我们可在创建再入锁时选择是否是公平的,当公平性为真时,会倾向于将锁赋予等待时间最久的 +线程。`ReentrantLock fairLock = new ReentrantLock(true);` + +- 公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。 +- 若要保证公平性则会引入额外开销,自然 会导致一定的吞吐量下降。所以,我建议只有当你的程序确实有公平性需要的时候,才有必 要指定它。 + + +ReentrantLock 相比 synchronized,因为可以像普通对象一样使用,所以可以利用其提供 的各种便利方法,进行精细的同步操作,甚至是实现 synchronized 难以表达的用例,如: + +- 带超时的获取锁尝试。 +- 可以判断是否有线程,或者某个特定线程,在排队等待获取锁。 - 可以响应中断请求。 + + +**条件变量**(java.util.concurrent.Condition) + +- 相当于将 wait、notify、notifyAll 等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。 +- 条件变量最为典型的应用场景就是标准类库中的 ArrayBlockingQueue 等。 + + +Java6之后,在高竞争情况下,ReentrantLock 仍然有一定优势。并发在4个线程以下synchronized效果更好,越大,lock性能越好。 + + +- [ ] 掌握 synchronized、ReentrantLock 底层实现 +- [ ] 理解锁膨胀、降级; +- [ ] 理解偏斜锁、自旋锁、轻量级锁、重量级锁等概念。 +- [ ] 掌握并发包中 java.util.concurrent.lock 各种不同实现和案例分析。 + + +## synchronized 底层如何实现? 什么是锁的升级、降级? + +synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,**Monitor对象是同步的基本实现单元**。 + +- 在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的**互斥锁**,因为**需要进行用户态到内核态的切换**,所以同步操作是一个无差别的重量级操作。 +- 所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。 +- 初始状态时默认是偏向锁时,线程请求先通过CAS替换mark word中threadId,如果替换成功则该线程持有当前锁。如果 替换失败,锁会升级为轻量级锁, + + +### 偏向锁 + +- 当**没有竞争**出现时,**默认会使用偏斜锁**。 +- JVM 会利用 CAS 操作在对象头上的`Mark Word`部分设置**线程ID**,以表示这个对象偏向于当前线程。 +- 这么做的假设是基于在很多应用场景中,**大部分对象生命周期被一个线程锁定**,使用偏斜锁可以降低无竞争开销。 + + +如果有**另外的线程试图锁定某个已经被偏斜过的对象**,JVM 就需要撤销(revoke)偏斜锁,并**切换到轻量级锁**实现。 + + +### 轻量级锁 + +- 轻量级锁依赖 CAS 操作 Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。 + + +>当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。 + +作者说---我个人认为,能够基础性地理解这些概念和机制,其实对于大多数并发编程已经足够了,毕竟大部分工程师未必会进行更底层、更基础的研发,很多时候解决的是知道与否,**真正的提高还要靠实践踩坑**。 + + +#### 锁升级过程 + +synchronized 是 JVM 内部的 Intrinsic Lock,所以偏斜锁、轻量级 锁、重量级锁的代码实现,并不在核心类库部分,而是在 JVM 的代码中。 + + +1. 首先,synchronized 的行为是 JVM runtime 的一部分,所以我们需要先找到 Runtime 相关的功能实现。通过在代码中查询类似`“monitor_enter”`或`“Monitor Enter”`,很直观的就可以定位到代码: + +```c++ +sharedRuntime.cpp/hpp,它是解释器和编译器运行时的基类。 +synchronizer.cpp/hpp,JVM 同步相关的各种基础逻辑。 + +//在 sharedRuntime.cpp 中,下面代码体现了 synchronized 的主要逻辑。 + +Handle h_obj(THREAD, obj); + if (UseBiasedLocking) { + // Retry fast entry if bias is revoked to avoid unnecessary inflation + ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK); + } else { + ObjectSynchronizer::slow_enter(h_obj, lock, CHECK); + } + +``` + +- UseBiasedLocking: 是一个检查,因为在JVM启动时可以指定是否启用偏向锁; +- fast_enter 是我们熟悉的完整锁获取路径; +- slow_enter 则是绕过偏斜锁,直接进入轻量级锁获取逻辑。 + +>偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的 synchronized 块儿时,才能体现出明显改善。 有人认为当你需要大量使用并发库时,就意味着并发高也就是不需要偏向锁。 建议最好是在实践中进行测试。 + +偏斜锁会延缓 JIT 预热的进程,所以很多性能测试中会显式地关闭偏斜锁, 命令如下:`-XX:-UseBiasedLocking` + + +类似 `fast_enter` 这种实现,解释器或者动态编译器,都是拷贝`synchronizer.cpp`这段基础逻辑,所以如果我们修改这部分逻辑,要保证一致性。微小的 问题都可能导致死锁或者正确性问题。 + +```c++ +void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS){ + if (UseBiasedLocking) { + if (!SafepointSynchronize::is_at_safepoint()){ + BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_reb... + if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { + return; + } + } else { + assert(!attempt_rebias, "can not rebias toward VM thread"); + BiasedLocking::revoke_at_safepoint(obj); + } + assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); + } + slow_enter(obj, lock, THREAD); +} +``` + +- biasedLocking定义了偏斜锁相关操作 + - revoke_and_rebias 是获取偏斜锁的入口方法 + - revoke_at_safepoint 则定义了当检测到安全点时的处理逻辑。 +- 如果获取偏斜锁失败,则进入 slow_enter。 + +>这个方法里面同样检查是否开启了偏斜锁,如果关闭了偏斜锁,是不会进入这个方法的,所以算是个额外的保障性检查吧。 + + +太多细节就不展开了,明白它是通过 **CAS 设置 Mark Word** 就完全够用了,对象头中 Mark Word 的结构,可以参考下图: 被偏斜的对象,对象头前部有个`Thread pointor`和`Epoch`。 + + +2. **轻量级锁** + +slow_enter: +```c++ +void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { + markOop mark = obj->mark(); + if(mark->is_neutral()) { + // 将目前的 Mark Word 复制到 Displaced Header 上 + lock->set_displaced_header(mark); + // 利用 CAS 设置对象的 Mark Word + if (mark == obj()->cas_set_mark((markOop) lock, mark)) { + TEVENT(slow_enter: release stacklock); + return; + } + //检查存在竞争 + } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { + //clean + ock->set_displaced_header(NULL); + return; + } + + // 重置 Displaced Header + lock->set_displaced_header(markOopDesc::unused_mark()); + ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD); +} + +//更多细节: +// deflate_idle_monitors是分析锁降级逻辑的入口,这部分行为还在进行持续改进,因为 其逻辑是在安全点内运行,处理不当可能拖长 JVM 停顿(STW,stop-the-world)的 时间。 + +//fast_exit 或者 slow_exit 是对应的锁释放逻辑。 +``` + +- 设置 Displaced Header,然后利用 cas_set_mark 设置对象 Mark Word,如果成功就成功获取轻量级锁。 +- 否则 Displaced Header,然后进入锁膨胀阶段,具体实现在 inflate 方法中。 + + +### 其他的一些特别的锁类型 + +1. ReadWriteLock 是一个单独的接口,它通常是代表了一对儿锁,分别对应只读和写操作,标准类库中提供了再入版本的读写锁实现(ReentrantReadWriteLock),对应的语义和 ReentrantLock 比较相似。 +2. StampedLock 竟然也是个单独的类型,从类图结构可以看出它是不支持再入性的语义的, 也就是它不是以持有锁的线程为单位。 + + +**为什么我们需要读写锁(ReadWriteLock)等其他锁呢?** + +- 虽然ReentrantLock 和 synchronized 简单实用,但是行为上有一定局限性, 通俗点说就是“太霸道”,**要么不占,要么独占**。 +- 有的时候不需要大量竞争的写操作,而是以并发读取为主。 +- Java 并发包提供的读写锁等扩展了锁的能力,它所基于的原理是**多个读操作是不需要互斥的**,因为读操作并不会更改数据,所以不存在互相干扰。 + - 在运行过程中,如果**读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得**,而只好等待对方操作结束,这样就可以**自动保证不会读取到有争议的数据**。 + + +读写锁看起来比 synchronized 的粒度似乎细一些,但在实际应用中,其表现也并不尽如人意,主要还是因为**相对比较大的开销**。 +> 啥开销? + +所以,JDK 在后期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。 优化读基于假设,**大多数情况下读操作并不会和写操作冲突**,其逻辑是: + - **先试着读** + - 然后通过**validate方法**确认是否进入了写模式 + - 如果没有进入,就成功避免了开销; + - 如果进入,则尝试获取读锁。 + +>请注意:writeLock 和 unLockWrite 一定要保证成对调用。 + + +Java 并发包内的各种同步工具,不仅仅是各种 Lock,其他的如Semaphore、CountDownLatch,甚至是早期的FutureTask等,都是基 于一种AQS框架。 + + +**自旋锁** + +- 基于大部分的锁都是使用很短的时间的, 获取不到锁等一下就可能后去到的假设。 +- 当获取锁失败的时候,不进入休眠等待(操作系统层面挂起,重新唤醒有个内核切换花销),而是继续“运动”做几个空循环,再进行尝试获取锁。 超过一定次数才正式挂起。 +- **好处**: 减少线程阻塞,内核用户态上下文切换开销。 适用于在锁竞争不激烈,占用锁非常短的情况。 属于乐观锁。 +- **缺点**:消耗CPU,单cpu无效,因为基于cas的轮询会占用cpu,导致无法做线程切换。 + + + +## 一个线程两次调用start()方法会出现什么情况? + +**典型回答**: +>Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException, 这是一种运行时异常,多次调用 start 被认为是编程错误。 + + +### 线程生命周期的不同状态 + +在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 `java.lang.Thread.State` 中,分别是: +- **新建(New)**: 表示线程被创建出来还没真正启动的状态 +- **就绪(Runnable)**: 表示该线程已经在 JVM 中**执行**,当然由于执行需要计算资源,它**可能是正在运行**,也可能还在**等待系统分配给它CPU片段**,在就绪队列里面排队。 + >在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。 +- **阻塞(Blocked)**: 表示线程在等 待 Monitor lock。 + >比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已 经独占了,那么当前线程就会处于阻塞状态。 +- **等待(Waiting)**: 表示正在等待其他线程采取某些操作。类似的如生产者消费者模式,发现条件为满足就让线程等待(wait),条件满足通过类似notify等动作,通知消费线程继续工作。 + >Thread.join() 也会令线程进入等待状态。 +- **计时等待(Time_Wait)**: 与等待状态类似,调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本 +- **终止(Terminated)**: 不管是意外退出还是正常执行结束,线程已经完成使命,终止 运行 + + +### 线程到底是什么以及 Java 底层实现方式 + +**是什么**? + +- 线程是系统调度的最小单元,一个进程可以包含多个线 程。 +- 作为任务的真正运作者,有自己的栈(Stack)、寄存器(Register)、本地存储 (Thread Local)等, +- 会和进程内其他线程共享文件描述符、虚拟地址空间等。 + + +**Java底层实现方式** + +线程还分为内核线程、用户线程,Java 的线程实现其实是与虚拟机相关的。 + +在 Java 1.2 之后,JDK 已经抛弃了所谓的Green Thread,也就是用户调度的线程,现在的模型是**一对一映射到操作系统内核线程**。 + +如果我们来看 Thread 的源码,你会发现其基本操作逻辑大都是以**JNI 形式调用的本地代码**。 + +- Java 语言得益于精细粒度的线程和相关的并发操作,其 构建高扩展性的大型应用的能力已经毋庸置疑。 +- 其复杂性也提高了并发编程的门槛,go语言的协程大大提高构建并发应用的效率 +- Java 也在Loom项目中,孕育新的类似轻量级用户线程(Fiber)等机制 + + +如何创建线程? +- 直接扩展 Thread 类,然后实例化。 +- 实现一个 Runnable,将代码逻放在 Runnable 中,然后构建 Thread 并启动(start),等 待结束(join)。 +- **好处:** 不会受Java 不支持类多继承的限制,重用代码实现,当我们需要重 复执行相应逻辑时优点明显。 + + +### 线程状态的切换,以及和锁等并发工具类的互动 + +有哪些因素可能影响线程的状态呢?主要有: + +- **线程自身的方法**,除了 start,还有多个 join 方法,等待线程结束;yield 是告诉调度 器,主动让出CPU; 被标记为过时的 resume、stop、suspend + +- **基类 Object** 提供了一些基础的 wait/notify/notifyAll 方法。 + - 如果我们持有某个对象的 Monitor 锁,调用 wait 会让当前线程处于等待状态,直到其他线程 notify 或者 notifyAll。 + - 所以,本质上是提供了 Monitor 的获取和释放的能力,是基本的线程间通信 方式。 + +- **并发类库**中的工具,比如 CountDownLatch.await() 会让当前线程进入等待状态,直到 latch 被基数为 0,这可以看作是线程间通信的 Signal。 + + +有了并发包,大多数情况下,我们已经不再 需要去调用 wait/notify 之类的方法了。 + + +**守护线程(Daemon Thread)**,有的时候应用中需要一个长期驻留的服务程序, 但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,**将结束进程**。 注意,**必须在线程启动之前设置。** + + +### 线程编程时容易踩的坑与建议 + +**Spurious wakeup** + +>尤其是在多核 CPU 的系统中,线程等待存在一种可能,就是 在没有任何线程广播或者发出信号的情况下,线程就被唤醒,如果处理不当就可能出现诡异 的并发问题. + +所以我们在等待条件过程中,建议采用下面模式来书写。 +```java +// 推荐 +while ( isCondition()) { + waitForAConfition(...); +} + +// 不推荐,可能引入 bug +if ( isCondition()) { + waitForAConfition(...); +} + +``` + +自旋锁”(spin-wait, busy-waiting),也可以认为其不算是一种锁,而是一种**针对短期等待的性能优化**技术。 + + +**慎用ThreadLocal** + + + Java 提供的一种**保存线程私有信息**的机制,因为其在**整个线程生命周期内有效**,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务 ID、Cookie 等上下文相关信息。 + + - 数据存储于线程相关的 ThreadLocalMap,其内部条目是 **弱引用**. + +```java +static class Entry extends ThreadLocalMap { + static class Entry extends WeakReference> { + Object value; + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } + } +} +``` + +- 当 Key 为 null 时,该条目就变成“废弃条目”,相关“value”的回收,往往依赖于几个 关键点,即 set、remove、rehash。 +- 通常弱引用都会和**引用队列配合清理机制使用**,但是 ThreadLocal 是个**例外**,它并没有这么做。 + - 这意味着,**废弃项目的回收依赖于显式地触发**,否则就要等待线程结束,进而回收相应 ThreadLocalMap! + - 这就是很多 OOM 的来源,所以通常都会建议,**应用一定要自己负责remove**,并且**不要和线程池配合**,因为 worker 线程往往是不会退出的。 +>theadlocal里面的值如果是线程池的线程里面设置的,当任务完成,线程归还线程池时, 这个threadlocal里面的值是不是不会被回收? + + +## 第18讲 | 什么情况下Java程序会产生死锁?如何定位、修复? + +死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。 + +- 死锁不仅仅是在线程之间会发生,**存在资源独占的进程之间同样也可能出现死锁**。 +- 通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,**由于互相持有对方需要的锁,而永久处于阻塞的状态。** + + +**定位** + +定位死锁最常见的方式就是**利用jstack等工具获取线程栈**,然后定位互相之间的依赖关 系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位 + + +### 写一个可能死锁的程序,考察下基本的线程编程 + +```java +public class DeadLockSample extends Thread { + private String first; + private String second; + public DeadLockSample(String name, String first, String second) { + super(name); + this.first = first; + this.second = second; + } + + public void run() { + synchronized(first){ + System.out.println(this.name + " obtained: " + first); + try { + Thread.sleep(1000L); + synchronized(second) { + System.out.println(this.name + " obtained: " + second); + } + } catch (InterruptedException e) { + //Do nothing + } + } + } + + public static void main(String[] args) throws InterruptedException { + String lockA = "lockA"; + String lockB = "lockB"; + DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB); + DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + } +} +``` + +先调用 Thread1 的 start,但是可能Thread2 却先打印出来了呢? +>这就是因为线程调度依赖于(操作系统)调度器,虽然你可以通过优先级之类进行影响,但 是具体情况是不确定的。 + + +**定位流程** + +1. jps、ps看进程ID +2. 调用 jstack 获取线程栈: `jstack your_pid` +3. 分析得到的输出: ,找到处于 BLOCKED 状态的线 程,按照试图获取(waiting)的锁 ID查找,很快就定位 问题. +4. 结合代码分析线程栈信息. + + +**区分线程状态 -> 查看等待目标 -> 对比 Monitor 等持有状态** + +### 诊断死锁的工具,分布式环境下能否用API实现? + +使用 Java 提供的标准管理 API,ThreadMXBean,其直接就提供了 findDeadlockedThreads() 方法用于定位。 + + +### 如何在编程中尽量避免一些典型场景的死锁 + +**发生死锁的原因**: +- 互斥条件,类似 Java 中 Monitor 都是独占的,要么是我用,要么是你用。 +- 互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。 +- 循环依赖关系,两个或者多个个体之间出现了锁的链条环。 + + +**避免**: +- 尽量避免使用多个锁,并且只有需要时才持有锁。 +- 如果必须使用多个锁,尽量设计好锁的获取顺序,这个说起来简单,做起来可不容易,你可以参看著名的银行家算法。 +- 使用带超时的方法,为程序带来更多可控性。 +- 通过静态代码分析(如 FindBugs)去查找固定的模 式,进而定位可能的死锁或者竞争情况。 + - 类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中 + + +有时候并不是阻塞导致的死锁,只是某个线程进入了死循环,导致其他线程一直等待,这种问题如何诊断呢? + +>CPU使用量飙升,用`top -Hp`看使用率高的pid,转换为16进制,去jstack搜索线程状态。 + + +## 第19讲 | Java 并发包提供了哪些并发工具类? + \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\345\274\202\345\270\270\345\244\204\347\220\206.md" "b/Java/\345\237\272\347\241\200/\345\274\202\345\270\270\345\244\204\347\220\206.md" similarity index 77% rename from "Java\345\237\272\347\241\200/\345\274\202\345\270\270\345\244\204\347\220\206.md" rename to "Java/\345\237\272\347\241\200/\345\274\202\345\270\270\345\244\204\347\220\206.md" index ba39e70..73d791f 100644 --- "a/Java\345\237\272\347\241\200/\345\274\202\345\270\270\345\244\204\347\220\206.md" +++ "b/Java/\345\237\272\347\241\200/\345\274\202\345\270\270\345\244\204\347\220\206.md" @@ -19,17 +19,33 @@ ## 分类 1. 所有异常都是`Throwable`的子类,分为: - - `Error`致命异常:系统发生了不可控错误,针对此类错误,程序无法处理,需要人工介入。例如:StackOverflowError、OutOfMemoryError - - `Exception` 非致命异常:分为 - - `checked异常`-受检异常:需要在代码中**显示处理**的异常,否则会**编译出错** + - `Error`致命异常,不可预料,可能导致程序宕机:系统发生了不可控错误,针对此类错误,程序无法处理,需要人工介入。例如:StackOverflowError、OutOfMemoryError + - `Exception` 非致命异常,程序运行中可以预见的异常:分为 + - `checked异常`-受检异常:需要在代码中**显示处理**的异常,否则会**编译出错**。(会飘红) >如果能自行处理,则可以在当前方法捕获异常;如果无法处理,就继续向上抛出。例如:**SQLException**、**ClassNotFoundException** - 无能为力、引起注意型: 程序无法处理,比如字段超长导致的SQLException,重试多次也没啥用,一般处理做法是完整地保存异常现场,供工程师介入解决。 - - 力所能及、坦然处理型: 如未授权异常,程序可以跳转到权限申请页面。 + - 力所能及、坦然处理型: 如未授权异常,程序可以跳转到权限申请页面,网络重试等。 - `unchecked异常`-非受检异常:**运行时异常**,继承自`RuntimeException`,不需要程序进行显示的捕获和处理。 - 可预测异常(Predicted Exception):IndexOutBoundsException、NullPointerException等,基于待代码的性能和稳定性要求,这种异常就不应该被产出或抛出,应该提前做好边界检查、空指针判断等处理,提前避免这种异常。 因为显示声明或者捕获此类异常会对程序的 **可读性和运行效率**产生很大影响。 - 需捕获异常(Caution Exception):比如Dubbo框架进行RPC调用产生的**远程服务超时异常**,需要客户端进行显示捕获,不能因为服务端异常导致客户端不可用,一般处理方案有**重试或者降级处理**。 + - 不要捕获太泛的异常,比如Exception,会增加代码阅读难度 + - 不要生吞异常,至少打印重要的异常信息。 - 可透出异常(Ignored Exception):框架或者系统产生且会自行处理的异常,不需要程序关心。比如404或者Spring框架抛出的NoSuchRequestHandlingMethodException异常。 + +### Checked Exception +1. 是否需要定义成Checked Exception? + >因为这种类型设计的初衷更是为了**从异常情况恢复**,作为异常设计者,我们往往有充足信息进行分类。 +2. 在保证诊断信息足够的同时,也要考虑**避免包含敏感信息**,因为那样可能导致潜在的**安全**问题。 + 1. 类似 java.net.ConnectException, 出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机 器名、IP、端口等,一个重要考量就是信息安全。 + 2. 类似的情况在日志中也有,比如,用户 数据一般是不可以输出到日志里面的。 +3. 业界有一种争论(甚至可以算是某种程度的共识),Java 语言的**Checked Exception也许是个设计错误**,反对者列举了几点: + - Checked Exception 的**假设是我们捕获了异常,然后恢复程序**。但是,其实我们大多数情况下,根本就不可能恢复。Checked Exception的使用,已经大大**偏离了最初的设计目的**。 + - Checked Exception 不兼容functional编程,如果你写过Lambda/Stream 代码,相信深有体会 + > 很多开源项目(Spring、Hibernate)已经采纳了这种时间,甚至反映在新的变成语言设计中(Scala)。参考:http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/ +4. 有一些异常,比如和环境相关的 IO、网 络等,其实是存在可恢复性的 + + ## 异常几个关键字:try、catch、finally、throw、throws ### throw关键字 @@ -104,6 +120,25 @@ throw关键字后边创建的是编译异常(写代码的时候报错),我 * `String toString()` 返回 Throwable 的详细消息字符串 * `void printStackTrace()` JVM打印异常对象,默认此方法,打印的异常信息是最全面的 + +#### try-with-resources | multiple catch +自动按照约定俗成close哪些扩展了AutoCloseble 或者 Closeable的对象。 +```java +try(BufferedReader br = new BufferedReader(...); + BufferedWriter writer = new BufferedWriter(...)) {} //Try-with-resources + //do something +catch (IOException | XEception e){ //Multiple catch + //Handle it +} + +``` + +#### try-catch有性能开销 +- try-catch代码段会产生额外的性能开销,或者换个角度说,它往往会**影响JVM对代码进行优化**,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码; +- 与此同时,**利用异常控制代码流程,也不是一个好主意**,远比我们通常意义上的条件语句(if/else、switch)要低效。 +- Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。 +- 当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的 Exception 也是一种思路。 + ### finally代码块 注意: @@ -196,32 +231,6 @@ public class XXXException extends Exception | RuntimeException{ -# 日志 -记录系统日志的三大原因: -1. 记录操作轨迹 -2. 监控系统运行状况 -3. 回溯系统故障 - -## 日志规范 - -1. 日志命名:推荐的日志文件命名方式为 appName_logType logName.log。 其中 , logType 为 日志类型,推荐分类有 stats、 monitor、 visit 等 , logName 为日志描述。这种命名的 好处是: 通过文件名就可以知道曰志文件属于什么应用,什么类型,什么目的,也有利 于归类查找。例如, mppserver 应用中单独监控时区转换异常的日志文件名定义为 -`mppserver_monitor_timeZoneConvert.log`。 - -2. 日志保存多久?代码规约推荐曰志文件**至少保存15天**,可以根据日志文件的重要程度、 文件大小及磁盘空间再自行延长保存时间。 -3. 生产环境禁止输出 DEBUG 曰志旦有选择地输出 INFO日志。 -4. ERROR 级别只记录系统逻辑错误、异常或者违反重要的业务规则,其他错误都可以归为 WARN 级别。用户输入参数错误,这种WARN记录下,方便用户咨询时能还原现场就行。 如果输入ERROR就需要人工介入,显然是不合理的。 -5. 确保记录内容完整: 异常堆栈e一定要输出。 输出对象实例时,要确保实例类重写了toString方法,不然只是输出对象的hashCode值,没有实际意义。 - -记录日志时要考虑三个问题: - - 日志是否有人看 - - 看到这条日志能做什么 - - 能不能提升问题排查效率。 - -## 日志框架 -log4j、logback、jdk-logging、slf4j、commons-logging等,一般可分为三大部分: -- 日志门面 -- 日志适配器 -- 日志库 diff --git "a/Java/\345\237\272\347\241\200/\346\227\245\345\277\227.md" "b/Java/\345\237\272\347\241\200/\346\227\245\345\277\227.md" new file mode 100644 index 0000000..8b6a225 --- /dev/null +++ "b/Java/\345\237\272\347\241\200/\346\227\245\345\277\227.md" @@ -0,0 +1,76 @@ + +# 日志 +记录系统日志的三大原因: +1. 记录操作轨迹 +2. 监控系统运行状况 +3. 回溯系统故障 + +## 日志规范 + +1. 日志命名:推荐的日志文件命名方式为 appName_logType logName.log。 其中 , logType 为 日志类型,推荐分类有 stats、 monitor、 visit 等 , logName 为日志描述。这种命名的 好处是: 通过文件名就可以知道曰志文件属于什么应用,什么类型,什么目的,也有利 于归类查找。例如, mppserver 应用中单独监控时区转换异常的日志文件名定义为 +`mppserver_monitor_timeZoneConvert.log`。 + +2. 日志保存多久?代码规约推荐曰志文件**至少保存15天**,可以根据日志文件的重要程度、 文件大小及磁盘空间再自行延长保存时间。 +3. 生产环境禁止输出 DEBUG 曰志旦有选择地输出 INFO日志。 +4. ERROR 级别只记录系统逻辑错误、异常或者违反重要的业务规则,其他错误都可以归为 WARN 级别。用户输入参数错误,这种WARN记录下,方便用户咨询时能还原现场就行。 如果输入ERROR就需要人工介入,显然是不合理的。 +5. 确保记录内容完整: 异常堆栈e一定要输出。 输出对象实例时,要确保实例类重写了toString方法,不然只是输出对象的hashCode值,没有实际意义。 + +记录日志时要考虑三个问题: + - 日志是否有人看 + - 看到这条日志能做什么 + - 能不能提升问题排查效率。 + +## 日志框架 +log4j、logback、jdk-logging、slf4j、commons-logging等,一般可分为三大部分: +- 日志门面 +- 日志适配器 +- 日志库 + + + +# Log4j2详解 + +## XML配置详解 + +**配置文件的加载顺序** +- 优先查找 log4j.configurationFile 系统属性所指定的配置文件名。 + - 通过在代码中调用 System.setProperties("log4j.configurationFile","FILE_PATH") + - 将 -Dlog4jconfigurationFile=file://C:/configuration.xml 参数传递给 JVM; + +- 先查找test测试配置文件,在 classpath 中寻找 log4j2-test.properties 配置文件; + - 按照properties->yaml->json->xml的优先顺序去找。如果存在两个,从前到后,用最先的那个 +- 如果没找到带test的配置文件,就找log4j2.properties, 也是按照上面文件格式优先级找。 +- 如果还没找到,就使用默认的DefaultConfiguration 配置。 + + +**默认配置:** +```xml + + + + + + + + + + + + + + + + +``` + + +**热更新日志配置** +>通过设置 Configuration 元素的 monitorInterval 属性值(秒数)为一个非零值来让 Log4j 每隔指定的秒数来重新读取配置文件,可以用来动态应用 Log4j 配置。 +```xml + + +... + +``` + + diff --git "a/Java\345\237\272\347\241\200/\351\235\242\345\220\221\345\257\271\350\261\241.md" "b/Java/\345\237\272\347\241\200/\351\235\242\345\220\221\345\257\271\350\261\241.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\351\235\242\345\220\221\345\257\271\350\261\241.md" rename to "Java/\345\237\272\347\241\200/\351\235\242\345\220\221\345\257\271\350\261\241.md" diff --git "a/Java\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213/\345\244\232\347\272\277\347\250\213\345\237\272\347\241\200\345\205\245\351\227\250.md" "b/Java/\345\244\232\347\272\277\347\250\213/\345\244\232\347\272\277\347\250\213\345\237\272\347\241\200\345\205\245\351\227\250.md" similarity index 98% rename from "Java\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213/\345\244\232\347\272\277\347\250\213\345\237\272\347\241\200\345\205\245\351\227\250.md" rename to "Java/\345\244\232\347\272\277\347\250\213/\345\244\232\347\272\277\347\250\213\345\237\272\347\241\200\345\205\245\351\227\250.md" index 4b49df2..5f27eb4 100644 --- "a/Java\345\237\272\347\241\200/\345\244\232\347\272\277\347\250\213/\345\244\232\347\272\277\347\250\213\345\237\272\347\241\200\345\205\245\351\227\250.md" +++ "b/Java/\345\244\232\347\272\277\347\250\213/\345\244\232\347\272\277\347\250\213\345\237\272\347\241\200\345\205\245\351\227\250.md" @@ -1,4 +1,12 @@ -## 示例 +# 常见面试题 + +## + + + + + +## 示例代码 ```java package JavaBase; diff --git "a/Java\345\237\272\347\241\200/\346\266\210\346\201\257\351\230\237\345\210\227.md" "b/Java/\345\244\232\347\272\277\347\250\213/\345\271\266\345\217\221\344\270\216\345\244\232\347\272\277\347\250\213.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\346\266\210\346\201\257\351\230\237\345\210\227.md" rename to "Java/\345\244\232\347\272\277\347\250\213/\345\271\266\345\217\221\344\270\216\345\244\232\347\272\277\347\250\213.md" diff --git "a/Java/\345\244\232\347\272\277\347\250\213/\351\224\201.md" "b/Java/\345\244\232\347\272\277\347\250\213/\351\224\201.md" new file mode 100644 index 0000000..9eaee7a --- /dev/null +++ "b/Java/\345\244\232\347\272\277\347\250\213/\351\224\201.md" @@ -0,0 +1,53 @@ + + +# 锁优化之升入了解Lock同步锁的优化方法 + +## 与Synchronized的优势 +相对于需要 JVM **隐式**获取和释放锁的 Synchronized 同步锁,Lock 同步锁(以下简称 Lock 锁)需要的是**显示获取和释放锁**,这就为获取和释放锁提供了**更多的灵活性**。 + + + +Lock 锁的**基本操作**是**通过乐观锁来实现**的,但由于 Lock 锁也会在阻塞时被挂起,因此它依然**属于悲观锁**。 + + +从性能方面上来说,在并发量不高、竞争不激烈的情况下, Synchronized 同步锁由于具有分级锁的优势,性能上与 Lock 锁差不多;但在高负载、高并发的情况下, Synchronized 同步锁由于竞争激烈会升级到重量级锁,性 能则没有 Lock 锁稳定。 + + + **对比**: +- 概况比较: + +-- | Synchronized | Lock +---------|----------|--------- + 实现方式 | JVM层实现 | Java底层代码实现 + 锁的获取 | JVM隐式获取 | Lock.lock():获取锁,如被锁定则等待。 Lock.tryLock():如未被锁定才获取锁。 Lock.tryLock(long timeout, TimeUnit unit):获取锁,如已被锁定,则最多等待timeout时间后返回获取锁的状态。Lock.lockInterruptibly():如当前线程未被inteerup才获取锁 + 锁的释放 | JVM隐式释放 | 通过Lock.unlock(),在finally中释放锁 + 锁的类型 | 非公平锁、可重入 | 非公平锁、公平锁、可重入 + 锁的状态 | 不可中断 | **可中断** + + +- **从性能上比较**: + - 在**并发量不高、竞争不激烈**的情况下,Synchronized同步锁由于**具有分级锁的优势**(偏向、轻、重),性能上与Lock差不多。 + - 在**高负载、高并发**的情况下,Synchronized由于竞争会升级到重量级锁,性能没有Lock稳定。 + + +## Lock锁的实现原理 + +Lock 是一个接口类 +- 常用的实现类有:ReentrantLock、 ReentrantReadWriteLock(RRW),它们都是依赖 AbstractQueuedSynchronizer(AQS)类实现的。 + +- AQS 类结构中包含一个**基于链表**实现的等待队列(CLH 队 列),用于存储所有阻塞的线程,AQS 中还有一个 `state` 变量,该变量对 ReentrantLock 来说**表示加锁状态**。 + +- 通过一张图看下整个获取锁的流程: + +![](../../img/锁/锁_2022-03-31-01-49.png) + + +## 锁分离优化 Lock 同步锁 +- 读多写少的情况下,互斥锁就太重了,想到的优化方式: + + +**读写锁**( ReentrantReadWriteLock): +- ReentrantLock 是一个独占 锁,同一时间只允许一个线程访问,而 RRW 允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同 +时访问。 +- 读写锁内部维护了两个锁,一个是用于读操作的 ReadLock,一个是用于写操作的 WriteLock。如何实现锁分离来保证共享资源的原子性? +- \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/2021-05-12-14-49-15.png" "b/Java/\346\265\213\350\257\225/2021-05-12-14-49-15.png" similarity index 100% rename from "Java\345\237\272\347\241\200/2021-05-12-14-49-15.png" rename to "Java/\346\265\213\350\257\225/2021-05-12-14-49-15.png" diff --git "a/Java\345\237\272\347\241\200/\345\215\225\345\205\203\346\265\213\350\257\225.md" "b/Java/\346\265\213\350\257\225/\345\215\225\345\205\203\346\265\213\350\257\225.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\345\215\225\345\205\203\346\265\213\350\257\225.md" rename to "Java/\346\265\213\350\257\225/\345\215\225\345\205\203\346\265\213\350\257\225.md" diff --git "a/Java\345\237\272\347\241\200/\346\236\266\346\236\204.md" "b/Java/\350\256\276\350\256\241\346\250\241\345\274\217/\346\236\266\346\236\204.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\346\236\266\346\236\204.md" rename to "Java/\350\256\276\350\256\241\346\250\241\345\274\217/\346\236\266\346\236\204.md" diff --git "a/Java\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217\345\205\245\351\227\250\346\214\207\345\215\227.md" "b/Java/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\205\245\351\227\250\346\214\207\345\215\227.md" similarity index 71% rename from "Java\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217\345\205\245\351\227\250\346\214\207\345\215\227.md" rename to "Java/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\205\245\351\227\250\346\214\207\345\215\227.md" index acf3c88..6ab73c8 100644 --- "a/Java\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217\345\205\245\351\227\250\346\214\207\345\215\227.md" +++ "b/Java/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\205\245\351\227\250\346\214\207\345\215\227.md" @@ -18,7 +18,7 @@ 根据它们的**用途**,设计模式可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种: - 创建型模式主要用于描述如何创建对象 -- 结构型模式主要用于描述如何实现类或对象的组合 +- 结构型模式主要用于描述如何实现类或对象的组合、继承 - 行为型模式主要用于描述类或对象怎样交互以及怎样分配职责 >在GoF 23种设计模式中包含5种创建型设计模式、7种结构型设计模式和11种行为型设计模式。 @@ -117,8 +117,8 @@ 3. 多用组合,少用继承。 也就是**合成复用**原则。 **要点**: -- 模式可以让我们建造出具有良好OO设计质量的系统。 -- 模式被认为是历经验证的OO设计经验。 +- 模式可以让我们建造出具有良好设计质量的系统。 +- 模式被认为是历经验证的设计经验。 - 模式不是代码,只是这对设计问题的通用解决方案。 - 模式不是被发明的,而是被发现的。 - 大多数的模式和原则,都着眼于**软件变化的主题**。 @@ -141,6 +141,23 @@ ## 建(构)造者模式 +### 目的思想 +创建型模式的初衷: 将对象创建过程单独抽象出来,从结构上把对象使用逻辑和创建逻辑相互独立,隐藏对象实例的细节,进而为使用者实现了更加规范、统一的逻辑。 + + + +### 例子 + +1. JDK 最新版本中 HTTP/2 Client API,下面这个创建 HttpRequest 的过程,就是典型的构建器模式(Builder),通常会被实现成fluent 风格的 API,也有人叫它方法链。 +2. +```java +HttpRequest request = HttpRequest.newBuilder(new URI(uri)) + .header(headerAlice, valueAlice) + .headers(headerBob, value1Bob,headerCarl, valueCarl,headerBob, value2Bob) + .GET() + .build(); +``` + **PB序列化中的Builder** # 七个结构型模式 @@ -157,6 +174,18 @@ ## 装饰模式 +**识别** + +识别装饰器模式,可以通过识别类设计特征来进行判断,也就是其类构造函数以相同的抽象 类或者接口为输入参数。 + + +**例子** + + +1. **InputStream** 是一个抽象类,标准类库中 提供了 FileInputStream、ByteArrayInputStream 等各种不同的子类,分别从不同角度对 InputStream 进行了功能扩展,这是典型的装饰器模式应用案例。 + + - + ## 外观模式 ## 桥接模式 @@ -196,3 +225,80 @@ ## 访问者模式 ## 模板方法模式 + + +### 定义 +模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。 + +- 这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。 +- 这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。 + + +**模板模式有两大作用:复用和扩展**: + +- **复用** + +模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。所有的子类都可以复用父类中模板方法定义的流程代码。 + + + +- **扩展** + +这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转,你可以结合第 19 节来一块理解。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。 + +1. HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet()、doPost() 是模板中可以由子类来定制的部分。实际上,这就相当于 Servlet 框架提供了一个扩展点(doGet()、doPost() 方法),让框架用户在不用修改 Servlet 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。 +2. 跟 Java Servlet 类似,JUnit 框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown() 等),让框架用户可以在这些扩展点上扩展功能。 + + + + +>模板模式的核心,就是把固定的东西做成模板,把可变的东西做成扩展点。 一种稳定性与灵活性的平衡。 + +### 代码框架 + +templateMethod() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为 abstract,是为了强迫子类去实现。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现比较灵活 + + +```java +public abstract class AbstractClass { + public final void templateMethod() { + //... + method1(); + //... + method2(); + //... + } + + protected abstract void method1(); + protected abstract void method2(); +} + +public class ConcreteClass1 extends AbstractClass { + @Override + protected void method1() { + //... + } + + @Override + protected void method2() { + //... + } +} + +public class ConcreteClass2 extends AbstractClass { + @Override + protected void method1() { + //... + } + + @Override + protected void method2() { + //... + } +} + +AbstractClass demo = ConcreteClass1(); +demo.templateMethod(); +``` + +### 场景 diff --git "a/Java\345\237\272\347\241\200/Java\346\226\260\347\211\271\346\200\247.md" "b/Java/\350\277\233\351\230\266/Java\346\226\260\347\211\271\346\200\247.md" similarity index 53% rename from "Java\345\237\272\347\241\200/Java\346\226\260\347\211\271\346\200\247.md" rename to "Java/\350\277\233\351\230\266/Java\346\226\260\347\211\271\346\200\247.md" index 123c8ed..e6655f2 100644 --- "a/Java\345\237\272\347\241\200/Java\346\226\260\347\211\271\346\200\247.md" +++ "b/Java/\350\277\233\351\230\266/Java\346\226\260\347\211\271\346\200\247.md" @@ -1,6 +1,23 @@ -## Java 8 +# Java 8 + + + + +# 其他Java版本 + +## Java 9 + +### 字符串 String +在 Java 9 中,我们引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。 将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并 且将相关字符串操作类都进行了修改。另外,所有相关的 Intrinsic 之类也都进行了重写, 以保证没有任何性能损失。 + +**好处**:**即更小的 内存占用、更快的操作速度。** + +最大字符串的大小缩小了一杯。原来 char 数组的实现,字符串的最大长度就是数组本身的长度限制,但是替换成 byte 数组,同样数组长度下,存储能力是退化了一倍的! +>好在这是存在于理论中的极限, 还没有发现现实应用受此影响。 + ## Java 10 + ### 1. var 1. 在声明局部变量时,可以使用 var 代替具体的类名或接口名,让编译器自己去推断变量的类型。 2. 当然,只有在声明并且立即初始化变量的情况下才能使用 var。 diff --git "a/Java/\350\277\233\351\230\266/\343\200\212Java 8\345\256\236\346\210\230\343\200\213 \350\257\273\344\271\246\347\254\224\350\256\260.md" "b/Java/\350\277\233\351\230\266/\343\200\212Java 8\345\256\236\346\210\230\343\200\213 \350\257\273\344\271\246\347\254\224\350\256\260.md" new file mode 100644 index 0000000..df22ce7 --- /dev/null +++ "b/Java/\350\277\233\351\230\266/\343\200\212Java 8\345\256\236\346\210\230\343\200\213 \350\257\273\344\271\246\347\254\224\350\256\260.md" @@ -0,0 +1,285 @@ +# Java 8 的新功能 + +● 语言需要不断改进以跟进硬件的更新或满足程序员的期待 + +# 概述 + +主要: + +● Stream API +● 向方法传递代码的技巧 +● 接口中的默认方法 + + +好处: + +● Java 8对于程序员的主要好处在于它提供了更多的编程工具和概念,能以更快,更重要的是能以更为简洁、更易于维护的方式解决新的或现有的编程问题。 + ○ 流处理 + ■ Unix命令行允许多个程序通过管道(|)连接在一起,比如: cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3 + ○ 用行为参数化把代码传递给方法:如何区别于匿名函数 + ○ 并行与共享的可变数据 + +这两个要点(没有共享的可变数据,将方法和函数即代码传递给其他方法的能力)是我们平常所说的函数式编程范式的基石。 + +● 不能有共享的可变数据”的要求意味着,一个方法是可以通过它将参数值转换为结果的方式完全描述的; + ○ 换句话说,它的行为就像一个数学函数,没有可见的副作。 执行时在元素之间没有互动。 + +# 流 + +## 匿名函数: + + filterApples(inventory, (Apple a)-> a.getWeight() < 80 || + "brown".equals(a.getColor()) ); + +你甚至都不需要为只用一次的方法写定义;代码更干净、更清晰,因为你用不着去找自己到底传递了什么代码。但要是Lambda的长度多于几行(它的行为也不是一目了然)的话,那你还是应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。你应该以代码的清晰度为准绳。 + +要是没有多核CPU,可能他们真的就到此为止了,为了更好地利用并行,Java的设计师没有这么做。Java 8中有一整套新的类集合API——Stream,它有一套函数式程序员熟悉的、类似于filter的操作,比如map、reduce,还有我们接下来要讨论的在Collections和Streams之间做转换的方法。 + +## 流和Collection: + +● Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。 +● Stream允许并提倡并行处理一个Stream中的元素: 筛选一个Collection(将上一节的filterApples应用在一个List上)的最快方法常常是将其转换为Stream,进行并行处理,然后再转换回List +```java + +//顺序处理 +List heavyApples = inventory.stream().filter((Apple a)-> a.getWeight() > 150).collect(toList()); +//并行处理 +List heavyApples = inventory.parallelStream().filter((Apple a)-> a.getWeight() > 150).collect(toList()); + +``` + +## stream VS 一般循环迭代 +- 在少低数据量的处理场景中(size<=1000),stream 的处理效率是不如传统的 iterator 外部迭代器处理速度快的,但是实际上这些处理任务本身运行时间都低于毫秒,这点效率的差距对普通业务几乎没有影响,反而 stream 可以使得代码更加简洁; + + +- 在大数据量(szie>10000)时,stream 的处理效率会高于 iterator,特别是使用了并行流,在cpu恰好将线程分配到多个核心的条件下(当然parallel stream 底层使用的是 JVM 的 ForkJoinPool,这东西分配线程本身就很玄学),可以达到一个很高的运行效率,然而实际普通业务一般不会有需要迭代高于10000次的计算; + + +- Parallel Stream 受引 CPU 环境影响很大,当没分配到多个cpu核心时,加上引用 forkJoinPool 的开销,运行效率可能还不如普通的 Stream; + + +### 使用 Stream 的建议 + +* 简单的迭代逻辑,可以直接使用 iterator,对于有多步处理的迭代逻辑,可以使用 stream,损失一点几乎没有的效率,换来代码的高可读性是值得的; +* 单核 cpu 环境,不推荐使用 parallel stream,在多核 cpu 且有大数据量的条件下,推荐使用 paralle stream; +* stream 中含有装箱类型,在进行中间操作之前,最好转成对应的数值流,减少由于频繁的拆箱、装箱造成的性能损失; + + +### parallelStream + +parallelStream其实就是一个并行执行的流.它通过默认的ForkJoinPool,可能提高你的多线程任务的速度. + + +**作用**: +Stream具有平行处理能力,处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作,因此像以下的程式片段: +```java +List list = Arrays.asList(1,2,3,4,5,6,7,8); +list.paralleltream().forEach(out::println); + +``` +你得到的展示顺序不一定会是1、2、3、4、5、6、7、8、9,而可能是任意的顺序,就forEach()这个操作來讲,如果平行处理时,希望最后顺序是按照原来Stream的数据顺序,那可以调用forEachOrdered()。例如: +```java +List list = Arrays.asList(1,2,3,4,5,6,7,8); +list.paralleltream().forEachOrdered(out::println); +``` + + +**ForkJoin框架**: +是从jdk7中新特性,它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。 + +ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,**ForkJoinPool需要使用相对少的线程来处理大量的任务**。 +>比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。 + +那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。 + +所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。 + + +那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢? +- 首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。 +- 但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。 + + +forkjoin最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高性能的关键,而这里我们要提到的**工作窃取(work-stealing)算法**就是整个forkjion框架的核心理念,工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。 + +干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。 + + +## 默认方法 + +Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。 + +为什么要加默认方法? + +比如上面的collection.stream().xxxx, 以前的collection或者说List这些都没有stream(),是java8加的,但是没有这个方法,java7运行这个代码就会编译错误,如果是我们自己的接口,增加了一个方法,而这个接口有多个实现,这些实现都需要去实现这个新的方法,不然就无法编译通过。 + +你如何改变已发布的接口,而不破坏已有的实现呢? +这个问题再Java 8 解决了——接口可以包含实现类没有提供实现的方法签名了!, 那谁来实现它? 缺失的方法主体(实现)随着接口一起提供。(这就是默认实现),也就是说接口里面不再是没有实现,只有方法签名,而是可以增加一个默认方法(带着默认实现)。 + +● Java 8在接口声明中使用新的default关键字来表示这一点 +● Java 8 List接口中有如下的默认方法实现,实现直接用List调用sort方法。 + +一个类可以实现多个接口,不是吗?那么,如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。我们在第9章中会谈到,Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题. + +## 来自函数式编程的其他好思想 + +● 将方法和Lambda作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。 +● 通过使用更多的描述性数据类型来避免null。 +● (结构)模式匹配: 使用多态和方法重载来替代if-then-else; + ○ 你可以把模式匹配看作switch的扩展形式,可以同时将一个数据类型分解成元素 + ○ 函数式语言倾向于允许switch用在更多的数据类型上,包括允许模式匹配(在Scala代码中是通过match操作实现的) + +## 通过行为参数化传递代码 + +● 行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式 + +### 举个例子:选苹果 + +● 从一堆苹果里筛选出 + ○ 红苹果、绿苹果...用颜色做为参数搞定。 + ○ 然后,要重的轻的,又复制一下上面的代码,把颜色改为重量 + ○ ....违反了DRY(Don't RepeatYourself,不要重复自己)法则,且你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件 +```java + +public static List filterApplesByColor(List inventory, String color) { + List result=new ArrayList(); + for (Apple apple: inventory){ + if ( apple.getColor().equals(color) ) { + result.add(apple); + } + } + return result; +} +List greenApples=filterApplesByColor(inventory, "green"); +List greenApples=filterApplesByColor(inventory, "red"); + +public static List filterApplesByWeight(List inventory, int weight) { + List result=new ArrayList(); + for (Apple apple: inventory){ + if ( apple.getWeight() > weight ){ + result.add(apple); + } + } + return result; +} + +//3 、通过标记,将不同的条件区分,比如枚举类。。。hen +List greenApples=filterApples(inventory, "green", 0, true); +List heavyApples=filterApples(inventory, "", 150, false); + +``` +- 首先,客户端代码看上去糟透了。true和false是什么意思? +- 此外,这个解决方案还是不能很好地应对变化的需求 + +试试高度抽象,用策略模式: + +● 定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法 +● 该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。 +这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。 +```java + +public interface ApplePredicate{ + boolean test(Apple apple); +} + +public class AppleGreenColrPredict implements ApplePredicate{ + public boolean test(Apple apple){ + return "green".equals(apple.getColor()); + } +} + +//4. 根据抽象条件筛选 +public static List filterApples(List inventory, ApplePredicate p){ + List result = new ArrayList<>(); + for (Apple apple: inventory){ + if ( p.text()){ + result.add(apple); + } + } + return result; +} + +List greenApples = filterApples(inventory, new AppleGreenColrPredict()); + +``` +上面的中方式有几个关键点: +1.传递代码/行为。 + +现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。把filterApples方法的行为参数化 +但是, +由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的. 其实可以通过Lambda直接将表达式"green".equals(apple.getColor)传递给filterApples方法,不用定义个ApplePredicate类。 + +2.多种行为,一个参数。 + +这样可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰嗦,因为你需要声明很多只要实例化一次的类。让我们来看看可以怎样改进。 +对付啰嗦: + +● 匿名类: 同时声明和实例化一个类 + +//5. 使用匿名类 +```java +List greenApples = filterApples(inventory, new AppleGreenColrPredict(){ + public boolean test(Apple apple){ + return "green".equals(apple.getColor()); + } +}); + +``` +● 但匿名类还是不够好。 + ○ 第一,它往往很笨重,因为它占用了很多空间 + ○ 第二,很多程序员觉得它用起来很让人费解 + +在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法 + +- 第六次尝试: 使用Lambda表达式 +```java +List greenApples = filterApples(inventory, (Apple apple) -> "green".equals(apple.getColor())); + + +``` +- 第七次尝试:将List类型抽象化, filterApples方法还只适用于Apple。你还可以将List类型抽象化,从而超越你眼前要处理的问题 +```java + +public interface Predicate{ + boolean test(T t); +} + +//4. 根据抽象条件筛选 +public static List filter(List list, Predicate p){ + List result = new ArrayList<>(); + for (T e: list){ + if ( e.test()){ + result.add(apple); + } + } + return result; +} + +List greenApples = filter(inventory, (Apple apple) -> "green".equals(apple.getColor())); + +List evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0); + +``` +在灵活性和简洁性之间找到了最佳平衡 + +### 来些实例 + +接口只有一个实现,可以直接用匿名函数 -> Lambda + +1. Comparator排序 +```java + +inventory.sort(new Comparator() { + public int compare(Apple a1, Apple a2){ + return a1.getWeight().compareTo(a2.getWeight()); + } +}); + +nventory.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight())); + +``` +2. 用Runnable执行代码块 +```java + +Thread t=new Thread(()-> System.out.println("Hello world")); +``` \ No newline at end of file diff --git "a/Java/\350\277\233\351\230\266/\345\217\215\345\260\204&\346\263\233\345\236\213.md" "b/Java/\350\277\233\351\230\266/\345\217\215\345\260\204&\346\263\233\345\236\213.md" new file mode 100644 index 0000000..e69de29 diff --git "a/Java/\350\277\233\351\230\266/\345\244\232\346\200\201-\351\253\230\347\272\247\347\250\213\345\272\217\345\221\230\347\232\204\345\210\206\346\260\264\345\262\255.md" "b/Java/\350\277\233\351\230\266/\345\244\232\346\200\201-\351\253\230\347\272\247\347\250\213\345\272\217\345\221\230\347\232\204\345\210\206\346\260\264\345\262\255.md" new file mode 100644 index 0000000..bfb6953 --- /dev/null +++ "b/Java/\350\277\233\351\230\266/\345\244\232\346\200\201-\351\253\230\347\272\247\347\250\213\345\272\217\345\221\230\347\232\204\345\210\206\346\260\264\345\262\255.md" @@ -0,0 +1,35 @@ + + +既然多态这么好,为什么很多程序员不能在自己的代码中很好地运用多态呢? + +因为多态需要构建出一个抽象。 + +**构建抽象**,需要找出不同事物的共同点,而这是最有挑战的部分。 + +寻找共同点这件事,地基还是在分离关注点上。 + +我们构建出来的抽象会以接口的方式体现出来,强调一点,这里的接口不一定是一个语法,而是一个类型的约束。 + +所以,在这个关于多态的讨论中,接口、抽象类、父类等几个概念都是等价的,为了叙述方便,我这里**统一采用接口**的说法。 + +> 多态是一个分水岭,将基于对象与面向对象区分开来,可以说,没写过多态的代码,就是没写过面向对象的代码。 + + +首先,**接口将变的部分和不变的部分隔离开来。** +- 不变的部分就是接口的约定 +- 而变的部分就是子类各自的实现。 + + + +只要能够遵循相同的接口,就可以表现出来多态,所以,多态并不一定要依赖于继承。 + +>比如,在动态语言中,有一个常见的说法,叫 Duck Typing,就是说,如果走起来像鸭子,叫起来像鸭子,那它就是鸭子。 + +两个类可以不在同一个继承体系之下,但是,只要有同样的方法接口,就是一种多态。 Go语言的设计就体现了这一点。 + + +在面向对象本身的体系之中,封装和多态才是重中之重,而继承则处于一个很尴尬的位置。 + +- 封装是面向对象的根基,软件就是靠各种封装好的对象逐步组合出来的; +- 继承给了继承体系内的所有对象一个约束,让它们有了统一的行为; +- 多态让整个体系能够更好地应对未来的变化。 \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" "b/Java/\350\277\233\351\230\266/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" rename to "Java/\350\277\233\351\230\266/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" diff --git "a/Java/\350\277\233\351\230\266/\346\266\210\346\201\257\351\230\237\345\210\227.md" "b/Java/\350\277\233\351\230\266/\346\266\210\346\201\257\351\230\237\345\210\227.md" new file mode 100644 index 0000000..e69de29 diff --git "a/Java\345\237\272\347\241\200/\347\274\223\345\255\230.md" "b/Java/\350\277\233\351\230\266/\347\274\223\345\255\230.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\347\274\223\345\255\230.md" rename to "Java/\350\277\233\351\230\266/\347\274\223\345\255\230.md" diff --git "a/Java\345\237\272\347\241\200/\351\233\206\345\220\210.md" "b/Java/\351\233\206\345\220\210/\351\233\206\345\220\210.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\351\233\206\345\220\210.md" rename to "Java/\351\233\206\345\220\210/\351\233\206\345\220\210.md" diff --git "a/Java\345\237\272\347\241\200/\345\237\272\347\241\200.md" "b/Java\345\237\272\347\241\200/\345\237\272\347\241\200.md" deleted file mode 100644 index 26262cb..0000000 --- "a/Java\345\237\272\347\241\200/\345\237\272\347\241\200.md" +++ /dev/null @@ -1,8 +0,0 @@ - -protected 需要从以下两个点来分析说明: - -子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问; - -子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。 - -protected 可以修饰数据成员,构造方法,方法成员,不能修饰类(内部类除外)。 \ No newline at end of file diff --git a/img/.DS_Store b/img/.DS_Store new file mode 100644 index 0000000..c2caf06 Binary files /dev/null and b/img/.DS_Store differ diff --git "a/img/Flink\345\255\246\344\271\240\345\205\245\351\227\250/Flink\345\255\246\344\271\240\345\205\245\351\227\250_2023-04-01-00-52.png" "b/img/Flink\345\255\246\344\271\240\345\205\245\351\227\250/Flink\345\255\246\344\271\240\345\205\245\351\227\250_2023-04-01-00-52.png" new file mode 100644 index 0000000..c8cffeb Binary files /dev/null and "b/img/Flink\345\255\246\344\271\240\345\205\245\351\227\250/Flink\345\255\246\344\271\240\345\205\245\351\227\250_2023-04-01-00-52.png" differ diff --git "a/img/GC\347\232\204\344\272\214\344\270\211\344\272\213/GC\347\232\204\344\272\214\344\270\211\344\272\213_2022-03-30-15-33.png" "b/img/GC\347\232\204\344\272\214\344\270\211\344\272\213/GC\347\232\204\344\272\214\344\270\211\344\272\213_2022-03-30-15-33.png" new file mode 100644 index 0000000..afb4986 Binary files /dev/null and "b/img/GC\347\232\204\344\272\214\344\270\211\344\272\213/GC\347\232\204\344\272\214\344\270\211\344\272\213_2022-03-30-15-33.png" differ diff --git "a/img/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213_2023-03-15-02-10.png" "b/img/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213_2023-03-15-02-10.png" new file mode 100644 index 0000000..a788e75 Binary files /dev/null and "b/img/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213_2023-03-15-02-10.png" differ diff --git "a/img/go\345\205\245\351\227\250/.DS_Store" "b/img/go\345\205\245\351\227\250/.DS_Store" new file mode 100644 index 0000000..5008ddf Binary files /dev/null and "b/img/go\345\205\245\351\227\250/.DS_Store" differ diff --git "a/img/go\345\205\245\351\227\250/go\345\205\245\351\227\250_2022-04-26-19-17.png" "b/img/go\345\205\245\351\227\250/go\345\205\245\351\227\250_2022-04-26-19-17.png" new file mode 100644 index 0000000..75bbf52 Binary files /dev/null and "b/img/go\345\205\245\351\227\250/go\345\205\245\351\227\250_2022-04-26-19-17.png" differ diff --git "a/img/\343\200\212Go \347\226\221\351\232\276\346\235\202\347\227\207\343\200\213/Go\345\214\205\345\210\235\345\247\213\345\214\226\351\241\272\345\272\217.jpg" "b/img/\343\200\212Go \347\226\221\351\232\276\346\235\202\347\227\207\343\200\213/Go\345\214\205\345\210\235\345\247\213\345\214\226\351\241\272\345\272\217.jpg" new file mode 100644 index 0000000..e6b5276 Binary files /dev/null and "b/img/\343\200\212Go \347\226\221\351\232\276\346\235\202\347\227\207\343\200\213/Go\345\214\205\345\210\235\345\247\213\345\214\226\351\241\272\345\272\217.jpg" differ diff --git "a/img/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213_2022-05-14-16-41.png" "b/img/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213_2022-05-14-16-41.png" new file mode 100644 index 0000000..779f730 Binary files /dev/null and "b/img/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213/\343\200\212Go\350\257\255\350\250\200\346\240\270\345\277\20336\350\256\262\343\200\213_2022-05-14-16-41.png" differ diff --git "a/img/\343\200\212Go\350\257\255\350\250\200\351\241\271\347\233\256\345\274\200\345\217\221\345\256\236\346\210\230\343\200\213/cloudTec.jpg" "b/img/\343\200\212Go\350\257\255\350\250\200\351\241\271\347\233\256\345\274\200\345\217\221\345\256\236\346\210\230\343\200\213/cloudTec.jpg" new file mode 100644 index 0000000..4b911fd Binary files /dev/null and "b/img/\343\200\212Go\350\257\255\350\250\200\351\241\271\347\233\256\345\274\200\345\217\221\345\256\236\346\210\230\343\200\213/cloudTec.jpg" differ diff --git "a/img/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\345\212\237\345\212\263\343\200\213/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\345\212\237\345\212\263\343\200\213_2023-05-12-23-55.png" "b/img/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\345\212\237\345\212\263\343\200\213/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\345\212\237\345\212\263\343\200\213_2023-05-12-23-55.png" new file mode 100644 index 0000000..fb3039b Binary files /dev/null and "b/img/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\345\212\237\345\212\263\343\200\213/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\345\212\237\345\212\263\343\200\213_2023-05-12-23-55.png" differ diff --git "a/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-03.png" "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-03.png" new file mode 100644 index 0000000..2faef7a Binary files /dev/null and "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-03.png" differ diff --git "a/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-04.png" "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-04.png" new file mode 100644 index 0000000..dee8cf4 Binary files /dev/null and "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-04.png" differ diff --git "a/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-11.png" "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-11.png" new file mode 100644 index 0000000..8b00f69 Binary files /dev/null and "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-11.png" differ diff --git "a/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-27.png" "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-27.png" new file mode 100644 index 0000000..0e660af Binary files /dev/null and "b/img/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260_2022-04-06-19-27.png" differ diff --git "a/img/\351\224\201/\351\224\201_2022-03-31-01-49.png" "b/img/\351\224\201/\351\224\201_2022-03-31-01-49.png" new file mode 100644 index 0000000..a180cb0 Binary files /dev/null and "b/img/\351\224\201/\351\224\201_2022-03-31-01-49.png" differ diff --git "a/\345\244\247\346\225\260\346\215\256/Kafka/Kafka\345\205\245\351\227\250.md" "b/\345\244\247\346\225\260\346\215\256/Kafka/Kafka\345\205\245\351\227\250.md" deleted file mode 100644 index b3e7229..0000000 --- "a/\345\244\247\346\225\260\346\215\256/Kafka/Kafka\345\205\245\351\227\250.md" +++ /dev/null @@ -1,48 +0,0 @@ -# Kafka入门指南 - -## 简单认识下 - -## 概念名词解释 -Kafka 是一个**分布式**的,**支持多分区、多副本**,基于 **Zookeeper** 的分布式**消息流**平台,它同时也是一款开源的基于**发布订阅**模式的**消息引擎**系统。 - -- **消息**:Kafka 中的数据单元被称为消息 -- **主题**:消息的种类称为 主题(Topic),相当于是对消息进行分类 -- **分区**:主题可以被分为若干个分区(partition) - - 同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性, - - 单一主题中的分区有序,但是无法保证主题中所有的分区有序 -- **生产者**: 向主题发布消息的客户端应用程序称为生产者(Producer),生产者用于持续不断的向某个主题发送消息 -- **消费者**:订阅主题消息的客户端程序称为消费者(Consumer),消费者用于处理生产者产生的消息。 - - **消费者群组**:生产者与消费者的关系就如同餐厅中的厨师和顾客之间的关系一样,一个厨师对应多个顾客,也就是一个生产者对应多个消费者,消费者群组(Consumer Group)指的就是由一个或多个消费者组成的群体。 -- **偏移量**:偏移量(Consumer Offset)是一种元数据,它是一个不断递增的整数值,用来记录消费者发生**重平衡**时的位置,以便用来恢复数据。 - - **重平衡**:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。 - -- **broker**: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。 - - **broker 集群**:broker 是集群 的组成部分,broker 集群由一个或多个 broker 组成,每个集群都有一个 broker 同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。 -- **副本**:Kafka 中消息的备份又叫做 副本(Replica),副本的数量是可以配置的,Kafka 定义了两类副本:领导者副本(Leader Replica) 和 追随者副本(Follower Replica),前者对外提供服务,后者只是被动跟随。 - -### 特性 - -- `高吞吐、低延迟`:kakfa 最大的特点就是收发消息非常快,kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。 -- `高伸缩性`: 每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。 -- `持久性、可靠性`: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储。 -- `容错性`: 允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作 -高并发: 支持数千个客户端同时读写 - -**问:**Kafka为什么快? -* 顺序读写 -* 零拷贝 -* 消息压缩 -* 分批发送 - -### 使用场景 - -### 对比 - - -# 参考资料 - - -作者:程序员cxuan -链接:https://juejin.im/post/6844903495670169607 -来源:掘金 - diff --git "a/\346\225\231\350\202\262/\345\237\271\345\205\273\345\255\251\345\255\220/\345\233\255\344\270\201\344\270\216\346\234\250\345\214\240.md" "b/\346\225\231\350\202\262/\345\237\271\345\205\273\345\255\251\345\255\220/\345\233\255\344\270\201\344\270\216\346\234\250\345\214\240.md" new file mode 100644 index 0000000..266e035 --- /dev/null +++ "b/\346\225\231\350\202\262/\345\237\271\345\205\273\345\255\251\345\255\220/\345\233\255\344\270\201\344\270\216\346\234\250\345\214\240.md" @@ -0,0 +1,75 @@ + +## “教养”是一种糟糕的现代发明 + +**养育中的悖论** + +- 依赖和独立 +- 我们对孩子的爱的特殊性 +- 玩耍和工作 +- 创新和传承 + + +**混乱是童年** + +- 人类的发展策略有两步:先随机生成多种可能性,再保留可行的选项 +- 孩子的混乱天性为人性的可进化性做出了特有的贡献。 +- 漫长的童年为我们提供了探索的良机。 +- 你要让他学习,就需要让他玩。 小时候。 + + +## 爱,是持续进化的保障 + +**爱的三面手** +- 父母 +- 祖父母 +- 异亲:姑姑叔叔舅舅等 +>保证孩子有一个长的童年 + + +**对孩子的爱就像一个无法言喻的承诺** + + + +## 孩子学习的方法 + +**边看边学** + +- 孩子都是优秀的小演员 +- 孩子的模仿能力高级又高效 +- 孩子拥有超越成人的创造力 + - 老师也不知道怎么玩,我们一起来探索吧 + - 孩子的模仿无时无刻,而不是你打起精神给他演示的时候,所以要让自己变好,而不是装 +- 仪式模仿,找到文化归属感 +- 和孩子一起做,而不是“照我说的做” + + +**边听边学** + +- 依恋模式决定孩子更相信谁 +- 孩子会因为什么原因而相信你的话 +- 孩子知道虚构和假想不是现实 +- 问为什么是在寻求更好的解释 + - 认真回答,给个尽量正确的答案。构造孩子的更多逻辑,更多的因果关系 +- 你的解释影响孩子的思维方式 +- 孩子对你的信任胜过一切方法 + + +**边玩边学** + +- 打闹是一种社交演练 +- 聪明的动物对一切都感兴趣 +- 玩玩具就是在做科学实验 +- 假装是人类独有的玩耍方式 +- 爱假装的孩子善于弄清别人怎么想 +- 玩耍教会我们应对意外 + + +**边练边学** + +- 从探索学习到掌握式学习 + - 玩具他在专注,叫他吃饭打断了不好,让他培养。 + - 人类要时刻注意周围环境,天性就是被其他事情分散注意力。 +- 学校教育应该服务于不同类型的孩子 +- 学徒训练是历史主流教育方式 +- 重要的学习在教室之外 +- 青春期:游走在冲动和控制之间 \ No newline at end of file diff --git "a/\345\244\247\346\225\260\346\215\256/Flink/Flink\345\255\246\344\271\240\345\205\245\351\227\250.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Flink/Flink\345\255\246\344\271\240\345\205\245\351\227\250.md" similarity index 94% rename from "\345\244\247\346\225\260\346\215\256/Flink/Flink\345\255\246\344\271\240\345\205\245\351\227\250.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Flink/Flink\345\255\246\344\271\240\345\205\245\351\227\250.md" index 88a9c36..bbfc3e8 100644 --- "a/\345\244\247\346\225\260\346\215\256/Flink/Flink\345\255\246\344\271\240\345\205\245\351\227\250.md" +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Flink/Flink\345\255\246\344\271\240\345\205\245\351\227\250.md" @@ -1,15 +1,15 @@ # BOBO学习Flink -## 学习资料-巨人肩膀 +# 学习资料-巨人肩膀 -### 文档 +## 文档 - 官方文档(中文):https://flink.apache.org/zh/ -### 书籍 +## 书籍 - 《基于Apache Flink的流处理》 - 《StreamingSystems》 英文 -### 视频教程 +## 视频教程 1. https://github.com/flink-china/flink-training-course 2. 拉勾教育-flink教程 @@ -23,7 +23,20 @@ - 在启用高可用选项的情况下,它不存在单点失效问题。 >事实证明,Flink 已经可以扩展到数千核心,其状态可以达到 TB 级别,且仍能保持高吞吐、低延迟的特性。 +### 流式处理框架对比:Storm、Spark Streaming +- [美团基于 Flink 的实时数仓建设实践](https://tech.meituan.com/2018/10/18/meishi-data-flink.html) +- [流计算框架 Flink 与 Storm 的性能对比](https://tech.meituan.com/2017/11/17/flink-benchmark.html) + +**[美团实时计算技术选型对比](https://tech.meituan.com/2021/08/26/data-warehouse-in-meituan-waimai.html)** +![](../../img/Flink学习入门/Flink学习入门_2023-04-01-00-52.png) + + + +以下实时计算场景建议考虑使用 Flink 框架进行计算: ++ 要求消息投递语义为 Exactly Once 的场景; ++ 数据量较大,要求高吞吐低延迟的场景; ++ 需要进行状态管理或窗口统计的场 ## 基础特性 **Flink自身特点:** @@ -336,6 +349,22 @@ ETL 作业通常会周期性地触发,将数据从事务型数据库拷贝到 由于 Flink 是为了提升流处理而创建的平台,所以它适用于各种需要非常低延迟(微秒到毫秒级)的实时数据处理场景,比如实时日志报表分析。而且 Flink 用流处理去模拟批处理的思想,比 Spark 用批处理去模拟流处理的思想扩展性更好,所以我相信将来 Flink 会发展的越来越好,生态和社区各方面追上 Spark。 #### Storm +Storm是一个没有批处理功能的数据流处理器。 + +Flink的并行任务的接口有点类似Storm的bolts。 + +他们的目标都是通过流水线数据传输实现低延迟流处理。 Flink提供了更高级别的API, 比如DataStream API提供可Map/GroupBy/Window和Join等功能, 而不是使用一个或者多个读取器和收集器实现螺栓(bolts)的功能. 使用Storm时,必须手动实现许多此功能。 + + +处理语义的区别。 + +- Storm保证at-least-once +- Flink保证exactly-once. + +提供这些保证的实现也不同, Storm使用记录级别的确认,而flink使用Chandy-Lamport算法的变体,简而言之,数据源会定期标记注入数据流。 + +flink的checkpoint方法比Storm的记录级确认更轻量级。 + # 架构 @@ -426,4 +455,17 @@ Flink会周期性地为应用状态生成检查点,一旦发生故障,Flink ## 核心窗口API -类型,自定义窗口操作,核心结构(分配器、触发器和移除器) \ No newline at end of file +类型,自定义窗口操作,核心结构(分配器、触发器和移除器) + + +### Window Assigner + + +### Window Trigger + + + +### Window Evictors + + +### \ No newline at end of file diff --git "a/\345\244\247\346\225\260\346\215\256/Hive\345\205\245\351\227\250\346\214\207\345\215\227.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Hive\345\205\245\351\227\250\346\214\207\345\215\227.md" similarity index 100% rename from "\345\244\247\346\225\260\346\215\256/Hive\345\205\245\351\227\250\346\214\207\345\215\227.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Hive\345\205\245\351\227\250\346\214\207\345\215\227.md" diff --git "a/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kafka/Kafka\345\205\245\351\227\250.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kafka/Kafka\345\205\245\351\227\250.md" new file mode 100644 index 0000000..2a54963 --- /dev/null +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kafka/Kafka\345\205\245\351\227\250.md" @@ -0,0 +1,179 @@ +# Kafka入门指南 + +## 简单认识下 + +与大多数消息系统相比,Kafka 具有更好的吞吐量、内置分区、复制和容错能力,这使其成为大规模消息处理应用程序的良好解决方案。 + + + +## 概念名词解释 +Kafka 是一个**分布式**的,**支持多分区、多副本**,基于**Zookeeper** 的分布式**消息流**平台,它同时也是一款开源的基于**发布订阅**模式的**消息引擎**系统。 + +- **消息**:Kafka 中的数据单元被称为消息 +- **主题**:消息的种类称为 主题(Topic),相当于是对消息进行分类 +- **分区**:主题可以被分为若干个分区(partition) + - 同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性, + - 单一主题中的分区有序,但是无法保证主题中所有的分区有序 +- **生产者**: 向主题发布消息的**客户端**应用程序称为生产者(Producer),生产者用于持续不断的向某个主题发送消息 +- **消费者**:订阅主题消息的**客户端**程序称为消费者(Consumer),消费者用于处理生产者产生的消息。 + - **消费者群组**:生产者与消费者的关系就如同餐厅中的厨师和顾客之间的关系一样,一个厨师对应多个顾客,也就是一个生产者对应多个消费者,消费者群组(Consumer Group)指的就是由一个或多个消费者组成的群体。 +- **偏移量**:偏移量(Consumer Offset)是一种元数据,它是一个不断递增的整数值,用来记录消费者发生**重平衡**时的位置,以便用来恢复数据。 + - **重平衡**:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。 + +- **broker**: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。Kafka 的**服务器端**。 + - **broker 集群**:broker 是集群 的组成部分,broker 集群由一个或多个 broker 组成,每个集群都有一个 broker 同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。 +- **副本**:Kafka 中消息的备份又叫做 副本(Replica),副本的数量是可以配置的,Kafka 定义了两类副本:领导者副本(Leader Replica) 和 追随者副本(Follower Replica),前者对外提供服务,后者只是被动跟随。 +>生产者总是向领导者副本写消息;而消费者总是从领导者副本读消息。至于追随者副本,它只做一件事:向领导者副本发送请求,请求领导者把最新生产的消息发给它,这样它能保持与领导者的同步。 + + + +### 特性 + +- `高吞吐、低延迟`:kakfa 最大的特点就是收发消息非常快,kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。 +- `高伸缩性`: 每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。 +- `持久性、可靠性`: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储。 +- `容错性`: 允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作. + +- `高并发`: 支持数千个客户端同时读写. + +**问:Kafka为什么快?高吞吐?** +* 顺序读写 +* 零拷贝 +* 消息压缩 +* 分批发送 + + +**问: Kafka Broker 是如何持久化数据的?** + +Kafka 使用 消息日志(Log)来保存数据,一个日志就是磁盘上一个只能追加写(Append-only)消 息的物理文件。 +>因为只能追加写入,故避免了缓慢的随机 I/O 操作,改为性能较好的顺序 I/O 写操作,这也是实现 Kafka 高吞吐量特性的一个重要手段。 + + +**问:Kafka定期删除数据回收磁盘,怎么删除呢?** + +**通过日志段(Log Segment)机制**。在 Kafka 底层,一 个日志又近一步细分成多个日志段,消息被追加写到当前最新的日志段中,当写满了一个日 志段后,Kafka 会自动切分出一个新的日志段,并将老的日志段封存起来。 + +Kafka 在后台还 有**定时任务会定期地检查**老的日志段**是否能够被删除**,从而实现回收磁盘空间的目的。 + + +**再说说消费者** + + +### 使用场景 + +### 对比 + + + +## 设计思想 + +### 持久化 & 效率 & 端到端批量压缩 +**问:Kafka为什么快?高吞吐?** + +**顺序读写** —— 不要害怕系统文件,磁盘很慢? + + * 磁盘顺序写入的性能约为 600MB/秒,而随机写入的性能仅为约 100k/秒——相差超过 6000 倍。几乎达到甚至好于内存随机读取 + * 这些线性读取和写入是所有使用模式中最可预测的,并且由操作系统进行了大量优化。现代操作系统提供预读和后写技术,以大块倍数预取数据,并将较小的逻辑写入分组为大的物理写入。 + * 在某些情况下,顺序磁盘访问可能比随机内存访问更快!磁盘寻到是最耗时的,那就减少寻道。 **而Kafka就是利用这个特性,可以在磁盘存储,即利用了磁盘空间大便宜的特点,让消息敢且能长时间大量存储,又能快速读取。** + * 持久队列可以建立在简单的读取和附加到文件的基础上,就像日志记录解决方案的常见情况一样。这种结构的优点是所有操作都是 O(1) 并且读取不会阻塞写入或彼此。尽管它们的寻道性能较差,但这些驱动器具有可接受的大型读取和写入性能,而且价格是后者的 1/3,容量是后者的 3 倍。 + + + **零拷贝** + * 持久日志块的网络传输。现代 unix 操作系统提供高度优化的代码路径,用于将数据从页面缓存传输到套接字;在 Linux 中,这是通过sendfile 系统调用完成的。 + * 从文件到套接字传输数据的通用数据路径: 四个副本在两个系统间调用,很低效。 + 1. 操作系统从磁盘读取数据到内核空间的pagecache + 2. 应用程序从内核空间读取数据到用户空间缓冲区 + 3. 应用程序将数据写回内核空间的套接字缓冲区 + 4. 操作系统将数据从套接字缓冲区复制到 NIC 缓冲区,并在此处通过网络发送 + - 而使用sendfile,通过允许操作系统将数据从页面缓存直接发送到网络,可以避免这种重新复制。只需要最终拷贝到网卡缓冲区。 + - 我们期望的是:一个主题的多个消费者,能使用上面的零拷贝优化,数据只被复制到页面缓存中一次并在每次消费时重复使用,而不是存储在内存中并在每次读取时复制到用户空间。这可以以接近网络连接限制的速率使用消息。 + >pagecache 和 sendfile 的这种组合意味着在 Kafka 集群上,消费者大部分时间都被捕获,您将看不到磁盘上的任何读取活动,因为它们将完全从缓存中提供数据。 + + + **消息压缩** + - 在某些情况下,瓶颈实际上不是 CPU 或磁盘,而是**网络带宽**。 + - Kafka 通过高效的批处理格式支持这一点。一批消息可以聚集在一起压缩并以这种形式发送到服务器。这批消息将以压缩形式写入,并在日志中保持压缩状态,只会被消费者解压。 + - Kafka 支持 GZIP、Snappy、LZ4 和 ZStandard 压缩协议。 + * 另一个低效率是字节复制。在低消息速率下这不是问题,但在负载下影响很大。为了避免这种情况,我们采用了一种由生产者、代理和消费者**共享的标准化二进制消息格式**(因此数据块可以在它们之间传输而无需修改)。 + + + **分批发送** + * 减少小IO,为此我们的协议是围绕“**消息集**”抽象构建的,该抽象将消息自然地分组在一起。这允许网络请求将消息组合在一起并分摊网络往返的开销,而不是一次发送一条消息。服务器依次将消息块一次性附加到其日志中,而消费者一次获取大的线性块。 + + +### 生产者 + +**负载均衡** + +- 生产者直接将数据发送到作为分区领导者的代理,而无需任何中间路由层。 +- 为了帮助生产者做到这一点,所有 Kafka 节点都可以在任何给定时间回答有关哪些服务器处于活动状态以及主题分区的领导者所在的元数据请求,以允许生产者适当地定向其请求。 +- 随机路由,或者指定分区函数路由。比如按照用户ID分区,同一用户的消息都会发送到同一个分区。 + + +**异步发送** + +- 批处理是效率的重要驱动因素之一,为了启用批处理,Kafka 生产者将尝试在内存中累积数据并在单个请求中发送更大的批次。批处理可以配置为累积不超过固定数量的消息,并且等待时间不超过某个固定的延迟限制(比如 64k 或 10 毫秒)。 +- 积累更多字节发送,一般很少有大IO操作。 不过这种缓存大小可配置,因为需要权衡这带来的额外延迟,目的是获取更好的吞吐量。 + + + +### 消费者 + +**推与拉** + +- 基于推送的系统难以处理不同的消费者,因为代理控制数据传输的速率。目标通常是让消费者能够以最大可能的速度消费;在推送系统中,这意味着当消费者的消费率低于生产率时,消费者往往会不知所措(本质上是拒绝服务攻击)。 +- 基于拉动的系统具有更好的特性,即消费者可以简单地落后并在可能的时候赶上来。 +- 基于拉动的系统的另一个优点是它有助于将发送给消费者的数据积极地分批处理。 +- 基于推送的系统必须选择立即发送请求或积累更多数据然后在不知道下游消费者是否能够立即处理它的情况下发送它。 + + +**消费者地位** + +- 代理发送完消息是否需要等待消费者确认? + - 不等待——可能出现消息丢失。 消费者出问题,没处理就奔溃了。 + - 等待—— 解决了丢失问题,但出现了新的问题: + - 重复消费: 消费者处理了消息但是在发送确认之前失败,那么消息将被消费两次。 + - 性能问题: 代理需要保存每个消息的多个状态(锁定发出去的消息以免二次发送,标记为永久消费以标识可以删除) + +如何处理已发送但从未确认的消息? + +- 主题分为一组完全有序的分区,每个分区在任何给定时间都由每个订阅消费者组中的一个消费者消费。这意味着消费者在每个分区中的位置只是一个整数,即下一条要消费的消息的偏移量。 已消耗内容的**状态非常小**,每个分区只有一个数字。这使得定期检查此状态的操作十分简单低耗。 +- 这个附带有一个好处: 消费者可以故意**回退到旧的偏移量**并**重新消费**数据。这违反了队列的共同约定,但事实证明这是许多消费者的基本特征。 + + +**离线数据加载** + +- 可扩展的持久性允许消费者**只定期消费**的可能性,例如**批量数据加载**,周期性地将数据批量加载到离线系统,如 Hadoop 或关系数据仓库。 +- 在 Hadoop 的情况下,我们通过将负载拆分到各个映射任务来并行化数据加载,每个映射任务对应一个节点/主题/分区组合,从而允许加载中的完全并行。 + +**静态成员** + +- 静态成员旨在提高流应用程序、消费者组和其他构建在组再平衡协议之上的应用程序的可用性。 +- 再平衡协议依赖组协调器将实体 ID 分配给组成员,这些生成的 ID 是短暂的,会在成员重新启动和重新加入时更改。 +- 对于基于消费者的应用程序,这种“动态成员资格”可能会导致**在代码部署、配置更新和定期重启**等管理操作期间将大部分任务**重新分配**给不同的实例。对于大型状态应用程序,混洗任务需要**很长时间**才能在处理之前**恢复其本地状**态,并导致应用程序部分或完全不可用。 +- Kafka 的组管理协议**允许组成员提供持久的实体 ID**。基于这些id,组成员资格保持不变,因此不会触发重新平衡。 + + +### 消息传递语义 + +可以提供多种可能的**消息传递保证**: + +* 最多一次—**—消息可能会丢失**,但永远不会重新传递。 +* 至少一次——消息永远不会丢失,但可能会**重新传递**。 +* Exactly once——这正是人们真正想要的,每条消息只传递一次。 + +这可以分为两个问题: 发布消息的持久性保证和消费消息时的保证。 + +- kafka不丢数据: 发布消息时,我们有消息被“提交”到日志的概念。一旦发布的消息被提交,只要复制该消息写入的分区的一个代理保持“活动”状态,它就不会丢失。 +- 在 0.11.0.0 之前,如果生产者未能收到指示消息已提交的响应,它别无选择,**只能重新**发送消息。 符合至少一次的传递语义。 +- 从 0.11.0.0 开始,Kafka 生产者还**支持幂等**传递选项,保证重新发送不会导致日志中出现重复条目​​。 + - 为此,代理为每个生产者分配一个 ID,并使用生产者随每条消息发送的序列号对消息进行重复数据删除。 +- 同样从 0.11.0.0 开始,生产者支持使用类似**事务**的语义**将消息发送到多个主题分区的能力**:即要么所有消息都已成功写入,要么都没有。 + + +# 参考资料 + +- 官网文档: https://kafka.apache.org/documentation.html#design +- 掘金: https://juejin.im/post/6844903495670169607 + + diff --git "a/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kafka/\343\200\212Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\343\200\213\350\257\276\347\250\213\347\254\224\350\256\260.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kafka/\343\200\212Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\343\200\213\350\257\276\347\250\213\347\254\224\350\256\260.md" new file mode 100644 index 0000000..e7cb167 --- /dev/null +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kafka/\343\200\212Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\343\200\213\350\257\276\347\250\213\347\254\224\350\256\260.md" @@ -0,0 +1,243 @@ + + +# 学习Kafka的正确姿势 + +## 基本学习路线 + +**软件工程师**掌握Kafka的路线: +- 第一步,就是要根据你掌握的编程语言去寻找对应的 **Kafka 客户端**。它们更新和维护的速度很快,非常适合你持续花时间投入。 +- 第二步,马上去**官网**上学习一下**代码示例**,如果能够**正确编译和运行**这些样例,你就能轻松地驾驭客户端了。 +- 第三步,你可以尝试修改样例代码尝试去理解并使用其他的 API,之后观测你修改的结果。 +- 第四步,自己编写一个小型项目来验证下学习成果,然后就是改善和提升客户端的可靠性和性能了。 这一步,你可以熟读一遍 Kafka 官网文档,确保你理解的那些可能影响可靠性和性能的参数。 +- 最后,学习Kafka的高级功能: 流处理应用开发(时间窗口聚合,流处理连接) + + +**运维工程师**学习路线: +- 学习如何搭建和管理Kafka线上环境。 +- 对生产环境的监控是重中之重。 Kafka提供了超多JMX监控指标,你可以选择任意你熟悉的框架进行监控。 +- 有了监控数据,就需要观测真实业务负载下的Kafka集群表现。 根据指标找出系统瓶颈,提升整个系统的吞吐量。 + + + +## 基本使用 + +### 线上方案制定 + + +### 集群配置参数 + +--- +# 一、Kafka入门 + +## 消息引擎基础 + +维基百科介绍:消息引擎系统是一组规范。企业利用这组规范在不同系统之间传递语义准确的消息,实现松耦合的异步式数据传递。 + + +**消息的格式** + +既然消息引擎是用于**在不同系统之间传输消息**的,那么如何设计待传输**消息的格式**从来都是一等一的大事。试问一条消息如何做到**信息表达业务语义而无歧义**,同时它还要能最大限度地**提供可重用性以及通用性**? + +Kafka选择了**纯二进制的字节序列**。当然消息还是结构化的,只是在使用之前都要将其转换成二进制的字节序列。 + + +**消息模型** + +点对点模型 vs 发布/订阅模型 +- 点对点模型:也叫消息队列模型。 一对一服务, 比如电话客服。 +- 发布/订阅模型: 有个topic的概念, 可以存在多个发布者向相同的主题发送消息,订阅者也可以有多个。 比如报纸订阅。 + +Kafka支持这两种传输方式。 + + +**为啥要用消息引擎,而不是直接系统之间发送?** + +- **削峰填谷** + - 上下游处理消息的逻辑不一样,而速度有慢有快。 + +- 发送发和接收方的松耦合,一定程度上简化了应用的开发,减少了系统间不必要的交互。 + + +**聪明人也要下死功夫。** + +作者说: 在 2015 年那会儿,我花了将近 1 年的时间阅读 Kafka 源代码,期间多次想要放弃。你要知道**阅读将近 50 万行源码是多么痛的领悟**。我还记得当初为了手写源代码注释,自己写满了一个厚厚的笔记本。之前的所有努力也没有白费,以至于后面写书、写极客时间专栏就变成了一件件水到渠成的事情。 + +## Kafka基本术语 + +(术语见 《Kafka入门》- 概念名词解释) + +消息模型-点对点: 点对点指的是同一条消息只能被下游的一个消费者消费,其他消费者则不能染指。在 Kafka 中实现这种 P2P 模型的方法就是引入了**消费者组**(Consumer Group)。 + +所谓的消费者组,指的是多个消费者实例共同组成一个组来消费一组主题。这组主题中的每个分区都只会被组内的一个消费者实例消费,其他消费者实 例不能消费它。 + + +为什么要引入消费者组呢?主要是为了提升消费者端的吞吐量。多个消费者实例同时消费,加速整个消费端的吞吐量(TPS)。 + + +假设组内某个实例挂掉了,Kafka 能够自动检测到,然后把这个Failed 实例之前负责的分区转移给其他活着的消费者。这个过程就是 Kafka 中大名鼎鼎的“**重平衡**”(Rebalance)。嗯,其实既是大名鼎鼎,也是臭名昭著,因为由重平衡引发的消费者 问题比比皆是。事实上,目前很多重平衡的 Bug 社区都无力解决。Rebalance 是 Kafka 消费者端实现高可用的重要手段。 + + +每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上, 这个字段就是消费者位移(Consumer Offset)。 + +上面的“位移”表征的是分区内的消息位置,它是不变的,即一旦消息被成功写入 到一个分区上,它的位移值就是固定的了。而消费者位移则不同,它可能是随时变化的,毕 竟它是消费者消费进度的指示器嘛。 + + + +**kafka是按照什么规则将消息划分到各个分区的?** + +>如果producer指定了要发送的目标分区,消息自然是去到那个分区;否则就按照 producer端参数partitioner.class指定的分区策略来定;如果你没有指定过partitioner.class,那 么默认的规则是:看消息是否有key,如果有则计算key的murmur2哈希值%topic分区数;如果 没有key,按照轮询的方式确定分区。 + + +**Kafka的follower副本为什么不像redis、kafka那样提供读写分离功能?** + +- kafka客户端读操作是会移动broker中分区的offset,如果副本提供读服务,副本更变 offset,再回同步领导副本,数据一致性就无法得到保障。 follower 提供读服务会提高数据一致性保证的复杂度。 +- kafka集群可以通过增加partition及broker的方式实现负载均衡,并不需要从follower + + +## Kafka角色定位 + +Apache Kafka 是消息引擎系统,也是一个 分布式流处理平台(Distributed Streaming Platform)。 + +- 最初定位: 分布式、分区化且带备份功能的 提交日志(Commit Log)服务。 + + +LinkedIn 最开始有强烈的数据强实时处理方面的需求,其内部的诸多子系统要执行多种类型的数据处理与分析,主要包括业务系统和应用程序性能监控,以及用户行为数据处理等。 + +当时他们碰到的主要问题包括: +- 数据正确性不足。因为数据的收集主要采用轮询(Polling)的方式,如何确定轮询的间 隔时间就变成了一个高度经验化的事情。虽然可以采用一些类似于启发式算法 (Heuristic)来帮助评估间隔时间值,但一旦指定不当,必然会造成较大的数据偏差。 + +- 系统高度定制化,维护成本高。各个业务子系统都需要对接数据收集模块,引入了大量的定制开销和人工成本。 + + +为了解决这些问题,LinkedIn 工程师尝试过使用 ActiveMQ 来解决这些问题,但效果并不 理想。显然需要有一个“大一统”的系统来取代现有的工作方式,而这个系统就是 Kafka。 + +Kafka 在设计之初就旨在提供三个方面的特性: +- 提供一套 API 实现生产者和消费者; +- 降低网络传输和磁盘存储开销; +- 实现高伸缩性架构。 + + +作为流处理平台,Kafka 与其他主流大数据流式计算框架相比,优势在哪里呢? + +- 第一点是更容易实现端到端的正确性(Correctness)。Google 大神 Tyler 曾经说过,流处理要最终替代它的“兄弟”批处理需要具备两点核心优势: 要实现**正确性**和**提供能够推导时间的工具**。实现正确性是流处理能够匹敌批处理的基石。 + - 实现正确性的基石则是要求框架能提供精确一次处理语义,即处理一条消息有且只有一次机 会能够影响系统状态。目前主流的大数据流处理框架都宣称实现了精确一次处理语义,但这是有限定条件的,即它们只能实现框架内的精确一次处理语义,无法实现端到端的。 + - 因为当这些框架与外部消息引擎系统结合使用时,它们无法影响到外部系统的处理语义,所以 + - 如果你搭建了一套环境使得 Spark 或 Flink 从 Kafka 读取消息之后进行 有状态的数据计算,最后再写回 Kafka,那么你只能保证在 Spark 或 Flink 内部,这条消息对于状态的影响只有一次。但是计算结果有可能多次写入到 Kafka,因为它们不能控制 Kafka 的语义处理。 + - 相反地,Kafka 则不是这样,因为所有的数据流转和计算都在 Kafka 内部完成,故 Kafka 可以实现端到端的精确一次处理语义。 +- 第二点是它自己对于流式计算的定位。 + - 官网上明确标识 Kafka Streams 是一个用于搭建实时流处理的客户端库而非是一个完整的功能系统。这就是说, 你不能期望着 Kafka 提供类似于集群调度、弹性部署等开箱即用的运维特性,你需要自己 选择适合的工具或系统来帮助 Kafka 流处理应用实现这些功能。 + - 针对小企业,流处理数据量并不巨大,逻辑不复杂,这种时候用Kafka流处理组件就简单很多。 + + + +## Kafka版本选择 + +--- +# 二、客户端 + +## 生产者 + +### 分区机制 + + +### 压缩算法 + + +### 无消息丢失配置 + + + +### 高级功能 + + +### TCP连接管理 + + +### 幂等性生产与事务 + + + +## 消费者 + +### 消费者组 + + +### 位移主题 + + +### Rebalance + + +### 位移提交 + + + +### 异常处理 + + +### 多线程开发实例 + + +### TCP连接管理 + + +### group监控 + +--- +# Kafka原理 + +## 备份机制 + + +## 请求处理 + +## Rebalance全流程解析 + + +## Controller + + +## 高水位 + +--- +# 运维与监控 + + +## 主题管理 + +## 动态配置 + + +## 消费组位移管理 + + +## KafkaAdminClient + + +## 认证机制 + + +## MirrorMaker + + +## 监控框架 + + +## 授权管理 + +## Kafka调优 + + +## 流处理应用搭建实例 + + +--- +# 高级Kafka应用 + +## Kafka Stream + + +## Kafka DSL开发 + + +## 应用实例 \ No newline at end of file diff --git "a/\345\244\247\346\225\260\346\215\256/Kylin/Kylin\345\205\245\351\227\250\346\214\207\345\215\227.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kylin/Kylin\345\205\245\351\227\250\346\214\207\345\215\227.md" similarity index 100% rename from "\345\244\247\346\225\260\346\215\256/Kylin/Kylin\345\205\245\351\227\250\346\214\207\345\215\227.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/Kylin/Kylin\345\205\245\351\227\250\346\214\207\345\215\227.md" diff --git "a/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/es/Elasticsearch \346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/es/Elasticsearch \346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230.md" new file mode 100644 index 0000000..ad877ee --- /dev/null +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/es/Elasticsearch \346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230.md" @@ -0,0 +1,128 @@ + +# 《Elasticsearch 核心技术与实战》 +- Github 地址 https://github.com/onebirdrocks/geektime-ELK/ +- 极客时间:《Elasticsearch核心技术与实战》视频课程购买地址 - https://time.geekbang.org/course/intro/197 +## 第一部分:初识 Elasticsearch +### 第 1 章:概述 +1. 课程介绍 +2. 课程综述及学习建议 +3. Elasticsearch 简介及其发展历史 +4. Elastic Stack 家族成员及其应用场景 +### 第 2 章:安装上手 +1. Elasticsearch 的安装与简单配置 +2. Kibana 的安装与界面快速浏览 +3. 在 Docker 容器中运行 Elasticsearch,Kibana 和 Cerebro +4. Logstash 安装与导入数据 +### 第 3 章:Elasticsearch 入门 +1. 基本概念(1):索引,文档和 REST API +2. 基本概念(2):节点,集群,分片及副本 +3. 文档的基本 CRUD 与批量操作 +4. 倒排索引入门 +5. 通过分析器进行分词 +6. Search API 概览 +7. URI Search 详解 +8. Request Body 与 Query DSL 简介 +9. Query String & Simple Query String 查询 +10. Dynamic Mapping 和常见字段类型 +11. 显式 Mapping 设置与常见参数介绍 +12. 多字段特性及 Mapping 中配置自定义 Analyzer +13. Index Template 和 Dynamic Template +14. Elasticsearch 聚合分析简介 +15. 第一部分总结 +## 第二部分:深入了解 Elasticsearch +### 第 4 章:深入搜索 +1. 基于词项和基于全文的搜索 +2. 结构化搜索 +3. 搜索的相关性算分 +4. Query & Filtering 与多字符串多字段查询 +5. 单字符串多字段查询:Dis Max Query +6. 单字符串多字段查询:Multi Match +7. 多语言及中文分词与检索 +8. Space Jam,一次全文搜索的实例 +9. 使用 Search Template 和 Index Alias 查询 +10. 综合排序:Function Score Query 优化算分 +11. Term & Phrase Suggester +12. 自动补全与基于上下文的提示 +13. 配置跨集群搜索 +### 第 5 章:分布式特性及分布式搜索的机制 +1. 集群分布式模型及选主与脑裂问题 +2. 分片与集群的故障转移 +3. 文档分布式存储 +4. 分片及其生命周期 +5. 剖析分布式查询及相关性算分 +6. 排序及 Doc Values & Fielddata +7. 分页与遍历:From, Size, Search After & Scroll API +8. 处理并发读写操作 +### 第 6 章:深入聚合分析 +1. Bucket & Metric 聚合分析及嵌套聚合 +2. Pipeline 聚合分析 +3. 作用范围与排序 +4. 聚合分析的原理及精准度问题 +### 第 7 章:数据建模 +1. 对象及 Nested 对象 +2. 文档的父子关系 +3. Update By Query & Reindex API +4. Ingest Pipeline & Painless Script +6. Elasticsearch 数据建模实例 +7. Elasticsearch 数据建模最佳实践 +8. 第二部分总结回顾 +## 第三部分:管理 Elasticsearch 集群 +### 第 8 章:保护你的数据 +1. 集群身份认证与用户鉴权 +2. 集群内部安全通信 +3. 集群与外部间的安全通信 +### 第 9 章:水平扩展 Elasticsearch 集群 +1. 常见的集群部署方式 +2. Hot & Warm 架构与 Shard Filtering +3. 如何对集群进行容量规划 +4. 分片设计及管理 +5. 在私有云上管理 Elasticsearch 集群的一些方法 +6. 在公有云上管理与部署 Elasticsearch 集群 +### 第 10 章:生产环境中的集群运维 +1. 生产环境常用配置与上线清单 +2. 监控 Elasticsearch 集群 +3. 诊断集群的潜在问题 +4. 解决集群 Yellow 与 Red 的问题 +5. 提升集群写性能 +6. 提升进群读性能 +7. 集群压力测试 +8. 段合并优化及注意事项 +9. 缓存及使用 Breaker 限制内存使用 +10. 一些运维的相关建议 +### 第 11 章:索引生命周期管理 +1. 使用 Shrink 与 Rollover API 有效管理时间序列索引 +2. 索引全生命周期管理及工具介绍 +## 第四部分:利用 ELK 做大数据分析 +### 第 12 章:用 Logstash 和 Beats 构建数据管道 +1. Logstash 入门及架构介绍 +2. Beats 介绍 +### 第 13 章:用 Kibana 进行数据可视化分析 +1. 使用 Index Pattern 配置数据 +2. 使用 Kibana Discover 探索数据 +3. 基本可视化组件介绍 +4. 构建 Dashboard +## 第 14 章:探索 X-Pack 套件 +1. 用 Monitoring 和 Alerting 监控 Elasticsearch 集群 +2. 用 APM 进行程序性能监控 +3. 用机器学习实现时序数据的异常检测(上) +4. 用机器学习实现时序数据的异常检测(下) +5. 用 ELK 进行日志管理 +6. 用 Canvas 做数据演示 + +## 第五部分:应用实战工作坊 +### 实战 1:电影搜索服务 +1. 项目需求分析及架构设计 +2. 将音乐数据导入 Elasticsearch +3. 搭建你的电影搜索服务 +4. 基于 Java 和 Elasticsearch 构建应用 +### 实战 2:Stackoverflow 用户调查问卷分析 +1. 需求分析及架构设计 +2. 数据 Extract & Enrichment +3. 构建 Insights Dashboard +### 备战:Elastic 认证 +1. Elastic 认证介绍 +2. 考点梳理 +3. 集群的数据备份 + +# ELK 相关下载资源 +1. ELK 7.x 推荐官网直接下载,如网速低,可使用以下链接 - 百度网盘下载(https://pan.baidu.com/s/1CRT3W4wEESglCBDnslk2AA) diff --git "a/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/es/Elasticsearch\345\205\245\351\227\250.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/es/Elasticsearch\345\205\245\351\227\250.md" new file mode 100644 index 0000000..9f39556 --- /dev/null +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/es/Elasticsearch\345\205\245\351\227\250.md" @@ -0,0 +1,237 @@ + + +## 如何在开发机上运行多个Elasticsearch实例 +bin/elasticsearch -E node.name=node0 -E cluster.name=clusterDemo -E path.data=node0_data -d +bin/elasticsearch -E node.name=node1 -E cluster.name=clusterDemo -E path.data=node1_data -d +bin/elasticsearch -E node.name=node2 -E cluster.name=clusterDemo -E path.data=node2_data -d +bin/elasticsearch -E node.name=node3 -E cluster.name=clusterDemo -E path.data=node4_data -d + + + + + + +# 查询 + +## 语法示例 + +### match查询(分词) + +- 会对字段值进行分词匹配 + +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "match": { + "bookName":"my test" + } + } +} +``` + + +**match_phrase查询** + +>**不分词? 默认是要连续匹配**:它不是匹配到某一处分词的结果就算是匹配成功了,而是需要query中所有的词都匹配到,而且相对顺序还要一致,而且默认还是连续的,其实类似精确包含. + +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "match_phrase": { + "bookName":"is a test" + } + } +} +``` + + +**搜索的严格程度:slop** + +将slop置为1,然后搜索"is test",虽然is test中间省略了一个词语"a",但是在slop为1的情况下是可以容忍你中间省略一个词语的,也可以搜索出来结果。 +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "match_phrase": { + "bookName":{ + "query":"is test", + "slop":1 + } + } + } +} +``` + +### multi_match查询 + +- 类似Or操作,满足一个就返回 + +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "multi_match": { + "query" : "老坛", + "fields" : ["bookName", "author"] + } + } +} +``` + +### term查询(不分词) + +- 它和match的唯一区别就是match需要对query进行分词,而term是不会进行分词的,它会直接拿query整体和原文进行匹配。 +- 但是原文指的是被分词后的原文, 在原文被分好的每一个词语里,没有一个词语是:"This is a test doc",那自然是什么都搜不到了。所以在这种情况下就只能用某一个词进行搜索才可以搜到, 比如等于 “test” +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "term": { + "bookName": "This is a test doc" + } + } +} +``` + + +**terms查询** + +- terms查询事实上就是多个term查询取一个交集 +- 也就是要满足多个term查询条件匹配出来的结果才可以查到,所以是比单纯的term条件更为严格了: + +- 比如这个例子,是要求原文中既有This这个词,又有is这个词才可以被查到,那按照这个规则我们是可以匹配到数据的: + - 但是如果改成了一个不存在的词便匹配不到了: + +```json +{ + "query": { + "terms": { + "bookName": ["This", "is"] + } + } +} +{ + "query": { + "terms": { + "bookName": ["This", "my"] + } + } +} +``` + + +### fuzzy查询 + +- fuzzy是ES里面的模糊搜索,它可以借助term查询来进行理解。 +- fuzzy和term一样,也**不会将query进行分词**,但是不同的是它在进行匹配时可以容忍你的词语拼写有错误。 +- 至于容忍度如何,是根据参数**fuzziness**决定的。 +- fuzziness默认是2,也就是在默认情况下,fuzzy查询容忍你有两个字符及以下的拼写错误。无论是错写多写还是少写都是计算在内的。 +>即如果你要匹配的词语为test,但是你的query是text,那也可以匹配到。 + +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "fuzzy": { + "bookName":"text" + } + } +} + +{ + "query": { + "fuzzy": { + "bookName":{ + "value":"texts", + "fuzziness":1 + } + } + } +} +``` + +### range查询 + +range查询时对于某一个**数值字段**的大小范围查询. + +* gte:大于等于 +* gt:大于 +* lt:小于 +* lte:小于等于 + +```json +GET http://ip:prot/textbook/_search +{ + "query": { + "range": { + "num": { + "gte":20, + "lt":30 + } + } + } +} +``` + +### bool查询 +bool查询是上面查询的一个综合,它可以用多个上面的查询去组合出一个大的查询语句,它也有一些关键字: + +* must:代表且的关系,也就是必须要满足该条件 +* should:代表或的关系,代表符合该条件就可以被查出来 +* must_not:代表非的关系,也就是要求不能是符合该条件的数据才能被查出来 + + +例如: 要求must里面的match是必须要符合的,但是should里面的两个条件就可以符合一条即可。 + +```json +GET http://ip:prot/textbook/_search +{ + "query":{ + "bool":{ + "must":{ + "match":{ + "bookName":"老坛" + } + }, + "should":{ + "term":{ + "author":"老坛" + }, + "range":{ + "num":{ + "gt":20 + } + }, + } + } + } +} + +``` + +### 排序和分页 + +排序和分页也是建立在上述的那些搜索之上的。排序和分页的条件是和query平级去写的。 + +先举个例子: + +```json +GET http://ip:prot/textbook/_search +{ + "query":{ + "match":{ + "bookName":"老坛" + } + }, + // 它代表的意思是按照页容量为100进行分页,取第一页​。 + "from":0, + "size":100, + "sort":{ + "num":{ + "order":"desc" + } + } +} +``` \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\345\210\206\346\236\220\344\270\216\346\200\235\347\273\264.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\225\260\346\215\256\345\210\206\346\236\220\344\270\216\346\200\235\347\273\264.md" similarity index 100% rename from "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\345\210\206\346\236\220\344\270\216\346\200\235\347\273\264.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\225\260\346\215\256\345\210\206\346\236\220\344\270\216\346\200\235\347\273\264.md" diff --git "a/\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/SolrCloud\345\256\230\346\226\271\346\226\207\346\241\243-\347\277\273\350\257\221\350\256\241\345\210\222.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/SolrCloud\345\256\230\346\226\271\346\226\207\346\241\243-\347\277\273\350\257\221\350\256\241\345\210\222.md" similarity index 100% rename from "\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/SolrCloud\345\256\230\346\226\271\346\226\207\346\241\243-\347\277\273\350\257\221\350\256\241\345\210\222.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/SolrCloud\345\256\230\346\226\271\346\226\207\346\241\243-\347\277\273\350\257\221\350\256\241\345\210\222.md" diff --git "a/\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/solr vs es.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/solr vs es.md" similarity index 95% rename from "\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/solr vs es.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/solr vs es.md" index 16c2b12..518db6f 100644 --- "a/\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/solr vs es.md" +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/solr vs es.md" @@ -98,6 +98,15 @@ Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时 Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。 ## 相同点 +- https://blog.csdn.net/qq_41965731/article/details/90266636 + + +**相关性打分**: +相关度自然打分(权重越高分越高): +- - tf(Term Frequency)越高、权重越高 +df (Document Frequency)越高、权重越低 +- 人为影响分数:设置Boost值(加权值) + ## 不同点 diff --git "a/\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/solrCloud\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/solrCloud\345\255\246\344\271\240\347\254\224\350\256\260.md" similarity index 100% rename from "\345\244\247\346\225\260\346\215\256/\346\243\200\347\264\242\345\274\225\346\223\216/solrCloud\345\255\246\344\271\240\347\254\224\350\256\260.md" rename to "\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\243\200\347\264\242\345\274\225\346\223\216/solrCloud\345\255\246\344\271\240\347\254\224\350\256\260.md" diff --git "a/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\345\205\245\351\227\250.md" "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\345\205\245\351\227\250.md" new file mode 100644 index 0000000..651a420 --- /dev/null +++ "b/\346\225\260\346\215\256\345\267\245\347\250\213\345\270\210/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\345\205\245\351\227\250.md" @@ -0,0 +1,225 @@ + +# 入门介绍 + +## 为什么要使用MQ +1)**解耦**:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那 如果 C 系统现在不需要了呢? +>就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。 + +(2)**异步**: A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地 写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。 + +(3)**削峰**:减少高峰时期对服务器压力。 + + +**缺点** + +- **系统可用性降低**:万一 MQ 挂了,MQ一挂,整套系统崩溃,GG? +- **系统复杂度提高**: 硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况? 怎么保证消息传递的顺序性?问题一大堆。 +- **一致性问题**: A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致 了。 + + +## Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别? + +- 吞吐:kafka和RocketMQ支撑高吞吐,ActiveMQ和RabbitMQ比他们低一个数量级。 +- 延迟:RabbitMQ是最低的 + +1. 社区活跃度 +2. 持久化消息功能比较 + 1. Kafka、ActiveMq 和RabbitMq 都支持。 +3. 综合技术实现 + 1. 指标:可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统等等。 + 2. RabbitMq / Kafka 最好,ActiveMq 次之,ZeroMq 最差。当然ZeroMq 也可以做到,不过自己必须手动写代码实现,代码量不小。 +4. 高并发 + 1. RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言。 +5. 比较关注的比较, RabbitMQ 和 Kafka + 1. RabbitMq 比Kafka 成熟,在可用性上,稳定性上,可靠性上, RabbitMq 胜于 Kafka (理论 上)。 + 2. Kafka 的定位主要在日志等方面, 因为Kafka 设计的初衷就是处理日志的,可以看做是一个 日志(消息)系统一个重要组件。 + 3. Kafka高吞吐、TPS + + +## 如何保证高可用的 + + **RabbitMQ**: + - 普通集群模式:你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据。 + >这方案主要是提 高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。 + - 镜像集群模式: 所谓的 RabbitMQ 的高可用模式. 源数据和消息都存在多个实例上。每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据。 + >**坏处**:性能开销也太大了吧,消息需要同步到所有机器上,导 致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下, 也是每个节点都放这个 queue 的完整数据。 + + +**Kafka**: + +- **多节点**: 由多个 broker 组成,每个 broker 是一个节点; +- **分片**: 你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在 多个机器上的,每个机器就放一部分数据。 +- **副本**: Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。 + + +## 如何保证消息的可靠传输? 如果消息丢了怎么办? + +### 生产者丢失 + +**现象**: +生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢,比如网络问题。 + + +**解决思路**: + +1. **事务机制**: 可以选择用 RabbitMQ 提供的**事务功能**,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息: + - 如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到**异常报错**,此时就可以回滚事务channel.txRollback,然后重试发送消息; + - 如果收到了消息,那么可以提交事务channel.txCommit。吞吐量会下来,因为太耗性能。 + - **事务机制是同步的** + +2. **开启confirm模式**: 开启后,每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中: + - RabbitMQ 会给回传一个ack消息,表示收到了。 + - 如果没能处理,会回调你一个 nack接口,告诉你这个消息接收失败,你可以重试。 + - 可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。(可能导致重复消费) + - **confirm机制是异步的**, 所以一般在生产者这块避免数据丢失,都 是用confirm机制的。 + +### 消息队列中丢失 + +MQ机器挂了 + +**解决思路** + +- 必须开启 RabbitMQ 的持久化 +- 注意: 持久化可以跟生产者那边的confirm机制配合起来,**只有消息被持久化到磁盘之后,才会通知生产者ack了**,所以哪怕是在持久化到磁盘之前, RabbitMQ 挂了,数据丢了,生产者收不到ack,你也是**可以自己重发**的。 + + +### 消费端丢失 + +消费进程挂了或者重启了。 + + +**解决思路**: + +- 用 RabbitMQ 提供的ack机制: 关闭 RabbitMQ 的自动ack,可以通过一个 api 来调用就行,然后每次你自己代码里确 保处理完的时候,再在程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处 理,消息是不会丢的。 + + +### 如何保证消息顺序性 + +**场景**: RabbitMQ:一个 queue,多个 consumer,顺序就错乱了; + +**解决**: +- 拆分多个queue,每个queue对应一个消费者。 缺点就是queue多一些。 +- 一个queue对应一个consumer, 然后consumer内部用内存队列排队,发给底层的worker来处理。 + + +### 场景题目 + +1. 如何解决消息队列的**延时以及过期失效**问题? + +假设你用的是 RabbitMQ, RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。 + +**大量数据搞丢,这问题很严重。** + +业务低峰期,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。 + +如果需要快速修复呢? + + +2. 有几百万消息**持续积**压几小时,说说怎么解决? + +- 消息积压处理办法: 临时紧急扩容 + - 确认消费程序是否有问题,先修复 consumer 的问题,确保其恢复消费速度。 + - 然后将现有consumer都停掉? + - 新建一个topic,partition扩大10倍,建立10倍queue + - **写一个临时分发数据的consumer程序**, 部署去消费原来的topic积压的数据,消费之后不做耗时的处理,直接均匀 轮询写入临时建立好的 10 倍数量的 queue。 + - 临时征用 10 倍的机器来部署 consumer,每一 批 consumer 消费一个临时 queue 的数据。 + + +3. 消息**队列满**了以后该怎么处理? + +你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。 + +MQ不能直接丢弃消息嘛? + + +## 设计一个消息队列,会如何设计? + +1. 可伸缩: 快速扩容, 分布式 +2. 高可用 +3. 持久化 +4. 数据一致性 + + +## 05 | 如何确保消息不会丢失? + + +### 检测消息丢失的方法 + +我们可以利用消息队列的有序性来验证是否有消息丢失。原理非常简单,在 Producer 端, 我们给每个发出的消息附加一个连续递增的序号,然后在 Consumer 端来检查这个序号的 连续性。 + +- 如果检测到序号不连续,那就是丢消息了。还可 以通过缺失的序号来确定丢失的是哪条消息,方便进一步排查原因。 + + +大多数消息队列的客户端都支持拦截器机制,你可以利用这个拦截器机制,在 Producer 发 送消息之前的拦截器中将序号注入到消息中,在 Consumer 收到消息的拦截器中检测序号 的连续性。 好处:不会侵入业务代码逻辑,稳定后还能关闭。 + + +**难点** + +- 像 `Kafka` 和 `RocketMQ` 这样的消息队列,它是**不保证在 Topic 上的严格顺序**的, 只能**保证分区上的消息是有序的**: + >所以我们在发消息的时候必须要指定分区,并且,在每个分区单独检测消息序号的连续性。 + +- 如果你的系统中 **Producer 是多实例**的,由于并不好协调多个Producer 之间的发送顺序, 所以也需要每个 Producer 分别生成各自的消息序号,并且需要附加上 Producer 的标识, 在 Consumer 端**按照每个Producer分别来检测序号的连续性**。 + + +- Consumer 实例的数量最好和分区数量一致,做到 Consumer 和分区一一对应,这样会比较方便地在 Consumer 内检测消息序号的连续性。 + + + +### 确保消息可靠传递 + +整个消息从生产到消费的过程中,哪些地方可能会导致丢消息,以及应该如何避免消息丢失。 + +- 生产阶段: 在这个阶段,从消息在 Producer 创建出来,经过网络传输发送到 Broker 存储阶段: +- 存储阶段: 在这个阶段,消息在 Broker 端存储,如果是集群,消息会在这个阶段被复制到其他的副本上。 +- 消费阶段: Consumer从Broker上拉取消息,经过网络传输发送到 Consumer 上。 + + + +1. **生产阶段** + +在生产阶段,消息队列通过最常用的请求确认机制,来保证消息的可靠传递: 客户端发消息到Broker,Broker收到消息后给客户端返回一个确认响应, 客户端收到响应,完成一次正常消息发送。 + +- 只要 Producer 收到了 Broker 的确认响应,就可以保证消息在生产阶段不会丢失 +- 有些消息队列在长时间没收到发送确认响应后,会自动重试,如果重试再失败,就会**以返回值或者 异常的方式**告知用户。 **正确处理返回值或者捕获异常,就可以保证这个阶段的消息不会丢失。** +- 异步发送时,则需要在回调方法里进行检查。这个地方是需要特别注意的,**很多丢消息的原 +因**就是,我们使用了异步发送,却**没有在回调中检查发送结果**。 + + +2. **存储阶段** + +正常运行就不会丢消息,但如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。 + +**如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。** + +- 对于单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息**写入磁盘后再给Producer返回确认响应**。 + >例如,在 RocketMQ 中,需要将刷盘方式 flushDiskType 配 +置为 SYNC_FLUSH 同步刷盘。 + +- 如果是 Broker 是由多个节点组成的集群,需要将 Broker 集群配置成: 至少将消息发送到 +2 个以上的节点,再给客户端回复发送确认响应。这样当某个 Broker 宕机时,其他的Broker 可以替代宕机的 Broker,也不会发生消息丢失。 + + +3. **消费阶段** + +类似生产阶段,通过确认机制保证消息的可靠传递, 消费客户端从Broker拉取消息后,执行用户业务逻辑成功后,才会给Broker发送消费确认消息。 + +- 如果Broker长时间内没收到响应,下次拉消息的时候再次返回同一条消息,确保不在网络传输过程中丢失。 + +- 不要在收到消息后就立即发送消费确认,而是应该在执完所有消费业务逻辑之后,再发送消费确认。 + + +## 06 | 如何处理消费过程中的重复消息? + +**消息重复的情况必然存在** + +在 MQTT 协议中,给出了三种**传递消息时**能够提供的**服务质量标准**,这三种服务质量从低 到高依次是: + +- A**t most once**: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,**允许丢消息**。一般都是一些**对消息可靠性要求不太高的监控场景**使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。 + + +- **At least once**: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消 息,但是**允许有少量重复消息**出现。 + + +- Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是**最高的等级。** + +>在 Kafka 中,事务和 Excactly once 主要是为了配合流计算使用的特性,Kafka 支持的“Exactly once”和我们刚刚提到的消息传递的服务质量标准“Exactly once”是不一样的. \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL/\343\200\212MySQL\345\256\236\346\210\23045\350\256\262\343\200\213\346\236\201\345\256\242\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/\346\225\260\346\215\256\345\272\223/MySQL/\343\200\212MySQL\345\256\236\346\210\23045\350\256\262\343\200\213\346\236\201\345\256\242\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..4297b2f --- /dev/null +++ "b/\346\225\260\346\215\256\345\272\223/MySQL/\343\200\212MySQL\345\256\236\346\210\23045\350\256\262\343\200\213\346\236\201\345\256\242\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,179 @@ + +# 基础篇 + +## 01 | 基础架构:一条SQL查询语句是如何执行的? + +大体来说,MySQL 可以分为两部分: +- Server层 +- 存储引擎层。 + + +Server 层包括: +- 连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核 心服务功能 +- 以及所有的内置函数(如日期、时间、数学和加密函数等) +- 所有**跨**存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 + + +存储引擎层负责数据的**存储和提取**。 +其架构模式是**插件式**的,支持 InnoDB、MyISAM、 Memory 等多个存储引擎。 + + +### 连接器 +连接器负责跟客户端建立连接,获取权限,维持和管理连接。 + +- 连接命令中的mysql是客户端工具,用来跟服务端建立连接。 在完成TCP握手后,连接器开始验证你的身份,这个时候就需要输入账户和密码 + - 密码不对,就denied + - 正确,就会去查权限表,具体的拥有的权限,用于后续权限判断逻辑。 + >这意味着,建立连接后,再修改权限表,是无法影响这次已经建立的连接的权限。 +- 如果没有操作,进入空闲状态,sleep。show processlist可以查看 +- 客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,**默认值是 8 小时**。 +- +- 长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。 + - 建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。 + - 长连接有时候会使 MySQL 占用内存涨得特别快。 + - 这是因为MySQL 在执行过程中**临时使用的内存是管理在连接对象里面的**。这些资源会**在连接断开的时候才释放**。 + - 所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉 (OOM),从现象看就是 MySQL 异常重启了。 + - **解决办法**: + - 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后, 断开连接,之后要查询再重连。 + - 每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。 + + +### 查询缓存 + +之前执 行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。 + +key 是查询的语句,value 是查询的结果。 + +**大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。** + + +查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。 + +因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。 + +对于很少更新的表,而且查询条件经常重复,可以考虑。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。 + +**可以按需使用**: + +你可以将参数 query_cache_type 设置 成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓 存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样: + +```sql +select SQL_CACHE * from T where ID=10; +``` + +>MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻 底没有这个功能了。 + + +### 分析器 +>MySQL 需要知道你要做什么, 因此需要对 SQL 语句做解析。 + +1. 分析器先会做“词法分析”。 + - 你输入的是由多个字符串和空格组成的一条 SQL 语句, MySQL 需要识别出里面的字符串分别是什么,代表什么。 + +2. 做完了这些识别以后,就要做“语法分析”。 + - 语法分析器会根据语法 规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。 + +>一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容。 + + +### 优化器 + +>经过了分析器,MySQL 就知道你要做什么了。 怎么做更好? + +- 优化器是在表里面有多个索引的时候,**决定使用哪个索引**; +- 或者在一个语句有多表关联 (join)的时候,**决定各个表的连接顺序**。 + +### 执行器 + +> 知道做什么和怎么做之后,开始执行语句。 + +1. **权限校验**: 开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返 回没有权限的错误。 + + - 在工程实现上,如果命中查询缓存,会在查询缓存返回结果 的时候,做权限验证。 + - 查询也会在优化器之前调用 precheck 验证权限。 + +2. **打开表**: + 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这 +个引擎提供的接口 + + +## 02 | 日志系统:一条SQL更新语句是如何执行的? + + + + +# 4. 索引 + +## 索引常见的模型 + +- hash表: 等值查询快,范围查询慢 +- 数组: 查询快,范围查询也快,就是更新要移动,适合不变动的数据 +- 搜索树: 二叉树层级太高,一般是N叉树。 N取决于数据块的大小 + + +**多叉树** + +你可以想象一下: 一棵 100 万节点的平衡二叉树,树高20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘**随机读**一个数据块需要 **10 ms** 左右的**寻址时间**。 +>也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。 + + +以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。 + +考虑到**树根的数据块总是在内存中**的,一个 10 亿行的表上一个整数字段的索引,查找一个值**最多只需要访问 3 次磁盘**。 + +其实,**树的第二层也有很大概率在内存**中,那么访问磁盘的平均次数就更少了。 + + +N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。跳表、LSM 树等数据结构也被用于引擎设计中,比如redis就用的跳表。 + + +你心里要有个概念,数据库底层存储的核心就是基于这些数据模型的。每碰到一个新数据库,我们需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景。 + +## Innodb索引模型 + +根据叶子节点的内容,索引类型分为主键索引和非主键索引。 + +- 主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。 +- 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。 + + +**基于主键索引和普通索引的查询有什么区别?** + +- 基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。 + + +## 索引维护 + +B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。 +>(当前数据页:300-500-600) +- 如果插入新的行 ID 值为 700,则只需要在 R5(600) 的记录后面插入一个新记录。 +- 如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。 +- 如果当前数据页已经满了,根据B+树算法,需要申请一个新的数据页,然后挪动部分数据过去。 这个过程成为**页分裂**。 性能会受到影响,同时空间利用率也降低。 +- 相应的也有**页合并**: 由于删除了数据,利用率很低后,需要将数据页做合并。 + + +**哪些场景下应该使用自增主键,而哪些场景下不应该?** + +- 自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。 +- **自增主键好处**-性能好: 自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。 +- **业务逻辑主键坏处** + - 性能差:业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。 + - 存储空间:**主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。** + >假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,如果用身份证号做主键,那么每个二级索引的叶子节点(每个非主键索引的叶子节点上都是主键的值)占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。 +- 适合用业务字段直接做主键而不是自增主键的场景: + - 只有一个索引,那就用这个索引字段作为主键 + - 该索引必须是唯一索引。 + + + +# 5. 锁 + +## 全局锁 + +全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。 + + +全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。在备份过程中整个库完全处于**只读状态**。 + + +但是让整库都只读,听上去就很危险:如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;如果你在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL/\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" "b/\346\225\260\346\215\256\345\272\223/MySQL/\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..c94bd1d --- /dev/null +++ "b/\346\225\260\346\215\256\345\272\223/MySQL/\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,55 @@ + + +# 写SQL的题目 + +## 基础 + +1. 查询分数排在第三名的同学的成绩: 主要是用limit带两个参数去确定第三位。 + 1. 第一个参数表示记录的起始位置(第一个记 录的位置是 0),第二个参数表示返回几条记录。 + 2. “LIMIT 2,1”就表示从第 3 条记 录开始,返回 1 条记录。 +2. Having操作的理解: + - 原理: HAVING 则需要跟分组关键字 +GROUP BY 一起使用,通过对分组字段或分组计算函数进行限定,来筛选结果。 + - 例题: 找出带学生超过2个的老师: +```sql +SELECT teacherid +FROM demo.teach +WHERE teacherid IS NOT NULL +GROUP BY teacherid +HAVING COUNT(*)>=2 +``` + + +# 索引 + +## 统计总数:count(列名)、count(1)、 count(*) 比较 + +- count(列名) 会过滤掉符合where条件但该列值为null的行, 可能不符合count的真正需求 + +- count(1) 和count(*) ,都是会统计为null的行. +- COUNT(*)是**SQL92**定义的标准统计行数的语法,因为他是标准语法,所以MySQL数据库对他进行过很多优化。 实际上count(1)也是一样做了优化,只是前者更标准。 + +>COUNT(常量) 和 COUNT(*)表示的是直接查询符合条件的数据库表的行数。而COUNT(列名)表示的是查询符合条件的列的值不为NULL的行数。 + +### COUNT(*)的优化 + +MyISAM不支持事务,MyISAM中的锁是表级锁;而InnoDB支持事务,并且支持行级锁。 + +因为MyISAM的锁是表级锁,所以同一张表上面的操作需要串行进行,所以,MyISAM做了一个简单的优化,那就是它可以把**表的总行数单独记录下来**,如果从一张表中使用COUNT(*)进行查询的时候,可以直接返回这个记录下来的数值就可以了,当然,前提是不能有where条件。 + + +从MySQL 8.0.13开始,针对InnoDB的 `SELECT COUNT(*) FROM tbl_name`语句,确实在扫表的过程中做了一些优化。 +>前提是查询语句中**不包含WHERE或GROUP BY**等条件。 + +COUNT(*)的目的只是为了统计总行数,所以,他根本不关心自己查到的具体值,所以,他如果能够在扫表的过程中,**选择一个成本较低的索引**进行的话,那就可以大大节省时间。 + + +InnoDB中索引分为聚簇索引(主键索引)和非聚簇索引(非主键索引),聚簇索引的叶子节点中保存的是整行记录,而非聚簇索引的叶子节点中保存的是该行记录的主键的值。 + +所以,相比之下,**非聚簇索引要比聚簇索引小很多**,所以MySQL会优先选择最小的非聚簇索引来扫表。 + +所以,当我们建表的时候,除了主键索引以外,创建一个非主键索引还是有必要的。 + + + +# 锁 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\347\237\245\350\257\206\344\270\200\347\202\271\344\270\200\346\273\264.md" "b/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\347\237\245\350\257\206\344\270\200\347\202\271\344\270\200\346\273\264.md" index 8f49ed9..3c06cdc 100644 --- "a/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\347\237\245\350\257\206\344\270\200\347\202\271\344\270\200\346\273\264.md" +++ "b/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\347\237\245\350\257\206\344\270\200\347\202\271\344\270\200\346\273\264.md" @@ -83,84 +83,20 @@ 4. 尽量避免分布式事务。 5. 单表拆分到数据1000万以内。 -### 我们的分库分表 -```js - - -String.prototype.Trim = function() { - return this.replace(/(^\s*)|(\s*$)/g, ""); -} - -function displayNID() { - var accountid = document.getElementById("accountid").value; - if(checkNum(accountid)){ - dbn = (accountid % 8) + 1; - tbn = (( Math.floor(accountid/8)) % 8) + 1; - document.getElementById("res").innerHTML= "0"+dbn+"_"+"0"+tbn; - } -} - -function displayLogNID() { - var accountid = document.getElementById("userid").value; - if(checkNum(accountid)){ - dbn = accountid % 128; - document.getElementById("dubhelogRes").innerHTML= dbn; - } -} - -function displayXuRiNID() { - var accountid = document.getElementById("XuRiaccountid").value; - if(checkNum(accountid)){ - dbn = (accountid % 32) + 1; - tbn = (( Math.floor(accountid/32)) % 32) + 1; - document.getElementById("resXuRi").innerHTML= "0"+dbn+"_"+"0"+tbn; - } -} - -function displayAdbillID() { - var accountid = document.getElementById("adbillAccountid").value; - if(checkNum(accountid)){ - dbn = (accountid % 4) + 1; - tbn = (( Math.floor(accountid/4)) % 4) + 1; - document.getElementById("resAdbill").innerHTML= "0"+dbn+"_"+"0"+tbn; - } -} -function displayChargeID() { - var accountid = document.getElementById("chargeAccountid").value; - if(checkNum(accountid)){ - dbn = (accountid % 2) + 1; - tbn = (( Math.floor(accountid/2)) % 4) + 1; - document.getElementById("resCharge").innerHTML= "0"+dbn+"_"+"0"+tbn; - } -} - -function displaySkuID() { - var accountid = document.getElementById("skuAccountid").value; - if(checkNum(accountid)){ - dbn = (accountid % 4) + 1; - tbn = (( Math.floor(accountid/2)) % 32) + 1; - document.getElementById("resSku").innerHTML= "0"+dbn+"_"+"0"+tbn; - } -} - -function checkNum(str) -{ - if(str=="") - { - alert("别猴急,请输入一个数字"); - return false; - } - str = str.Trim(); - for(var i=0;i"9") - { - alert("浪费时间,请输入数字而不是其他字符"); - return false; - } - } - return true; -} - -``` \ No newline at end of file + +## 怎么正确地使用 WHERE 和 HAVING? + +1. 第一个区别是:如果需要通过连接从关联表中获取需要的数据,WHERE 是先筛选后连 接,而 HAVING 是先连接后筛选。 +>这一点,就决定了在关联查询中,WHERE 比 HAVING 更高效。 + + +2. 第二个区别是: + - WHERE 可以直接使用表中的字段作为筛选条件,但不能使用分组中的计 算函数作为筛选条件; + - HAVING 必须要与 GROUP BY 配合使用,可以把分组计算的函数 和分组字段作为筛选条件。 +>这点决定了,在需要对数据进行分组统计的时候,HAVING 可以完成 WHERE 不能完成的 任务。 + + +HAVING适合做一些复杂的**统计查询**的时候,经常要用到**分组**时。如果此时不用having,需要把查询分成几步,把中间结果存起来,再用 WHERE 筛选,或者干脆把这部分筛选功能放在应用层面,用代码来实现。 + + +但是,这样做的效率很低,而且会增加工作量,加大维护成本。所以,**学会使用HAVING,对你完成复杂的查询任务非常有帮助**。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\344\277\241\346\201\257.md" "b/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\344\277\241\346\201\257.md" deleted file mode 100644 index dbb697e..0000000 --- "a/\346\225\260\346\215\256\345\272\223/MySQL/\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\344\277\241\346\201\257.md" +++ /dev/null @@ -1,24 +0,0 @@ -## 引言 - -这个网站可以开通代码权限 -http://vcs.biztech.sogou:8080/vcs/index.jsp#project_manage/project_manage_index - -这个网站可以查数据库ip -http://dbmall2.sogou/instance/ - -10、线上数据库: -for_dev -sogouAD!@# - -线下服务器 -用户名是root 密码是sogou_Dev@!~ -bizdev_xuri1! - -分库分表账号可以用:416384 - -# 项目 -## 快投项目 -qa的 kuaitou报告 mong: -10.153.52.191 kuaitoureport01.mongodb -10.153.54.239 kuaitoureport02.mongodb -10.153.61.232 kuaitoureport03.mongodb \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL/\347\264\242\345\274\225\350\257\246\350\247\243.md" "b/\346\225\260\346\215\256\345\272\223/MySQL/\347\264\242\345\274\225\350\257\246\350\247\243.md" new file mode 100644 index 0000000..fe1c3e5 --- /dev/null +++ "b/\346\225\260\346\215\256\345\272\223/MySQL/\347\264\242\345\274\225\350\257\246\350\247\243.md" @@ -0,0 +1,72 @@ + + +# 了解索引 + + +## 1. 索引分类 + +- 按数据结构分类可分为:B+tree索引、Hash索引、Full-text索引。 +- 按物理存储分类可分为:聚簇索引、二级索引(辅助索引)。 +* 按字段特性分类可分为:主键索引、普通索引、前缀索引。 +* 按字段个数分类可分为:单列索引、联合索引(复合索引、组合索引)。 + + +## 2. 设计原则 + +* **索引并非越多越好**,一个表中如有大量的索引,不仅占用磁盘**空间**,还会影响INSERT、DELETE、UPDATE等语句的性能,因为在表中的数据更改时,索引也会进行调整和更新。**耗时,降低更新性能** +* 避免对经常更新的表进行过多的索引,并且索引中的列要尽可能少。应该经常用于查询的字段创建索引,但要避免添加不必要的字段。 +* 数据量小的表最好不要使用索引,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。 +* 在条件表达式中经常用到的不同值较多的列上建立索引,在不同值很少的列上不要建立索引。比如在学生表的“性别”字段上只有“男”与“女”两个不同值,因此就无须建立索引,如果建立索引不但不会提高查询效率,反而会严重降低数据更新速度。 +* 当唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。 +* 在频繁进行排序或分组(即进行group by或order by操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引。 +* 经常作为筛选条件的字段列建立索引 + + +## 3. 高频索引问题 + +### 联合索引 + + +### 覆盖索引 + +即从辅助索引中就可以得到查询的字段,就不需要再查询聚集索引中的记录(回表查询)。 比如只取Id + +使用覆盖索引的好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。 + +所以要求我们尽量查找指定字段,避免回表。 + + +### 最左匹配原则 + +一般来说对于诸如(a,b)这样的联合索引,一般不可以选择b列作为查询条件。但是对于统计操作,如果是覆盖索引的,则优化器会进行选择。 + +### 聚集索引和辅助索引的区别 + +聚集索引与辅助索引,底层数据结构都是B+树,区别仅在于所存放数据的内容: + +- 聚集索引是根据主键创建的一棵B+树,聚集索引的**叶子节点存放了表中的所有记录**。(回表一般就是说回到聚集索引?) +- 辅助索引是根据索引键创建的一棵B+树,与聚集索引不同的是,其叶子节点仅存放索引键值,以及该索引键值指向的主键。 (覆盖索引就是直接在辅助索引得到了需要返回的字段,不再回表?) + + +## B+树索引 + +### 介绍 + +在数据库中,B+树的高度一般都在2~4层,这意味着查找某一键值最多只需要2到4次IO操作,这还不错。因为现在一般的磁盘每秒至少可以做100次IO操作,2~4次的IO操作意味着查询时间只需0.02~0.04秒 + + +B+树的键值为主键,若在建立时没有显式地指定主键,**则InnoDB存储引擎会自动创建一个6字节的列作为主键(rowId)**。 + + +### MyISAM B+树索引 +MyISAM存储引擎其实更像一张堆表,其特点整理如下: + +所有的行数据都存放于MYD文件中, +其B+树索引都是辅助索引,存放于MYI文件中。 +主键索引和其他索引不同之处在于其必须是唯一的,并且不可为NULL值。 +其索引页的大小默认为1KB,同样不可以进行调整。 +与InnoDB存储引擎不同的是,因为没有聚集索引,其索引叶节点存放的键值不是主键值,而是在MYD文件中的物理位置。 + + +# 参考文档 +- http://liuqh.icu/2021/10/25/db/03-mysql-index/ \ No newline at end of file diff --git "a/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\21210x\347\250\213\345\272\217\345\221\230\345\267\245\344\275\234\346\263\225\343\200\213.md" "b/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\21210x\347\250\213\345\272\217\345\221\230\345\267\245\344\275\234\346\263\225\343\200\213.md" new file mode 100644 index 0000000..37c1178 --- /dev/null +++ "b/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\21210x\347\250\213\345\272\217\345\221\230\345\267\245\344\275\234\346\263\225\343\200\213.md" @@ -0,0 +1,236 @@ + + +## 测试 + + +### 为什么写不好测试? + +* 开发者应该写测试; +* 要写可测的代码; +* 要想做好TDD,先要做好任务分解。 + + +**为什么你的测试不够好呢?** + +主要是因为这些测试不够简单。只有将复杂的测试拆分成简单的测试,测试才有可能做好。 + + +**简单的测试** + +把测试写简单,简单到一目了然,不需要证明它的正确性。所以,如果你见到哪个测试写得很复杂,它一定不是一个好的测试。 + + +**前置准备、执行、断言和清理** + +- 的核心是中间的执行部分,它就是测试的目标,但实际上,它往往也是最短小的, +一般就是一行代码调用。 + +- 前置准备,就是准备执行部分所需的依赖。比如,一个类所依赖的组件,或是调用方法所 +需要的参数。 + +- 断言是我们的预期,就是这段代码执行出来怎么算是对的。 + +- 清理是一个可能会有的部分,如果你的测试用到任何资源,都可以在这里释放掉。 + + +### 测试的坏味道 + +- 测试里做的事情太多,出现了几个不同方法的调用,这会导致不知道测试谁,一旦出错,就需要把所有相关的几个方法都查看一遍,这无疑是增加了工作的复杂度。 + > 多写几个测试就好了 + +- 测试一定要有断言。 +- 复杂。 + - 当你看到测试代码里出现各种判断和循环语句,基本上这个测试就有问题了。 + >多写几个测试,每个测试覆盖一种场景。 + + +#### 怎么样的测试算好的测试? 一段旅程(A-TRIP) + +- **Automatic,自动化**: 把测试尽可能交给机器执行,人工参与的部分越少越好。 + - 测试一定要有断言的原因,因为一个测试只有在有断言的情况下,机器才能自动地判断测试是否成功。 + +- **Thorough,全面的**: 应该尽可能用测试覆盖各种场景。 + - 在写代码之前,要考虑各种场景:正常的、异常的、各种边界条件 + - 写完代码之后,我们要看测试是否覆盖了所有的代码和所有的分支,这就是各种测试覆盖率工具发挥作用的场景了。 + +- **Repeatable,可重复的**: + - 某一个测试反复运行,结果应该是一样的,每一个测试本身都不应该依赖于任何不在控制之下的环境。 + - 一堆测试反复运行,结果应该是一样的。这说明测试和测试之间没有任何依赖 + +- **Independent,独立的**:测试和测试之间不应该有任何依赖。 + - 什么叫有依赖?比如,如果测试依赖于外部数据库或是第三方服务,测试 A 在运行时在数据库里写了一些值,测试 B要用到数据库里的这些值,测试 B 必须在测试 A 之后运行,这就叫有依赖。 + - 减少外部依赖可以用 mock,实在要依赖,每个测试自己负责前置准备和后续清理。 + +- **Professional,专业的** : 测试代码,也是代码,也要按照代码的标准去维护。 + - 这就意味着你的测试代码也要写得清晰,比如:良好的命名,把函数写小,要重构,甚至要抽象出测试的基础库 + + +>编写可测试的代码。 +很多人写不好测试,或者觉得测试难写,关键就在于,你始终是站在写代码的视角,而不是写测试的视角。 + + +**要想写好测试,就要写简单的测试。** + + +- 测试人员的测试会带着破坏的性质,开发人员总是认为一切操作都是合理的。 +- 开发常常潜意识的里只会考虑正常的情况,比如输入姓名的input,只会输入不超过三个字符的长度,到测试 +手中,会输入一长串,因为程序中没有做长度检查,超过数据库字段长度成都就挂了。 + + + +## 程序员也可以砍需求 + + +我:你没尝试着砍砍需求? +同事:怎么没尝试?产品的人都不同意。这批功能他们都说是关键功能。 +我:你有没有尝试把需求拆开了再砍呢? + + +以我们用了好多次的登录为例,如果我问你这个需求是什么,大多数人的第一直觉还是用户名密码登录。 + + +**基本上,闯入你脑海的需求描述是主题(epic),在敏捷开发中,有人称之为主用户故事(master story)。** + + +如果你对需求的管理粒度就是主题,那好多事情就没法谈了。 + +- 比如,时间紧迫的时候,我想砍需求,你问产品经理,**我不做登录行不行,你就等着被拒绝吧**。 +- 但是,如果你说时间比较紧,我**能不能把登录验证码放到后面做**,或是邮件地址验证的功能 +放到后面,这种建议**产品经理是可以和你谈的**。 + + +**绝大多数问题都是由于分解的粒度太大造成的,少有因为粒度太小而出问题的。**所以,需求分解的一个原则是,**粒度越小越好**。 + + +### 需求要分解 +“主题”只是帮你记住大方向,真正用来进行需求管理,还是要靠进一步分解出来的需求。 + +**用户故事**,它将是我们这里讨论需求管理的**基本单位**。 + +- 用户故事一定要**有验收标准**,以确保一个需求的**完整性**。 + + +**怎样评判拆分结果?** + +>只有细分的需求才能方便进行管理。什么样的需求才是一个好的细分需求呢? + +**用户故事的衡量标准**: INVEST 原则 +- Independent,独立的。 + - 尽可能不依赖于其它用户故事,彼此依赖的用户故事会让管理优先级、预估工作量都变得更加困难。 + - 如果真的有依赖,一种好的做法是,**将依赖部分拆出来**,重新调整。 + +- Negotiable,可协商的。 + +- Valuable,有价值的。 + +- Estimatable,可估算的。 + - 不能估算的用户故事,要么是因为有很多不确定的因素,要么是因为需求还是太大,这样的故事 +还没有到一个能开发的状态,还需要产品经理进一步分析。 + +- Small,小。 + - 不能在一定时间内完成的用户故事只应该有一个结果,拆分。 + +- Testable,可测试的。 + + +第一个关注点是可协商。 +- 作为实现者,**我们要问问题**。 +- 只是被动接受的程序员,**价值就少了一半**,只要你开始发问,你就会发现很多写需求的人没有想清楚的地方。 + + +第二个关注点--模块的核心:小。无论是独立性也好,还是可估算的也罢,其前提都是小。只有当用户故事够小了,我们后续的腾挪空间才会大。 + + + +**需求的估算** + + +估算的结果是相对的,不是绝对精确的,我们不必像做科研一样,只要给出一个相对估算就好。 + +一般来说,估算的过程也是大家加深对需求理解的过程。 + + +估算还有另外一个重要的作用:发现特别大的用户故事。一般而言,**一个用户故事应该在一个迭代内完成。** + +- 比如你们团队迭代周期是一周,那么如果一个需求要超过1周,就需要拆分。 + + +一般来说,用户故事有可能经过两次拆分。 + +- 一次是由负责业务需求的同事,比如,产品经理,根据业务做一次拆分。 +- 另外一次就是在估算阶段发现过大的用户故事,就再拆分一次。 + + +## 问题 1:面对不了解的技术,我该如何分解任务? + +答案很简单,**先把它变成你熟悉的技术**。 + + +**怎么把它变成你熟悉的技术?** + +>做一次技术 Spike。 + +Spike 这个词的原意是轻轻地刺,有人把它翻译成调研,我觉得是有些重了。 + +**Spike 强调的重点在于快速地试.** + +- 要在一定的时间内完成,比如,五人天,也就是一个人一周的时间,再多就不叫 Spike 了。简单的可能1天。 + + +Spike 的作用就在于消除不确定性,让项目经理知道这里要用到一项全团队没有人懂的技术,需要花时间弄清楚。 + + +项目经理比你更担心不确定性,你清楚地把问题呈现在他面前,项目经理是可以理解的,他更害怕的是,做到一半你突然告诉他,项目进度要延期。 + + +把事情做在前面,尽早暴露问题。 + + + +**怎么做技术 Spike 呢?** + + +假设你已经通过各种渠道,无论是新闻网站,还是技术 blog,又或是上级的安排,对要用的技术有了一些感性的认识,至少你已经知道这项技术是干什么的了。 + + +接下来,我们要进入到**技术 Spike 的任务分解**。 + +- 首先,快速地完成教程上的例子,让自己有一个直观的认识。 + - 跟着教程走一遍,最多也就是半天的时间。 +- 其次,我们要确定两件事:这项技术在项目中**应用场景**和我们的**关注点**。 + - 技术最终是要应用到项目中的,本着“以终为始”的原则,我们就应该奔着结果做,整个的Spike 都应该**围绕着最终的目标做**。 + - **很多程序员见到新技术都容易很兴奋,会把所有的文档通读一遍**。如果是技术学习,这种做法无可厚非。 但如果我们的目标是做Spike,快速地试,没有那么多时间,必须一切围绕结果来——找准关注点。 是为了提高性能还是吞吐? 新技术适用范围和假设,是否成立?是否满足?需要验证。 + - **防止发散**。 +- 确定好场景和关注点,接下来,我们要**开发出一个验证我们想法的原型**了。 + - 这个原型主要目的:快速地验证我们对这项技术的理解是否能够满足我们的假设。 +- 当你**把想法全部验证完毕**,这项技术就已经**由一项不熟悉的技术变成了熟悉的技术**。 + - 我们前面的问题也就迎刃而解了。这时候,你就可以决定,对于这项技术,是采纳还是放弃了。 + + +>当你确定要使用这项技术时,请丢**弃掉你的原型代码**。 我们需要为了项目重新设计,如果顺着原型接着做,你可能不会去设计,代码中会存在着大量对这项技术直接依赖的代码,这是值得警惕的,所有第三方技术都是值得隔离的。 + + + +## 问题 2:项目时间紧,该怎么办? + +项目时间紧,所以,他们没有时间做测试。 + + +这里有个典型误区:**混淆了目标与现状**。 + +- 目标是应该怎么做,现状是我们正在怎么做。 + +- 我们都知道现状是什么样的,问题是,**你对现状满意吗?** + +- **假设现在不忙了,你知道该怎么改进吗?** + +>遗憾的是,很多人根本回答不了这个问题,因为**忙是一种借口**,**一种不去思考改进的借口**。 + +- 首先要有一个目标 +- 接下来,我们以测试为例,讨论一下具体的改进过程。 + - 现状是: 团队之前没什么自动化测试 + - 目标是: 业务代码 100% 测试覆盖。 + - 如果要达成这个目标,我们需要做一个**任务分解**。 + - 分解的过程主要需要解决两方面的问题,一个是**与人的沟通**,另一方面是**自动化的过程**。 + - 与人的沟通,就是要与团队达成共识 + - 不能强求一步到位,只能逐步改进。 diff --git "a/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\212\346\225\217\346\215\267\350\275\257\344\273\266\345\274\200\345\217\221\343\200\213.md" "b/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\212\346\225\217\346\215\267\350\275\257\344\273\266\345\274\200\345\217\221\343\200\213.md" new file mode 100644 index 0000000..793c924 --- /dev/null +++ "b/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\212\346\225\217\346\215\267\350\275\257\344\273\266\345\274\200\345\217\221\343\200\213.md" @@ -0,0 +1,82 @@ + +# 《敏捷软件开发》 + +## 面向对象设计原则 + +- SRP 单一职责原则就一个类而言,应该有且仅有一个引起它变化的原因。 +- OCP 开放-封闭原则软件实体(类、模块和函数等)应可以扩展,但不可修改。 +- LSP 里氏替换原则子类型必须能替换掉它们的基本类型。 + + +- ISP 接口隔离原则**不应该强迫客户依赖于它们不用的方法**。接口属于客户,不属于它所在的类层次结构。 +- DIP 依赖倒置原则抽象不应该依赖于细节。细节应该依赖于抽象。 +- REP 重用发布等价原则重用的粒度就是发布的粒度。 + + +- CCP 共同重用原则一个包中的所有类应该是共同重用的。如果重用包中的一个类,那么就要重用包中的所有类。**相互之间没有紧密联系的类不应该在同一个包中。** +- CRP 共同封闭原则**一个包中所有的类对同一类性质的变化应该是共同封闭的**。一个变化若对一个包有影响,就会影响到包中所有的类,但不会影响到其他的包造成任何影响。 + + +- ADP 无依赖原则**在包的依赖关系中不允许存在环**。细节不应该有其他依赖关系。 +- SDP 稳定依赖原则**朝着稳定的方向进行依赖**。 +- SAP 稳定抽象原则一个包的抽象程度应该和其他的保持一致。 + + + + + + +# 《实现模式》- Kent Beck + +## 一种编程理论 + +### **模式** + + +> 是编码的一些约束(force),众多的模式共同构筑了一种编程风格。 + +- 模式让你感觉到束手束脚,但可以帮你节省时间和精力,提高效率。 +- 模式的种类繁多,众多模式构成了一种编程风格 + + +### **价值观** + + +> 是编程过程中统一支配性主题,影响了我们在编程中所作的每个决策。 + +- **沟通**: 考虑“如果别人看到这段代码会怎么想”, 让代码尽可能通俗易懂。 +- **简单**: 去掉多余的复杂性,精简代码,删去冗余逻辑。 +- **灵活**: 灵活性的提高可能以复杂性的提高为代价。 + + +### **原则** + + +> 原则是模式和价值观之间搭建的桥梁,在遇到没有现有模式可以解决的问题的时候,原则往往可以让我们“无中生有”的创造一些东西,而这些东西往往都是很不错的。 + +- 局部化影响: 修改A不影响B +- 最小化重复: 抽取相同逻辑 +- 将逻辑和数据捆绑?? +- 对称性: add-remove、addObserver-removeObserver +- 声明式表达: 顾名思义,好的命名本身就是一种注释 +- 变化率: Point(x, y) 相关的变量要放到一起,同步变化 + + +**模式描述了要做什么,价值观提供了动机,原则把动机转化成了实际行动。** + + + +### 动机 + +软件设计应该致力于减少整体成本。 +- 经过统计,人们发现,维护成本远远高于初始成本。 +- 因为理解现有代码需要耗费很多时间,而且容易出错,改动之后还需要重新测试和部署。 +- 通过合理的模式的使用,可以降低经济成本。 + + +### 类 + +1. 类的命名: 类的名字应该简明扼要, 简短而有变现力。 可以大大增加理解代码的容易度。 +2. 针对接口编程,不诊断实现编程。 +3. 重复代码考虑抽取到公共父类或者工具类中。 +4. 内部类和匿名内部类: 内部类被实例化之后,会获得创建它的外部类对象的引用, 可以直接访问后者的数据而不用建立额外的连接。 这种对象持有操作有内存泄漏的风险。 \ No newline at end of file diff --git "a/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245\343\200\213.md" "b/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245\343\200\213.md" new file mode 100644 index 0000000..33bee9f --- /dev/null +++ "b/\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266/\343\200\212\347\250\213\345\272\217\345\221\230\350\277\233\351\230\266\346\224\273\347\225\245\343\200\213.md" @@ -0,0 +1,90 @@ + +# 程序员进阶攻略-学习笔记 + +>本文为 学习胡峰老师课程-《程序员进阶攻略》笔记摘要,非原创 + +- 这是一个关于**程序员成长路径**的专栏。 有时候选择了合适的路,比光顾着赶路要重要得多。 + +![](../../img/《程序员进阶功劳》/《程序员进阶功劳》_2023-05-12-23-55.png) + + +**收获**: + +- 建立技术学习的体系框架与思维模型 +- 梳理清晰的成长与进阶路线 +- 扫清成长路上的迷茫和障碍 +- 形成明确的自我定位与认知 + + +# 征途:启程之初 + +## 1. 初心:为什么成为一名程序员? + +**初次接触** + +- 作者是初二在学校接触电脑,还知道主动学习什么basic语言,编程画了几何图形, 光这点我就很耐闷,谁教你下basic的? +- 为啥我当时只知道4399? 没网就是扫雷。。。还有去网吧看别人cs fire in hole,要不就是帝国时代。 + +- 作者当时畅想考清华的计算机专业,我梦想快点长大,就可以理直气壮进网吧,或者买点玩游戏 + + +**选择专业** + +- 作者第一志愿南大物理系,没考好,落到了第二志愿 东北大学机械工程专业。 +- 而我学校是第一志愿,不过可以选择多个专业,土木gg、机械gg、经管gg,最后落到我最后选的计算机,当时就觉得这个好歹感点兴趣,比哪些地质化学好一些。 结果就进入计算机了,而我入学后发现大部分都是调剂过来的,他们都没写计算机,我好歹是在最后写了。 也算是主动选择的吧,哈哈哈。 总之阴差阳错进入这个当时还不是很热门,过几年就火的专业。 + + +为什么成为一名程序员? 有人有天赋,有人凭兴趣,有人看前景。 而我是兴趣一部分,前景随缘到。 + + +## 2. 初惑:技术方向的选择 + +**选择语言** + +虽然语言可以切换,但是能做到自由切换的前提是你对一门语言掌握到通透之后,再学习其他语言可以触类旁通。 + + +**选择回报** + +选择技术方向,选择语言,本质都是一种投资。 + +毕竟精力是有限的,哪怕你学习很快,一个语言的生态系统是庞大的,也许你能做到入门甚至可以用新语言做东西,但是要深入了解就不是短期能做到的。 当然,也许那就够了。 + +一直在变化, 选择比较成熟的,不是走下坡路的。 比如Java、C++、Python,因为他们活了这么久,以后也会活很久,哪怕会走下坡路。 太新的可能死得快,也可能不断增加需求,风险高。 + + +最热的你是否有几处,是否能跨过门槛。 比如区块链、AI还有现在的大模型。 + + +>**技术总是短期被高估,但长期被低估。** + + +**选择行业** + + +## 3. 初程:带上一份技能地图 + + + + +# 4. 初感:别了校园,入了江湖 + + +# 修炼:程序之术 + + + + +# 修行:由术入道 + + + +# 徘徊:道中彷徨 + + + +# 寻路: 路在何方 + + +# 蜕变:破茧成蝶 + diff --git "a/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\345\210\267\351\242\230\346\212\200\345\267\247\345\222\214\347\247\257\347\264\257.md" "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\345\210\267\351\242\230\346\212\200\345\267\247\345\222\214\347\247\257\347\264\257.md" new file mode 100644 index 0000000..fd681c6 --- /dev/null +++ "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\345\210\267\351\242\230\346\212\200\345\267\247\345\222\214\347\247\257\347\264\257.md" @@ -0,0 +1,324 @@ + +# 五大模板 + +## 递归 + +- 满足三个条件 + - 大问题拆成2个子问题 + - 子问题求解方式跟大问题一样 + - 存在最小子问题 + +```java +//terminator 终止条件 +if level > MAX_VALUE + print result + return +// process +process_data(level, data) + +// drill down 进入下一层 +self.recursive(level + 1, data) + +// clear states还原一些影响 +reverse_state(level) +``` + + +## dp动态规划 + +```java +// 状态定义 +dp = new int[m + 1][n + 1] + +// 初始状态 +dp[0][0] = x; +dp[0][1] = y; + +// dp状态的推导 +for(int i = 0; i <= n; i++) { + for(int j = 0; j <= m; j++) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1], ...) + } +} + +//最优解 一般不是最前面就是最后面的值 +return dp[m][n]; +``` + +## 二分查找 + +```java +int left = 0; +int right = len - 1; +while(left <= right) { + int mid = left + (right - left) / 2; + if (arrays[mid] == target) { + return result; + } else if (arrays[mid] < target) { + left = mid + 1; + } else { + right = mid -1; + } +} + +``` + +## 图的dfs和bfs + +**dfs的递归写法** + +```python +visited = set() +def dfs(node, visited) { + set.add(node.val) + for next_node in node.children(): + if not next_node in visited: + dfs(next_node, visited) +} +``` + +**bfs的递归写法** + +```java +public void bfs(graph, start, end) { + Deque queue = new LinkedList<>(); + queue.append(start); + visited.add(start); + while(queue.isEmpth()) { + Node node = queue.pop(); + visited.add(node); + + process(node) + queue.push(node.getChildren()) + } +} + +``` + +## 位运算 + + +1. `X & 1 == 1 OR 0` 判断奇偶(x % 2 == 1) +2. `X = X & (X - 1)` => 清除最低位的1 +3. `X & -X` => 得到最低位的1 + + + +# 分类刷题 + + +## 字符串处理 + +### 字符串相加、相乘 + + + +## 数组 + + + +## 链表 + + + +## 排序 + +### 快排 + + +### 冒泡、选择、插入排序 + + +### 堆排序 + +#### 大顶堆、小顶堆 +- 大顶堆,一般用于寻找最小的k个数。 因为顶部是最大的,只要新来的值小于顶部,说明其就是当前遍历中最小的k个数,直到遍历完成。 +- 小顶堆相反,用来寻找最大的k个数。 + +```java +public MedianFinder() { + //由于 PriorityQueue 使用小顶堆来实现,这里通过修改两个整数的比较逻辑来让 PriorityQueue 变成大顶堆 + maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); + minHeap = new PriorityQueue<>(Integer::compareTo); + peek(); + poll(); + offer(); +} + +``` + + +## 动态规划 + +### 买卖股票 + +**121. 买卖股票的最佳时机** + +> 只能买卖一次 + +```java + +``` + + +## 树 + + +### 遍历 + +#### 递归 + + +#### 迭代 + +```java +public void LRN(TreeNode root) { + LinkedList s = new LinkedList<>(); + TreeNode t = root; + TreeNode r = null; //记录上次访问过的节点 + //当t指针为空,而且堆栈也为空的时候遍历就结束了 + while (t!=null || !s.isEmpty()){ + //每次当t不为空的时候就默认把t压入堆栈 + if (t!=null){ + s.addFirst(t); + t = t.left; + } else { + t = s.getFirst(); + if (t.right!=null && r != t.right){ + //该节点的右孩子不空,而且上一个访问的不是右孩子(证明这是从左孩子回溯过来的) + t = t.right; + } else { + //该节点的右孩子为空,或者右孩子已经访问过了 + t = s.removeFirst(); + System.out.println(t); //遍历节点 + r = t; + t = null; //防止t被压入堆栈,所以要置空 + } + } + } +} + +public void NLR(TreeNode root) { + LinkedList s = new LinkedList<>(); + TreeNode t = root; + //当t指针为空,而且堆栈也为空的时候遍历就结束了 + while (t!=null || !s.isEmpty()){ + //每次当t不为空的时候就默认把t压入堆栈 + if (t!=null){ + System.out.println(t); //遍历节点 + s.addFirst(t); + t = t.left; + } else { + t = s.removeFirst(); + t = t.right; + } + } +} + +public void LNR(TreeNode root) { + LinkedList s = new LinkedList<>(); + TreeNode t = root; + //当t指针为空,而且堆栈也为空的时候遍历就结束了 + while (t!=null || !s.isEmpty()){ + //每次当t不为空的时候就默认把t压入堆栈 + if (t!=null){ + s.addFirst(t); + t = t.left; + } else { + t = s.removeFirst(); + System.out.println(t); //遍历节点 + t = t.right; + } + } +} +``` + + + +## 图 + + +### 简单的网格 + +#### DFS遍历 + +```java +void dfs(int[][] grid, int r, int c) { + // 判断 base case + // 如果坐标 (r, c) 超出了网格范围,直接返回 + if (!inArea(grid, r, c)) { + return; + } + // 如果这个格子不是岛屿,直接返回。 0表示海洋 + if (grid[r][c] != 1) { + return; + } + // 将格子标记为「已遍历过」 + grid[r][c] = 2; + // 访问上、下、左、右四个相邻结点 + dfs(grid, r - 1, c); + dfs(grid, r + 1, c); + dfs(grid, r, c - 1); + dfs(grid, r, c + 1); +} + +// 判断坐标 (r, c) 是否在网格中 +boolean inArea(int[][] grid, int r, int c) { + return 0 <= r && r < grid.length + && 0 <= c && c < grid[0].length; +} + +``` + +**如何避免重复遍历**? + +- 答案是标记已经遍历过的格子。 + + +1. **求陆地最大面积** + +```java +int areaMax(int[][] grid) { + int res = 0; + for(int i = 0; i < grid.length; i++) { + for(int j = 0; j < grid[0].length; j++) { + if(grid[i][j] == 1) { + res = Math.max(res, area(grid, i, j)) + } + } + + } + return res; +} + +int area(int[][] grid, int r, int c) { + // 判断 base case + // 如果坐标 (r, c) 超出了网格范围,直接返回 + if (!inArea(grid, r, c)) { + return 0; + } + // 如果这个格子不是岛屿,直接返回。 0表示海洋 + if (grid[r][c] != 1) { + return 0; + } + // 将格子标记为「已遍历过」 + grid[r][c] = 2; + // 访问上、下、左、右四个相邻结点 + return 1 + + area(grid, r - 1, c); + + area(grid, r + 1, c); + + area(grid, r, c - 1); + + area(grid, r, c + 1); +} + +// 判断坐标 (r, c) 是否在网格中 +boolean inArea(int[][] grid, int r, int c) { + return 0 <= r && r < grid.length + && 0 <= c && c < grid[0].length; +} +``` + + +2:**填海造陆问题** + +LeetCode 827. Making A Large Island (Hard) \ No newline at end of file diff --git "a/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\346\227\245\345\270\270\345\210\267\351\242\230/1.\344\270\244\346\225\260\344\271\213\345\222\214.go" "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\346\227\245\345\270\270\345\210\267\351\242\230/1.\344\270\244\346\225\260\344\271\213\345\222\214.go" new file mode 100644 index 0000000..6ae2718 --- /dev/null +++ "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\346\227\245\345\270\270\345\210\267\351\242\230/1.\344\270\244\346\225\260\344\271\213\345\222\214.go" @@ -0,0 +1,22 @@ +package main + +/* + * @lc app=leetcode.cn id=1 lang=golang + * + * [1] 两数之和 + */ + +// @lc code=start +func twoSum(nums []int, target int) []int { + map1 := make(map[int]int, len(nums)) + map1[nums[0]] = 0 + for i := 1; i < len(nums); i++ { + if (map1[target-nums[i]] != 0) || ((target - nums[i]) == nums[0]) { + return []int{i, map1[target-nums[i]]} + } + map1[nums[i]] = i + } + return []int{} +} + +// @lc code=end diff --git "a/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\346\227\245\345\270\270\345\210\267\351\242\230/test.java" "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\346\227\245\345\270\270\345\210\267\351\242\230/test.java" new file mode 100644 index 0000000..e69de29 diff --git "a/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\351\235\242\350\257\225\351\242\230/1.\344\270\200\346\254\241\347\274\226\350\276\221.go" "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\351\235\242\350\257\225\351\242\230/1.\344\270\200\346\254\241\347\274\226\350\276\221.go" new file mode 100644 index 0000000..2b2ba3f --- /dev/null +++ "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/leetcode/\351\235\242\350\257\225\351\242\230/1.\344\270\200\346\254\241\347\274\226\350\276\221.go" @@ -0,0 +1,76 @@ +// 字符串有三种编辑操作:插入一个字符、删除一个字符或者替换一个字符。 +// 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。 +package main + +import ( + "fmt" + "math" +) + +func oneEditAway(first string, second string) bool { + if first == second { + return true + } + xLen := math.Abs(float64(len(first) - len(second))) + if xLen >= 2 { + return false + } + + //长度一样,只能交换 + // if xLen == 0 { + // var a byte + // var b byte + // for i := 0; i < len(first); i++ { + // if first[i] != second[i] { + // if a == 0 { + // a = first[i] + // b = second[i] + // continue + // } + // return a == second[i] && b == first[i] + // } + // } + // } + if xLen == 0 { + flag := false + for i := 0; i < len(first); i++ { + if first[i] == second[i] { + continue + } else if flag { + return false + } + flag = true + } + } + + //长度差1,只能在长的里面删除,或者短的里面插入 + if xLen == 1 { + var l string + var s string + if len(first) > len(second) { + l, s = first, second + } else { + l, s = second, first + } + + for i := 0; i < len(s); i++ { + if l[i] == s[i] { + continue + } else { + return l[i+1:] == s[i:] + } + } + + } + + return true + +} + +func main() { + fmt.Printf("%s and %s => %t", "pale", "ple", oneEditAway("pale", "ple")) + + fmt.Printf("%s and %s => %t", "pales", "pal", oneEditAway("pales", "pal")) + + fmt.Printf("%s and %s => %t", "abcdxabcde", "abcdeabcdx", oneEditAway("abcdxabcde", "abcdeabcdx")) +} diff --git "a/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..f0e4cd9 --- /dev/null +++ "b/\347\256\227\346\263\225&\346\225\260\346\215\256\347\273\223\346\236\204/\343\200\212\347\256\227\346\263\225\351\235\242\350\257\225\351\200\232\345\205\263\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,318 @@ + +# 数据结构基础 + +## 学习方法 + +1. 形成只是脉络-主干,然后把其他知识挂上去,记得牢靠 +2. 学习不擅长的骨头,需要刻意反复练习,刚开始是难受的,需要一个过程,慢慢变好。一个曲线 +3. 坚持、可以练习,自己不擅长的,痛苦的题目:动态规划、搜索、回溯 + +**分类**: +- **数据结构**: + - Array + - Stack/Queue + - PriorityQueue(heap) + - LinkedList(single/double) + - Tree/Binary Tree + - HashTable + - Disjoint Set + - Trie + - BloomFilter + - LRU Cache +- **算法**: + - General Coding + - In-order/Pre-order/Post-order traversal + - Greedy + - Recursion/Backtrace + - Breadth-first search + - Depth-first search + - Divide and Conquer + - Dynamic Programming + - Binary Search + - Graph + + +[下面图片参考链接](https://www.bigocheatsheet.com/) + + +**时间复杂度**: +O(1)、N、logN、 +![](../img/《算法面试通关》学习笔记/《算法面试通关》学习笔记_2022-04-06-19-11.png) + + + +数据结构的访问-插入删除的时间复杂度: +![](../img/《算法面试通关》学习笔记/《算法面试通关》学习笔记_2022-04-06-19-04.png) + +**空间复杂度**: + +总体图: +![](../img/《算法面试通关》学习笔记/《算法面试通关》学习笔记_2022-04-06-19-27.png) + + +## 数组&链表 + +- 数组: 随机访问,插入删除复杂度过高。 访问为主的用这个 +- 链表: 插入删除复杂度O(1) + - 插入删除操作频繁用链表 + - 不清楚大小的也可以用这个 + +列表排序时间复杂度: +![](../img/《算法面试通关》学习笔记/《算法面试通关》学习笔记_2022-04-06-19-03.png) + + +**链表**的题目,思维不难,难的是代码基本功,很绕,容易出错 +- 反转链表 +- 反转相邻节点 +- 给定链表,判断是否有环。 + + + ## 栈和队列 + + - 栈:先入先出 + - 队列:先入后出 + + + +栈常见题目: +- 匹配括号 +- 用两个栈实现队列 + + +队列常见题目: +- 用2个队列实现栈 +- 用一个队列实现栈 + + +### 堆 + + +- 703 第K大的元素 + + +239. 滑动窗口最大值 + + + + +### Map 和 Set + + + +1. 两数和 15. 3数之和 18、 四数之和 + + + +### tree 和 graph + +- 102 按层输出 +- 104/111 max/min depth +- 22. 生成括号 +- 235.二叉树/236.二叉搜索树上两个节点最近公共祖先 +- 98.验证二叉搜索树 + + +### 剪枝 +51/52 N皇后问题 + + +36/37 数独 + +### 递归 + +50. x的n次方 + +169. 找出众数,出现次数超过2/n的数 + +122. 买卖股票 + + + +### 二分查找 + +1. 要求数组有序 +2. 要求数组明确上下界 +3. 能够通过索引访问 + +69 题:求y的根号2值, 二分法、牛顿迭代法 + + + +### Trie树 + +208. 实现一个Trie树 + + +79 +212 + + + +### 位运算 + +191 x&(x-1) +231/338 + +52 N皇后&位运算 + + +### 动态规划 + +- 递推(递归+记忆化) +- 状态的定义:opt[n],dp[n],fib[n] +- 状态转移方程:opt[n]=best_of(opt[n-1], opt[n-2],...) +- 最优子结构 + + + +**DP vs 回溯 vs 贪心**: + +- 回溯(递归)——重复计算 +- 贪心 —— 永远局部最优 +- 动态规划—— 记录局部最优子结构、多种记录值 + + +- 70 爬楼梯 +- 120 三角 +- 152 相乘最大数 +- 121/122/123、309,188,714 股票 +- 300 最长递增子序列 +- 322 coin +- 72 编辑次数, 特别经典 + + +### 并查集 +- 200 islands +- 547 朋友圈 + + + +### LRU Cache +缓存替换算法 + +LFU- 最近最不常使用频率 + +- 146题 + +### 布隆过滤器 + +多个hash函数, 不在就肯定不在,在可能误报。 +bitcoin判断交易在哪个块里 + + + +## 面试 + +### 代码模板 + +**递归** +- 满足三个条件 + - 大问题拆成2个子问题 + - 子问题求解方式跟大问题一样 + - 存在最小子问题 + +```java +//terminator 终止条件 + +// process + +// drill down 进入下一层 + +// clear states还原一些影响 + +``` + +**DP** + +```java +// DP formula + +``` + + +### 面试 +1. 持续练习、刻意练习。 切题爽,但是切到一定阶段,要刻意去练习自己不太熟悉的数据结构和类型题目。 +2. 面试四件套: + 1. 确认细节:询问题目细节,边界条件,极端情况 + 2. 所有的解法都和面试官沟通一遍,当做同事探讨问题,而不是监考老师。 然后选择一种最快或者省空间,或者最优雅的解法 + 3. Code + 4. Test Case + + +## 最后的最后 + +- 3分学7分练 +- 环境准备 +- 切题姿势: + - 120题三角形:递归、dp + + +## 字节跳动热门题库 + +- [x] 1 两数之和(Two Sum) +- [x] 3 最长无重复子串(Longest Substring Without Repeating Characters) +- [x] 4 **寻找两个正序数组的中位数**(Median of Two Sorted Arrays) +- [x] 5 **最长回文子串**(Longest Palindromic Substring) xxx +- [ ] 8 字符串转换整数 (atoi) +- [x] 11 盛最多水的容器(Container With Most Water) +- [ ] 14 最长公共前缀 +- [ ] 15. 三数之和 +- [ ] 12 矩阵中的路径 +- [ ] 20 有效的括号 +- [x] 23 **合并K个排序链表**(Merge k Sorted Lists) +- [x] 43 字符串相乘(Multiply Strings) x +- [ ] 55 跳跃游戏 +- [ ] 53 最大子序和 +- [x] 64 最小路径和 +- [x] 70 **爬楼梯** +- [x] 76 最小覆盖子串(Minimum Window Substring) +- [ ] 88 合并两个有序数组 +- [ ] 94 二叉树的遍历(前序、中序、后序、层次遍历) + + +- [ ] 104 二叉树的最大深度 +- [ ] 110 平衡二叉树 +- [ ] 111 二叉树的最小深度 +- [ ] 121 买卖股票的最佳时机 + +- [ ] 153 寻找旋转排序数组中的最小值 + +- [ ] 155 最小栈 +- [ ] 159 无重复字符的最长子串(Longest Substring with At Most Two Distinct Characters) +- [ ] 146 LRU缓存机制 +- [ ] 160 相交链表 +- [ ] 198 打家劫舍 +- [ ] 200 岛屿问题 +- [ ] 206 **反转链表** xx +- [ ] 236 二叉树的公共祖先 +- [ ] 239 滑动窗口最大值 +- [ ] 300 最长上升子序列 +- [ ] 322 零钱兑换 +- [ ] 347 前 K 个高频元素 +- [ ] leetcode 373. 查找和最小的 K 对数. 要求讲出时间复杂度 +- [ ] 435 无重叠区间(Non-overlapping Intervals) +- [ ] 567 字符串的排列 +- [ ] 621 任务调度器 +- [ ] 704 二分查找 +- [ ] 912 **快排** 归并排序 堆排序 xx +- [ ] 1143 最长公共子序列 + +- [ ] 一道leetcode,给20-30分钟: +- [ ] n个骰子的点数和为k的概率 +- [ ] 实现LFU算法,有时间复杂度要求 + + +- [ ] 题目:思路:略微更改了一下原题,给出的题目其实是查找和最大的K对,是一道leetcode中等难度。算是经典的多路归并问题。 + +这里给一些类似的题目可以学习一下多路归并: +1. 丑数 II +2. 超级丑数 +3. 查找和最小的K对数字 +4. 最小区间 +5. 找出第 k 小的距离对 +6. 第 K 个最小的素数分数 +7. 有序矩阵中的第 k 个最小数组和 +8. 子数组和排序后的区间和 +9. 数组的最小偏移量 + +堆排序的时间复杂度、空间复杂度、排序的的过程。 diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Git\345\255\246\344\271\240\345\205\245\351\227\250.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Git\345\255\246\344\271\240\345\205\245\351\227\250.md" index 9bb6c47..ab4f871 100644 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Git\345\255\246\344\271\240\345\205\245\351\227\250.md" +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Git\345\255\246\344\271\240\345\205\245\351\227\250.md" @@ -234,3 +234,26 @@ git reset --soft HEAD^ ``` - 这样就成功的撤销了你的commit, 注意,仅仅是撤回commit操作,您写的代码仍然保留。 - HEAD^的意思是上一个版本,也可以写成HEAD~1, 如果你进行了2次commit,想都撤回,可以使用HE + + +## 如何迁移代码库 + +1. 在 GitHub 建立新仓库,空的 +2. 克隆 Coding 上的项目到本地。 + - 本地执行 +```s +git clone https://git.coding.net/wenyuan/blog.git --bare +``` + +3. 将克隆下来的仓库推送到GitHub + 1. 使用新仓库页面提供的仓库地址(web URL),推送所有的分支和对象 + ```s + cd blog.git + + git push https://github.com/wenyuan/blog.git --all + ``` +4. 完成后,再执行推送所有的Tags + +```s +git push https://github.com/wenyuan/blog.git --tags +``` \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Linux\345\237\272\347\241\200.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Linux\345\237\272\347\241\200.md" deleted file mode 100644 index c8626b0..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Linux\345\237\272\347\241\200.md" +++ /dev/null @@ -1,57 +0,0 @@ -# 常用操作 - -## 查看 - -1. 查磁盘使用率 -``` -df -Th -``` - - 查看当前目录大小 - - du -h --max-depth=0 - > --max-depth=n表示只深入到第n层目录,此处设置为0,即表示不深入到子目录。 - - du -s * | sort -nr | head 选出排在前面的10个, - - du -s * | sort -nr | tail 选出排在后面的10个。 - -2. 网络 - -``` - ifconfig -``` - -3. 内存使用情况 -``` -free -m - -以MB为单位显示内存使用情况 -``` -- 查看java程序设的内存,可以通过 ps -ef | grep jar (如果是resin容器启动,就看resin,设置是conf里面的resin.properties) - -jvm_args : -Xms1024m -Xmx15000m -XX:MaxPermSize=2048m -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=n - -1. CPU - -top后键入P看一下谁占用最大 -``` -# top -d 5 -``` - -5. 端口占用情况: - 1. windows对比 - ``` - netstat -aon|findstr "49157" 查到pid为2720 - tasklist|findstr "2720" 查看经常为 svchost.exe - 打开任务管理器,关掉或者。 - - taskkill /f /t /im svchost.exe - - taskkill -f -pid 14128 - - ``` - -## 字符串操作 - -**查找目录下所有文件中是否包含某个字符串**: -```sh -find .|xargs grep -ri "showIdeaDetailList.action" -``` - -## 网络,文件 -**下载文件**: `curl http://www.linux.com >> linux.html` \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\343\200\212\350\256\276\350\256\241\346\250\241\345\274\217\344\271\213\347\276\216\343\200\213-\350\257\276\347\250\213\347\254\224\350\256\260.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\343\200\212\350\256\276\350\256\241\346\250\241\345\274\217\344\271\213\347\276\216\343\200\213-\350\257\276\347\250\213\347\254\224\350\256\260.md" deleted file mode 100644 index 81801c8..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\343\200\212\350\256\276\350\256\241\346\250\241\345\274\217\344\271\213\347\276\216\343\200\213-\350\257\276\347\250\213\347\254\224\350\256\260.md" +++ /dev/null @@ -1,111 +0,0 @@ -# 《设计模式之美》 - -# 为什么学习设计模式 - -1. 应对面试中的设计模式相关问题 -2. 告别写被人吐槽的烂代码 - 1. 我见过太多的烂代码,比如命名不规范、类设计不合理、分层不清晰、没有模块化概念、代码结构混乱、高度耦合等等。这样的代码维护起来非常费劲,添加或者修改一个功能,常常会牵一发而动全身,让你无从下手,恨不得将全部的代码删掉重写! - 2. 每当我看到这样的好代码,都会立刻对作者产生无比的好感和认可。且不管这个人处在公司的何种级别,从代码就能看出,他是一个基础扎实的高潜员工,值得培养,前途无量!因此,代码写得好,能让你在团队中脱颖而出 -3. 提高复杂代码的设计和开发能力 - 1. 只是完成功能、代码能用,可能并不复杂,但是要想写出易扩展、易用、易维护的代码,并不容易。 - 2. 如何分层、分模块?应该怎么划分类?每个类应该具有哪些属性、方法?怎么设计类之间的交互?该用继承还是组合?该使用接口还是抽象类?怎样做到解耦、高内聚低耦合?该用单例模式还是静态方法?用工厂模式创建对象还是直接 new 出来?如何避免引入设计模式提高扩展性的同时带来的降低可读性问题? -4. 让读源码、学框架事半功倍 - 1. 优秀的开源项目、框架、中间件,代码量、类的个数都会比较多,类结构、类之间的关系极其复杂,常常调用来调用去。所以,为了保证代码的扩展性、灵活性、可维护性等,代码中会使用到很多设计模式、设计原则或者设计思想。如果你不懂这些设计模式、原则、思想,在看代码的时候,你可能就会琢磨不透作者的设计思路 - 2. 还有一个隐藏的问题,你可能自己都发现不了,那就是你自己觉得看懂了,实际上,里面的精髓你并没有 get 到多少!因为优秀的开源项目、框架、中间件,就像一个集各种高精尖技术在一起的战斗机。 -5. 为你的职场发展做铺垫 - -# 好代码 - -## 评判好坏标准 - -**评价词**:灵活性(flexibility)、可扩展性(extensibility)、可维护性(maintainability)、可读性(readability)、可理解性(understandability)、易修改性(changeability)、可复用(reusability)、可测试性(testability)、模块化(modularity)、高内聚低耦合(high cohesion loose coupling)、高效(high effciency)、高性能(high performance)、安全性(security)、兼容性(compatibility)、易用性(usability)、整洁(clean)、清晰(clarity)、简单(simple)、直接(straightforward)、少即是多(less code is more)、文档详尽(well-documented)、分层清晰(well-layered)、正确性(correctness、bug free)、健壮性(robustness)、鲁棒性(robustness)、可用性(reliability)、可伸缩性(scalability)、稳定性(stability)、优雅(elegant)、好(good)、坏(bad)…… - -**常用评价标准**: -1. **可维护性(maintainability)** - 1. 如果 bug 容易修复,修改、添加功能能够轻松完成,那我们就可以主观地认为代码对我们来说易维护。 - 2. 相反,如果修改一个 bug,修改、添加一个功能,需要花费很长的时间,那我们就可以主观地认为代码对我们来说不易维护。 - >代码的可维护性是由很多因素协同作用的结果。代码的可读性好、简洁、可扩展性好,就会使得代码易维护;相反,就会使得代码不易维护。更细化地讲,如果代码分层清晰、模块化好、高内聚低耦合、遵从基于接口而非实现编程的设计原则等等,那就可能意味着代码易维护。除此之外,代码的易维护性还跟项目代码量的多少、业务的复杂程度、利用到的技术的复杂程度、文档是否全面、团队成员的开发水平等诸多因素有关。 - -2. **可读性(readability)** - 1. 是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合 - 2. code review时同事看得轻松就是可读性好。 -3. **可扩展性(extensibility)** - 1. 方便加新功能。 -4. 灵活性(flexibility) - 1. 当我们添加一个新的功能代码的时候,原有的代码已经预留好了扩展点,我们不需要修改原有的代码,只要在扩展点上添加新的代码即可。这个时候,我们除了可以说代码易扩展,还可以说代码写得好灵活。 - 2. 当我们要实现一个功能的时候,发现原有代码中,已经抽象出了很多底层可以复用的模块、类等代码,我们可以拿来直接使用。这个时候,我们除了可以说代码易复用之外,还可以说代码写得好灵活。 - 3. 当我们使用某组接口的时候,如果这组接口可以应对各种使用场景,满足各种不同的需求,我们除了可以说接口易用之外,还可以说这个接口设计得好灵活或者代码写得好灵活。 -5. 简洁性(simplicity) - 1. 符合 KISS 原则 - 2. 思从深而行从简,真正的高手能云淡风轻地用最简单的方法解决最复杂的问题。这也是一个编程老手跟编程新手的本质区别之一。 -6. 可复用性 - 1. 代码可复用性跟 DRY(Don’t Repeat Yourself)这条设计原则的关系挺紧密的 - 2. 当讲到面向对象特性的时候,我们会讲到继承、多态存在的目的之一,就是为了提高代码的可复用性;当讲到设计原则的时候,我们会讲到单一职责原则也跟代码的可复用性相关;当讲到重构技巧的时候,我们会讲到解耦、高内聚、模块化等都能提高代码的可复用性。 -7. 可测试性(testability) - -## 面向对象、设计原则、设计模式、编程规范、重构 - -- [ ] 很多人用面向对象语言,写面向过程的代码 - -### 面向对象 VS 面向过程 -问题: -- 什么是面向过程编程与面向过程编程语言? -- 面向对象编程相比面向过程编程有哪些优势? -- 为什么说面向对象编程语言比面向过程编程语言更高级? -- 有哪些看似是面向对象实际是面向过程风格的代码? -- 在面向对象编程中,为什么容易写出面向过程风格的代码? -- 面向过程编程和面向过程编程语言就真的无用武之地了吗? - -**概念**: -- 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。 -- 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。 -- 面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以**数据**(可以理解为成员变量、属性)**与方法相分离为最主要的特点**。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。 - -**区别**: -- 代码的组织方式不同。 - - 面向过程风格的代码被组织成了一组方法集合及其数据结构(struct User),方法和数据结构的定义是分开的。 - - 面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中 - -**OOP的优势**: -1. OOP 更加能够应对大规模复杂程序的开发 - 1. 在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是采用曲线救国的策略,先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。 - 2. 这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。 - 3. 利用面向过程的编程语言照样可以写出面向对象风格的代码,只不过可能会比用面向对象编程语言来写面向对象风格的代码,付出的代价要高一些。 -2. OOP 风格的代码更易复用、易扩展、易维护 - 1. 而面向对象编程提供的封装、抽象、继承、多态这些特性,能极大地满足复杂的编程需求,能方便我们写出更易复用、易扩展、易维护的代码。 -3. OOP 语言更加人性化、更加高级、更加智能 - 1. **编程语言越来越人性化**,让人跟机器打交道越来越容易。笼统点讲,就是编程语言越来越高级。 - 2. 在进行面向对象编程时候,我们是在思考,如何给业务建模,如何将真实的世界映射为类或者对象,这**让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道**。可以这么说,越高级的编程语言离机器越“远”,离我们人类越“近”,越“智能”。 - 3. 如果一种新的突破性的编程语言出现,那它肯定是**更加“智能”**的。大胆想象一下,使用这种编程语言,我们可以无需对计算机知识有任何了解,无需像现在这样一行一行地敲很多代码,**只需要把需求文档写清楚,就能自动生成我们想要的软件了**。 - >但其实,进行面向对象编程的时候,很容易不由自主地就写出面向过程风格的代码,或者说感觉面向过程风格的代码更容易写。这是为什么呢? - 你可以联想一下,在生活中,你去完成一个任务,你一般都会思考,应该先做什么、后做什么,如何一步一步地顺序执行一系列操作,最后完成整个任务。**面向过程编程风格恰恰符合人的这种流程化思维方式**。 - - 而面向对象编程风格正好相反。它是一种自底向上的思考方式。它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。 - - 这样的思考路径比较适合复杂程序的开发,但并不是特别符合人类的思考习惯。 - - **让人迷惑,到底是面向对象更符合人的思维习惯,还是面向过程更符合?** - - -**哪些代码设计看似是面向对象,实际是面向过程** -1. 滥用getter、setter方法 - 1. 它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格 - 2. 在设计实现类的时候,除非真的需要,否则,尽量不要给属性定义 setter 方法。除此之外,尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器(比如例子中的 List 容器),也要防范集合内部数据被修改的危险。 -2. 滥用全局变量和全局方法 - 1. 问题 - 1. 常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。单例类对象在全局代码中只有一份,所以,它相当于一个全局变量。 - 2. 静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。 - 3. 而常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。 - 4. 静态方法一般用来操作静态变量或者外部数据。你可以联想一下我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。 - 5. 静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。 - 2. 常量类,如果只在自己类用到,可以定义到类里面。 如果其他可能用,也要做下区分MysqlConstants、RedisConfig,不要放在一个大的CommonConstants ,因为会有几个问题: - 1. 这样的设计会影响代码的可维护性。 - 2. 这样的设计还会增加代码的编译时间。 - 3. 这样的设计还会影响代码的复用性。 - 3. Utils类 - 1. **解决什么问题**:从业务含义上,A 类和 B 类并不一定具有继承关系,比如 Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能,但并不具有继承关系(既不是父子关系,也不是兄弟关系)。仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。如果不熟悉背后设计思路的同事,发现 Crawler 类和 PageAnalyzer 类继承同一个父类,而父类中定义的却是 URL 相关的操作,会觉得这个代码写得莫名其妙,理解不了。 - 2. 然后呢? 只包含静态方法不包含任何属性的 Utils 类,是**彻彻底底的面向过程的编程风格**。但这并不是说,我们就要杜绝使用 Utils 类了。实际上,从刚刚讲的 Utils 类存在的目的来看,它在软件开发中还是挺有用的,能解决代码复用问题。所以,这里并不是说完全不能用 Utils 类,而是说,要尽量避免滥用,不要不加思考地随意去定义 Utils 类。 - 3. 思考: - 1. 你真的需要单独定义这样一个 Utils 类吗?是否可以把 Utils 类中的某些方法定义到其他类中呢? - 2. FileUtils、IOUtils、StringUtils、UrlUtils不同用不同 -3. **定义数据和方法分离的类** - 1. 一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。 - 2. 实际上,这种开发模式叫作**基于贫血模型**的开发模式,也是我们现在非常常用的一种 Web 项目的开发模式 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/Linux\345\237\272\347\241\200.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/Linux\345\237\272\347\241\200.md" new file mode 100644 index 0000000..dda641e --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/Linux\345\237\272\347\241\200.md" @@ -0,0 +1,173 @@ +# 常用操作 + +## 常用命令 +- **ls** + - `ll` = `ls -al` 别名 +- **cd** + - `cd ~` = `cd $HOME` + - `cd -`,回到之前的工作目录 +- **cp** + - `cp -r 源 目标` :-r,复制该目录下的所有子目录和文件 + - `cp -s /home/cmd/hello.c softlink`: 创建软链接 +- **cat** 小文件 + - `-n` 显示行号 +- **more** 大文件 + - 当文件的内容大于一屏时,more命令可以按页来显示,并且支持翻页、直接跳转行 + - 打开后命令: q退出;回车一行;空格键下一页;ctrl+b 上一页;=显示当前行号;d,f跳很多页; + - +n 从第n行开始显示 + - -n 指定每屏显示的行数 + - +/pattern 在每个文档显示前搜寻该字(pattern),然后从该字串之后开始显示 +- **less** + - 与more类似,可以前后翻页,用键盘上下键即可。 + - q退出;b 向后翻一页;u 向前滚动半页; + - /hello:向下搜索字符串“hello”; ?hello:向上搜索字符串“hello” +- **touch** + - 改变一个文件的时间戳,或者新建一个空文件. +- **mkdir** + - `-p` 新建一个已存在的文件夹不会报错 + - `-m` 设置目录的读写权限。 + - 比如新建一个具有读写、执行权限的目录:scripts:`mkdir -m 777 scripts` +- **rm** + - `rm -r` 命令删除一个**目录**时,该目录下的所有子目录、文件都会被删除。 + - `rm -i test/*` 删除前需要确认。 + +- **tar** 打包和解压文件 + - 解压: 常用-zxvf + - -z 通过gzip指令压缩/解压缩文件,文件格式:*.tar.gz + - 打包: + - -c 新建打包压缩文件 + - -r 添加文件到压缩文件 + - 将某一个目录(比如:test)打包成压缩文件包test.tar.gz: `tar cvfz test.tar.gz test/` + - 通用 + - -x 解压缩打包文件 + - -v 在压缩/解压缩过程中,显示正在处理的文件名或目录 + - -f (压缩或解压时)指定要处理的压缩文件 + - -C dir 指定压缩/解压缩的目录,若无指定,默认是当前目录 + - —delete 从压缩文件中删除指定的文件 + +- **chmod** 改变文件或目录的权限 + - 读权限的值为4、写的权限值为2、可执行的权限值为1, 读写6,读执行5; + - -R参数是可选的,可以进行递归地持续更改,将指定目录下所有的子目录或文件都修改。 +- **whereis**: 定位一个文件的存储位置,这个文件可以是二进制文件、源文件或文本文件。 +- **whichis**: 在PATH环境变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。 +- **whatis**: 用一句话介绍命令的功能 +- **tee**: 从标准输入设备读取数据,并写到标准输出设备和指定的文件上。 + - `tee input.txt`: 使用tee命令后,shell就会进入输入交互状态,接收用户从标准输入设备(一般是键盘)输入的字符,将用户输入的字符显示到屏幕上,并写到指定的文件input.txt。 + - ` tee input1.txt input2.txt input3.txt` 内容一样 + - `tailf all.log | tee test.log` 会把hello.txt的内容 写入test.log +- **wc** : 统计一个文件中的行数、字数、字节数。 + * -w 统计字数:由空白、tab或换行字符分隔的字符串个数 + * -c 统计字节数,单位为Byte + * -l 统计行数 + * -m 统计字符数 + * -L 打印最长行的长度 +* **ifconfig**: 用来查看和配置网络设备 + * -a 查看全部网络接口配置信息 + * -s 简短摘要信息 类似 `netstat -i` + +## 查看 + +- `tree` 显示目录的树状结构。 + +### 按照资源分类 +1. 查**磁盘**使用率 +``` +df -Th +``` + +- 查看当前目录大小 +- du -h --max-depth=0 + > --max-depth=n表示只深入到第n层目录,此处设置为0,即表示不深入到子目录。 +- du -s * | sort -nr | head 选出排在前面的10个, +- du -s * | sort -nr | tail 选出排在后面的10个。 + + +2. **网络** + +``` + ifconfig +``` + +**端口使用情况** +```s +netstat -an | ag 2181 +``` + + +3. **内存**使用情况 +``` +free -m + +以MB为单位显示内存使用情况 +``` +- 查看java程序设的内存,可以通过 ps -ef | grep jar (如果是resin容器启动,就看resin,设置是conf里面的resin.properties) + +jvm_args : -Xms1024m -Xmx15000m -XX:MaxPermSize=2048m -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=n + + +4. **CPU** + +top后键入P看一下谁占用最大 +``` +# top -d 5 +``` + + +5. **端口**占用情况: + 1. windows对比 + ``` + netstat -aon|findstr "49157" 查到pid为2720 + tasklist|findstr "2720" 查看经常为 svchost.exe + 打开任务管理器,关掉或者。 + - taskkill /f /t /im svchost.exe + - taskkill -f -pid 14128 + + ``` +## 怎么查看日志 + +### tail + +`tail -300f test.log` 循环实时查看最后1000行记录(最常用的) + +`tail -fn 1000 test.log | grep '关键字'` + +`tail -n 4700 aa.log |more -1000 可以进行多屏显示(ctrl + f 或者 空格键可以快捷键)` + + +### sed +这个命令可以查找日志文件特定的一段 , 根据时间的一个范围查询,可以按照行号和时间范围查询. + +- 按照行号 +`sed -n '5,10p' filename` 这样你就可以只查看文件的第5行到第10行。 + +- 按照时间段 + `sed -n '/2014-12-17 16:17:20/,/2014-12-17 16:17:36/p' test.log` + +### less +一般流程是这样的: +``` + less log.log +shift + G 命令到文件尾部 然后输入 ?加上你要搜索的关键字例如 ?1213 按 n 向上查找关键字 +shift+n 反向查找关键字 less与more类似,使用less可以随意浏览文件,而more仅能向前移动,不能向后移动,而且 less 在查 看之前不会加载整个文件。 +less log2013.log 查看文件 +ps -ef | less ps查看进程信息并通过less分页显示 +history | less 查看命令历史使用记录并通过less分页显示 +less log2013.log log2014.log 浏览多个文件 +``` + +**`!!` 重复执行上一个命令** + +## 字符串操作 + +**查找目录下所有文件中是否包含某个字符串**: +```sh +find .|xargs grep -ri "showIdeaDetailList.action" +``` + +## 网络,文件 +**下载文件**: `curl http://www.linux.com >> linux.html` + + +# 基础核心概念 + + diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Shell\350\204\232\346\234\254\345\205\245\351\227\250.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/Shell\350\204\232\346\234\254\345\205\245\351\227\250.md" similarity index 84% rename from "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Shell\350\204\232\346\234\254\345\205\245\351\227\250.md" rename to "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/Shell\350\204\232\346\234\254\345\205\245\351\227\250.md" index 810a63f..b3d4770 100644 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/Shell\350\204\232\346\234\254\345\205\245\351\227\250.md" +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/Shell\350\204\232\346\234\254\345\205\245\351\227\250.md" @@ -138,3 +138,28 @@ chmod +x ./test.sh #使脚本具有执行权限 单对中括号,直接报错: + +# 案例实践 + +## 批量解压、分类归档压缩 + +```shell +#!/bin/sh +# 1. 批量解压 +for zipfile in `ls tauriel关键词下载-*.zip` +do + unzip $zipfile -d temp +done + +for file in `ls temp/download_cpc_info_*.csv` +do + filename=$(basename "$file") + # 名称download_cpc_info_22525608_1.csv 通过‘_’来分隔,如果想要取账号,就是取第4项 + accountId=`echo $filename | cut -d '_' -f 4` + echo "Input File: $file" + echo "Input FileName: $filename" + echo "Input accountId: $accountId" + zip $accountId.zip $file +done + +``` \ No newline at end of file diff --git "a/\347\274\226\347\250\213\345\267\245\345\205\267\350\275\257\344\273\266\347\233\270\345\205\263.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\267\245\345\205\267\350\275\257\344\273\266\347\233\270\345\205\263.md" similarity index 100% rename from "\347\274\226\347\250\213\345\267\245\345\205\267\350\275\257\344\273\266\347\233\270\345\205\263.md" rename to "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\274\226\347\250\213\345\267\245\345\205\267\350\275\257\344\273\266\347\233\270\345\205\263.md" diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234\351\202\243\347\202\271\344\272\213\345\204\277.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234\351\202\243\347\202\271\344\272\213\345\204\277.md" deleted file mode 100644 index 214589a..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234\351\202\243\347\202\271\344\272\213\345\204\277.md" +++ /dev/null @@ -1,33 +0,0 @@ -# 网络那点事 -[TOC] - -# TCP/IP协议 - -其实不管哪一层的协议,协议包含的报头信息一般都有: -- 源地址,目标地址,TCP是端口号。我要知道地方才能传输呀; -- 上一层用的的协议,肯定得懂规矩。 -- 数据:什么本层报头,上一层报头,上一层数据....; -- 对了,当初因为传输不靠谱,一般都有个校验值。 - -IP协议: - - TTL 数据报的生存时间,可经过的最多路由器总数。没经过一个路由器,该值减1,为0就丢弃该报文,并发送 **ICMP**报文通知源主机,防止其一直发送报文。 - >ICMP是检测传输网络是否通畅、主机是否可达、路由是否可用等网 络运行状态的协议。 - >虽然并不传输用户数据,但是对评估网络健康状态非常重 要,经常使用的 ping、 tracert 命令就是基于 ICMP 检测网络状态的有力工具。 - - -## 三次握手 -- 信息对等 - > 第三次握手后:B机器才能确认自己的发报能力和对方的收报能力 -- 防止超时 - > **这些都是源于报文可能丢失!!!或者迷路慢了。** TTL网络报文的生存时间 > TCP请求超时时间 ==> 两次握手建立连接的话,第一次连接请求慢到了,但是第二次重试连接成功,而且通信结束,顺便关闭连接了,(此时A已经不是SYN_SENT,不会接收请求)这第一次迷路的连接请求才到,B机器就以为A要跟他建立新连接,跟A发确认数据,A会直接丢掉,B就苦苦等待,也就是出现脏连接。 - -## 四次挥手 -1. A FIN=1,告诉B我准备分手了。 A现在是待分手(半关闭状态 FIN_WAIT_1) -2. B ACK=1 , 告诉B 分手就分手,谁怕谁!就是有点突然,等我准备一下(处理完数据)。B现在是待分手(半关闭状态 CLOSE_WAIT)A收到ACK进入FIN_WAIT_2(如果B这渣男不需要准备,直接发ACK、FIN),A可以直接跳过这个状态进入3中的TIME_WAIT -3. B FIN=1、ACK=1,主动告诉B,我做好准备了,随手可以分手。B现在是LAST_ACK -4. A ACK=1,确定了对方也做好分手的准备了,最后一次告诉你,分手吧不用回。 A现在是TIME_WAIT(顾名思义等某个时间),等两个月(2MSL),还没收到B的挽回消息,那就能确定,B收到了A最后的分手通牒。 **正式分手成功!!** - -- 这个MSL其实挺长的,在当前告诉网络,这是非常耗时的,在高并发等服务器上这是浪费资源。但是为什么还非要等呢? - 1. 确认被动关闭放能顺利进入CLOSED状态,B表示 我被分手但是不甘心,没收到你的最后通牒我是不会死心的。 如果你不给我时间,我可能就会一直拖着。 - 2. 防止失效请求。 -- TIME_WAIT状态下是无法释放句柄资源的,高并发服务器上会极大限制有效连接的数量,成为性能瓶颈。所以, **建议将高并发服务器的TIME_WAIT超时时间调小**。 (小于30秒为宜),怎么改? `/etc/sysctl.conf`,修改值 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\347\275\221\347\273\234\351\202\243\347\202\271\344\272\213\345\204\277.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\347\275\221\347\273\234\351\202\243\347\202\271\344\272\213\345\204\277.md" new file mode 100644 index 0000000..a7878a0 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/\347\275\221\347\273\234\351\202\243\347\202\271\344\272\213\345\204\277.md" @@ -0,0 +1,212 @@ +# 网络那点事 +[TOC] + +# TCP/IP协议 + +## TCP介绍 + +其实不管哪一层的协议,协议包含的报头信息一般都有: +- 源地址,目标地址,TCP是端口号。我要知道地方才能传输呀; +- 上一层用的的协议,肯定得懂规矩。 +- 数据:什么本层报头,上一层报头,上一层数据....; +- 对了,当初因为传输不靠谱,一般都有个校验值。 + +IP协议: + - TTL 数据报的生存时间,可经过的最多路由器总数。没经过一个路由器,该值减1,为0就丢弃该报文,并发送 **ICMP**报文通知源主机,防止其一直发送报文。 + >ICMP是检测传输网络是否通畅、主机是否可达、路由是否可用等网 络运行状态的协议。 + >虽然并不传输用户数据,但是对评估网络健康状态非常重 要,经常使用的 ping、 tracert 命令就是基于 ICMP 检测网络状态的有力工具。 + +### TCP与UDP +传输层: TCP可靠,UDP不可靠 +- TCP使用:`fd = socket(AF_INET,SOCK_STREAM,0);` + - 其中SOCK_STREAM,是指使用字节流传输数据,就是TCP协议。这也是TCP基于字节流的题中之义,UDP是面向数据包。TCP还有两个特点:可靠、面向连接 + - 在定义了socket之后,我们就可以愉快的对这个socket进行操作,比如用bind()绑定IP端口,用connect()发起建连。之后就可以recv()和send()接发数据了 + + +#### TCP基于字节流 + +- 字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串。 +- 纯裸TCP收发的这些 01 串之间是**没有任何边界**的,你根本不知道到哪个地方才算一条完整消息,这就是所谓的**粘包问题**。 +- **所以需要约定个规则去区分这些01串的边界、含义**——这就是上层协议,于是基于TCP,就衍生了非常多的协议,比如HTTP和RPC(这些是定义了不同消息格式的应用层协议)。 + - 比如约定消息头,消息头里写清楚一个完整的包长度是多少、消息体是否被压缩过和消息体格式之类的,只要上下游都约定好了,互相都认就可以了,这就是所谓的**协议**。 + + + +## 三次握手 & 四次挥手 + +### 三次握手 +- 信息对等 + > 第三次握手后:B机器才能确认自己的发报能力和对方的收报能力 +- 防止超时 + > **这些都是源于报文可能丢失!!!或者迷路慢了。** TTL网络报文的生存时间 > TCP请求超时时间 ==> 两次握手建立连接的话,第一次连接请求慢到了,但是第二次重试连接成功,而且通信结束,顺便关闭连接了,(此时A已经不是SYN_SENT,不会接收请求)这第一次迷路的连接请求才到,B机器就以为A要跟他建立新连接,跟A发确认数据,A会直接丢掉,B就苦苦等待,也就是出现脏连接。 + +### 四次挥手 +1. A FIN=1,告诉B我准备分手了。 A现在是待分手(半关闭状态 FIN_WAIT_1) +2. B ACK=1 , 告诉B 分手就分手,谁怕谁!就是有点突然,等我准备一下(处理完数据)。B现在是待分手(半关闭状态 CLOSE_WAIT)A收到ACK进入FIN_WAIT_2(如果B这渣男不需要准备,直接发ACK、FIN),A可以直接跳过这个状态进入3中的TIME_WAIT +3. B FIN=1、ACK=1,主动告诉B,我做好准备了,随手可以分手。B现在是LAST_ACK +4. A ACK=1,确定了对方也做好分手的准备了,最后一次告诉你,分手吧不用回。 A现在是TIME_WAIT(顾名思义等某个时间),等两个月(2MSL),还没收到B的挽回消息,那就能确定,B收到了A最后的分手通牒。 **正式分手成功!!** + +- 这个MSL其实挺长的,在当前告诉网络,这是非常耗时的,在高并发等服务器上这是浪费资源。但是为什么还非要等呢? + 1. 确认被动关闭放能顺利进入CLOSED状态,B表示 我被分手但是不甘心,没收到你的最后通牒我是不会死心的。 如果你不给我时间,我可能就会一直拖着。 + 2. 防止失效请求。 +- TIME_WAIT状态下是无法释放句柄资源的,高并发服务器上会极大限制有效连接的数量,成为性能瓶颈。所以, **建议将高并发服务器的TIME_WAIT超时时间调小**。 (小于30秒为宜),怎么改? `/etc/sysctl.conf`,修改值 + + +### QUIC 是如何解决TCP 性能瓶颈的? +TCP 队头阻塞的主要原因是数据包超时确认或丢失阻塞了当前窗口向右滑动,我们最容易想到的解决队头阻塞的方案是不让超时确认或丢失的数据包将当前窗口阻塞在原地。 + +QUIC (Quick UDP Internet Connections)也正是采用上述方案来解决TCP 队头阻塞问题的。 + + + +## HTTP + + +### HTTP1.1 + +1997年1月,HTTP/1.1 版本发布,只比 1.0 版本晚了半年。它进一步完善了 HTTP 协议,一直用到了20年后的今天,直到现在还是最流行的版本。 + +- 持久连接 + - TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。 + - 主动关闭:客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接。 +- 管道机制 + - 在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。服务器还是按照顺序回应。 +- Content-Length 字段 + - 一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是Content-length字段的作用,声明本次回应的数据长度。 +- 分块传输编码 + - 长度放在最前面,那就要在所有数据确认完后再发送,这意味着,服务器要等到所有操作完成,才能发送数据。太低效了。 + - 改进:产生一块数据,就发送一块,采用"流模式"(stream)取代"缓存模式"(buffer)。 + - 只要请求或回应的头信息有Transfer-Encoding字段,就表明回应将由数量未定的数据块组成。`Transfer-Encoding: chunked`. + - 每个非空的数据块之前,会有一个16进制的数值,表示这个块的长度。最后是一个大小为0的块,就表示本次回应的数据发送完了。 +- 客户端请求的头信息新增了Host字段,用来指定服务器的域名。有了Host字段,就可以将请求发往同一台服务器上的不同网站,为虚拟主机的兴起打下了基础。 + +虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为"队头堵塞"(Head-of-line blocking)。 + +参考链接:http://www.ruanyifeng.com/blog/2016/08/http.html + + +### HTTP/2 + +2015年,HTTP/2 发布。它不叫 HTTP/2.0,因为标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3。 + +- **二进制协议**: + - HTTP/1.1 版的头信息肯定是**文本(ASCII编码)**,数据体可以是文本,也可以是二进制。 + - HTTP/2 则是一个**彻底的二进制协**议,头信息和数据体都是二进制,并且统称为"帧"(frame):头信息帧和数据帧。 + - 二进制协议的一个好处是,可以定义额外的帧。比如 HEADERS 和 DATA 帧构成了 HTTP 请求和响应的基础; + - 在 HTTP/2 中定义了 10 种不同类型的帧,每种帧类型都有不同的用途。其它帧类型(比如 PRIORITY、SETTINGS、PUSH_PROMISE、WINDOW_UPDATE 等 )用于支持其它 HTTP/2 功能。 +- **多工(Multiplexing)**: + - HTTP/2 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞"。 + - 举个栗子:在一个TCP连接里面,服务器同时收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。这种双向的、实时的通信,就叫做多工(Multiplexing) +- **数据流**: + - 因为 HTTP/2 的数据包是**不按顺序发送**的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要**对数据包做标记**,指出它属于哪个回应。 + - HTTP/2 将**每个请求**或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。 + - 数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。 + - 客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。 +- **Header压缩HPACK**: + - HTTP/2 在客户端与服务器端都维护了一张首部字段索引列表, header 字段列表是以key - value 键值对元素构成的有序集合,每个header 字段元素都映射为一个索引值,报文中使用header 字段的索引值进行二进制编码传输 + - 为了进一步降低header 字段的传输开销,这些 header 字段表可以在编码或解码新 header 字段时进行增量更新,新的header 字段采用Huffman 编码(摩斯电码就采用了霍夫曼编码)可以进一步降低编码后的字节数。 +- **服务器推送**: + - HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。 + - 服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。 + + +**HTTP/2在HTTP1.1基础上做了哪些优化?** + +| HTTP/1.1 性能瓶颈 | HTTP/2 改进优化 | +|-------------------|-----------------------------| +| 存在**队头阻塞**问题,降低了TCP连接利用率 | 通过多数据流并发复用TCP连接,不仅解决了队头阻塞问题,还大大提高了TCP连接的利用率 | +| 重复传输**臃肿的首部字段**,降低了网络资源利用率 | 通过**首部压缩**,大大减少了需要传输的首部字段字节数,进一步提高了网络资源利用率 | +| 报文各字段长度不固定,增加了报文解析难度,只能串行解析 | 整个报文都采用二进制编码,且每个字段长度固定,可以**并行处理**,提高了报文处理效率 | +| 只能客户端发起请求,服务器响应请求,服务器端的数据更新不能及时反馈给客户端 | **支持服务器端向客户端推送资源**,服务器端的数据更新可以及时反馈给客户端,也可以通过**预判客户端需求提前向客户端推送相应资源**,提高客户端的访问响应效率 | + + +**Binary frame layer** + + +HTTP/2 使用帧来封装各字段信息,帧中包含表示整帧长度的字段,每个字段也有固定的长度,处理帧协议的程序就能预先知道会收到哪些字段信息,每个字段占用多少内存空间,也就可以并行处理数据帧。 + +HTTP/2 可以并行处理多个数据帧,再借助Stream Identifier 字段标识每个请求/响应数据流,可以让不同数据流的数据帧交错的在TCP连接上传输(借助Stream ID,即便交错传输也可以重新组装),这就实现了**多个数据流并发复用同一个TCP连接的效果**。 + +HTTP/2 中定义了 10种不同的帧类型,每种帧类型都有不同的用途,其中 HEADERS 和 DATA 帧构成了 HTTP 请求和响应的基础,这十种帧类型及功能描述如下: + +| 帧类型名称 | ID | 描述 | +|---------------|-----|----------------------| +| DATA | 0x0 | 传输流的核心内容 | +| HEADERS | 0x1 | 包含HTTP 首部,和可选的优先级参数 | +| PRIORITY | 0x2 | 指示或者更改流的优先级和依赖 | +| RST_STREAM | 0x3 | 允许一端停止流(通常由于错误导致的) | +| SETTINGS | 0x4 | 协商连接级参数 | +| PUSH_PROMISE | 0x5 | 提示客户端,服务器要推送些东西 | +| PING | 0x6 | 测试连接可用性和往返时延(RTT) | +| GOAWAY | 0x7 | 告诉另一端,当前端已结束 | +| WINDOW_UPDATE | 0x8 | 协商一端将要接收多少字节(用于流量控制)| +| CONTINUATION | 0x9 | 用以扩展HEADER 数据块 | + +**HTTP/2 所有性能增强的核心在于新的二进制分帧层**,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。 + +### 既然有HTTP协议,为什么还要有RPC + +[摘录自小白debug](https://mp.weixin.qq.com/s?__biz=MjM5NTY1MjY0MQ==&mid=2650859827&idx=3&sn=3baa2cdddab891cf90907dabb1985b77&chksm=bd017c7d8a76f56b7cdeed529c44363b7f65de1ad49785fb52aa1c746c0314e4d78282464e2a&scene=27) + + +**HTTP** + +HTTP协议(Hyper Text Transfer Protocol),又叫做超文本传输协议。 + + +**RPC** + +RPC(Remote Procedure Call),又叫做远程过程调用。它本身并不是一个具体的协议,而是一种调用方式。 + +- 想像调用本地方法一样调用远程的方法,屏蔽掉一些网络细节,基于这个思路,大佬们造出了非常多款式的RPC协议,比如比较有名的gRPC,thrift。 +- 虽然大部分RPC协议底层使用TCP,但实际上它们不一定非得使用TCP,改用UDP或者HTTP,其实也可以做到类似的功能。gRPC就是用的HTTP2协议, + + + +**TCP是70年代出来**的协议,而**HTTP是90年代**才开始流行的。而直接使用裸TCP会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有**80年代出来的RPC**。问题应该反过来 + +**那既然有RPC了,为什么还要有HTTP呢?** + + +**1. HTTP和RPC有什么区别** + +- **服务发现**: 想要访问某个服务,就去这些中间服务去获得IP和端口信息。 两者区别不大 + - HTTP:就是寻找IP和端口的过程,用到DNS服务。 + - RPC:一般会有专门的中间服务去保存服务名和IP信息,比如consul或者etcd,甚至是redis。 + - 由于dns也是服务发现的一种,所以也有基于dns去做服务发现的组件,比如CoreDNS。 +- **底层连接形式**: + - HTTP:以主流的HTTP1.1协议为例,其默认在**建立底层TCP连接**之后会一直保持这个连接(keep alive),之后的请求和响应都会**复用**这条连接。对于同一个域名,大多数浏览器允许同时建立6个持久连接。 + - RPC:也是通过**建立TCP长链接**进行数据交互,但不同的地方在于,RPC协议一般还会再建个连接池,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。 + - 由于连接池有利于提升网络请求性能,所以不少编程语言的**网络库里**都会**给HTTP加个连接池**,**比如go**就是这么干的。所以也不算大区别。 +- **传输的内容** + - 也就是消息头header和消息体body,字符串、数字直接就能转成0101,而结构体也有类似json,protobuf这些方案去实现(序列化、反序列化)。 + - HTTP1.1: 主要是传字符串,header和body都是如此。在body这块,它使用json来序列化结构体数据。 + - header内容冗余,约定好头部的第几位是content-type,就不需要每次都真的把"content-type"这个字段都传过来。 + - body的json也比较冗余 + - HTTP2在前者的基础上做了很多改进,所以**性能可能比很多RPC协议还要好**,甚至连**gRPC底层都直接用的HTTP2**。 + - RPC:因为它定制化程度更高,可以**采用体积更小的protobuf或其他序列化协议**去保存结构体数据,同时也不需要像HTTP那样考虑各种浏览器行为,比如302重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃HTTP,选择使用RPC的最主要原因。 + + +**为什么既然有了HTTP2,还要有RPC协议?** + +HTTP2是2015年才出来,很多公司内部RPC协议跑了很多年了,基于历史原因,一般也没必要去换了。 + +简单来说成熟的rpc库相对http容器,更多的是封装了“服务发现”,"负载均衡",“熔断降级”一类面向服务的高级特性。 + +可以这么理解,rpc框架是面向服务的更高级的封装。 + +如果把一个http servlet容器上封装一层服务发现和函数代理调用,那它就已经可以做一个rpc框架了。 + +**所以为什么要用rpc调用?** +因为良好的rpc调用是面向服务的封装,针对服务的可用性和效率等都做了优化。单纯使用http调用则缺少了这些特性。 + + +参考链接:https://www.zhihu.com/question/41609070/answer/191965937 + + +### HTTPS + +#### TSL/SSL & 四次挥手 + + + diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\343\200\212\350\256\276\350\256\241\346\250\241\345\274\217\344\271\213\347\276\216\343\200\213-\350\257\276\347\250\213\347\254\224\350\256\260.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\343\200\212\350\256\276\350\256\241\346\250\241\345\274\217\344\271\213\347\276\216\343\200\213-\350\257\276\347\250\213\347\254\224\350\256\260.md" new file mode 100644 index 0000000..db1855b --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\343\200\212\350\256\276\350\256\241\346\250\241\345\274\217\344\271\213\347\276\216\343\200\213-\350\257\276\347\250\213\347\254\224\350\256\260.md" @@ -0,0 +1,1018 @@ +# 《设计模式之美》 + +## 为什么学习设计模式 + +1. 应对面试中的设计模式相关问题 +2. 告别写被人吐槽的烂代码 + 1. 我见过太多的烂代码,比如命名不规范、类设计不合理、分层不清晰、没有模块化概念、代码结构混乱、高度耦合等等。这样的代码维护起来非常费劲,添加或者修改一个功能,常常会牵一发而动全身,让你无从下手,恨不得将全部的代码删掉重写! + 2. 每当我看到这样的好代码,都会立刻对作者产生无比的好感和认可。且不管这个人处在公司的何种级别,从代码就能看出,他是一个基础扎实的高潜员工,值得培养,前途无量!因此,代码写得好,能让你在团队中脱颖而出 +3. 提高复杂代码的设计和开发能力 + 1. 只是完成功能、代码能用,可能并不复杂,但是要想写出易扩展、易用、易维护的代码,并不容易。 + 2. 如何分层、分模块?应该怎么划分类?每个类应该具有哪些属性、方法?怎么设计类之间的交互?该用继承还是组合?该使用接口还是抽象类?怎样做到解耦、高内聚低耦合?该用单例模式还是静态方法?用工厂模式创建对象还是直接 new 出来?如何避免引入设计模式提高扩展性的同时带来的降低可读性问题? +4. 让读源码、学框架事半功倍 + 1. 优秀的开源项目、框架、中间件,代码量、类的个数都会比较多,类结构、类之间的关系极其复杂,常常调用来调用去。所以,为了保证代码的扩展性、灵活性、可维护性等,代码中会使用到很多设计模式、设计原则或者设计思想。如果你不懂这些设计模式、原则、思想,在看代码的时候,你可能就会琢磨不透作者的设计思路 + 2. 还有一个隐藏的问题,你可能自己都发现不了,那就是你自己觉得看懂了,实际上,里面的精髓你并没有 get 到多少!因为优秀的开源项目、框架、中间件,就像一个集各种高精尖技术在一起的战斗机。 +5. 为你的职场发展做铺垫 + +## 好代码 + +### 评判好坏标准 + +**评价词**:灵活性(flexibility)、可扩展性(extensibility)、可维护性(maintainability)、可读性(readability)、可理解性(understandability)、易修改性(changeability)、可复用(reusability)、可测试性(testability)、模块化(modularity)、高内聚低耦合(high cohesion loose coupling)、高效(high effciency)、高性能(high performance)、安全性(security)、兼容性(compatibility)、易用性(usability)、整洁(clean)、清晰(clarity)、简单(simple)、直接(straightforward)、少即是多(less code is more)、文档详尽(well-documented)、分层清晰(well-layered)、正确性(correctness、bug free)、健壮性(robustness)、鲁棒性(robustness)、可用性(reliability)、可伸缩性(scalability)、稳定性(stability)、优雅(elegant)、好(good)、坏(bad)…… + +**常用评价标准**: +1. **可维护性(maintainability)** + 1. 如果 bug 容易修复,修改、添加功能能够轻松完成,那我们就可以主观地认为代码对我们来说易维护。 + 2. 相反,如果修改一个 bug,修改、添加一个功能,需要花费很长的时间,那我们就可以主观地认为代码对我们来说不易维护。 + >代码的可维护性是由很多因素协同作用的结果。代码的可读性好、简洁、可扩展性好,就会使得代码易维护;相反,就会使得代码不易维护。更细化地讲,如果代码分层清晰、模块化好、高内聚低耦合、遵从基于接口而非实现编程的设计原则等等,那就可能意味着代码易维护。除此之外,代码的易维护性还跟项目代码量的多少、业务的复杂程度、利用到的技术的复杂程度、文档是否全面、团队成员的开发水平等诸多因素有关。 + +2. **可读性(readability)** + 1. 是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合 + 2. code review时同事看得轻松就是可读性好。 +3. **可扩展性(extensibility)** + 1. 方便加新功能。 +4. 灵活性(flexibility) + 1. 当我们添加一个新的功能代码的时候,原有的代码已经预留好了扩展点,我们不需要修改原有的代码,只要在扩展点上添加新的代码即可。这个时候,我们除了可以说代码易扩展,还可以说代码写得好灵活。 + 2. 当我们要实现一个功能的时候,发现原有代码中,已经抽象出了很多底层可以复用的模块、类等代码,我们可以拿来直接使用。这个时候,我们除了可以说代码易复用之外,还可以说代码写得好灵活。 + 3. 当我们使用某组接口的时候,如果这组接口可以应对各种使用场景,满足各种不同的需求,我们除了可以说接口易用之外,还可以说这个接口设计得好灵活或者代码写得好灵活。 +5. 简洁性(simplicity) + 1. 符合 KISS 原则 + 2. 思从深而行从简,真正的高手能云淡风轻地用最简单的方法解决最复杂的问题。这也是一个编程老手跟编程新手的本质区别之一。 +6. 可复用性 + 1. 代码可复用性跟 DRY(Don’t Repeat Yourself)这条设计原则的关系挺紧密的 + 2. 当讲到面向对象特性的时候,我们会讲到继承、多态存在的目的之一,就是为了提高代码的可复用性;当讲到设计原则的时候,我们会讲到单一职责原则也跟代码的可复用性相关;当讲到重构技巧的时候,我们会讲到解耦、高内聚、模块化等都能提高代码的可复用性。 +7. 可测试性(testability) + +### 面向对象、设计原则、设计模式、编程规范、重构 + +- [ ] 很多人用面向对象语言,写面向过程的代码 + + +# 面向对象 + +### 面向对象 VS 面向过程 +问题: +- 什么是面向过程编程与面向过程编程语言? +- 面向对象编程相比面向过程编程有哪些优势? +- 为什么说面向对象编程语言比面向过程编程语言更高级? +- 有哪些看似是面向对象实际是面向过程风格的代码? +- 在面向对象编程中,为什么容易写出面向过程风格的代码? +- 面向过程编程和面向过程编程语言就真的无用武之地了吗? + +**概念**: +- 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。 +- 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。 +- 面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以**数据**(可以理解为成员变量、属性)**与方法相分离为最主要的特点**。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。 + +**区别**: +- 代码的组织方式不同。 + - 面向过程风格的代码被组织成了一组方法集合及其数据结构(struct User),方法和数据结构的定义是分开的。 + - 面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中 + +**OOP的优势**: +1. OOP 更加能够应对大规模复杂程序的开发 + 1. 在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是采用曲线救国的策略,先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。 + 2. 这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。 + 3. 利用面向过程的编程语言照样可以写出面向对象风格的代码,只不过可能会比用面向对象编程语言来写面向对象风格的代码,付出的代价要高一些。 +2. OOP 风格的代码更易复用、易扩展、易维护 + 1. 而面向对象编程提供的封装、抽象、继承、多态这些特性,能极大地满足复杂的编程需求,能方便我们写出更易复用、易扩展、易维护的代码。 +3. OOP 语言更加人性化、更加高级、更加智能 + 1. **编程语言越来越人性化**,让人跟机器打交道越来越容易。笼统点讲,就是编程语言越来越高级。 + 2. 在进行面向对象编程时候,我们是在思考,如何给业务建模,如何将真实的世界映射为类或者对象,这**让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道**。可以这么说,越高级的编程语言离机器越“远”,离我们人类越“近”,越“智能”。 + 3. 如果一种新的突破性的编程语言出现,那它肯定是**更加“智能”**的。大胆想象一下,使用这种编程语言,我们可以无需对计算机知识有任何了解,无需像现在这样一行一行地敲很多代码,**只需要把需求文档写清楚,就能自动生成我们想要的软件了**。 + >但其实,进行面向对象编程的时候,很容易不由自主地就写出面向过程风格的代码,或者说感觉面向过程风格的代码更容易写。这是为什么呢? + 你可以联想一下,在生活中,你去完成一个任务,你一般都会思考,应该先做什么、后做什么,如何一步一步地顺序执行一系列操作,最后完成整个任务。**面向过程编程风格恰恰符合人的这种流程化思维方式**。 + - 而面向对象编程风格正好相反。它是一种自底向上的思考方式。它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。 + - 这样的思考路径比较适合复杂程序的开发,但并不是特别符合人类的思考习惯。 + + **让人迷惑,到底是面向对象更符合人的思维习惯,还是面向过程更符合?** + +### 封装、抽象、继承、多态 + +#### 封装 +封装主要讲的是如何隐藏信息、保护数据 + + +举个钱包的例子: + +- id/createTime这种属性应该在初始化时就设定,不应该再被改动,所以不应该暴露修改这些属性的set方法 +- 余额也不应该用set,根据业务它只有增减,所以设定incr、decr方法。 当然你也可以用set,但是你就需要在外面先加减,然后用set设置最新值。 +- 余额修改时间是跟修改余额操作一起的,没有其他需要修改它的地方,所以不需要暴露set方法到外面,设置为private,让incr这些操作里面调用,或者直接在里面赋值,不额外增加方法。 + + +对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持————访问权限控制。例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法。private 关键字修饰的属性只能类本身访问,可以保护其不被类之外的代码直接访问。 + + +**封装的意义** +- 过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改。 + >比如某个同事在不了解业务逻辑的情况下,在某段代码中“偷偷地”重设了 wallet 中的 balanceLastModifiedTime 属性,这就会导致 balance 和 balanceLastModifiedTime 两个数据不一致。 +- 修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。 +- 类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。 + - 如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。 + - 相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。 + - 这就好比,如果一个冰箱有很多按钮 + + +#### 抽象 +抽象主要讲的是:如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。 + + +很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等 + + +- 我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。举个简单例子,比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl(),那即便内部存储方式修改了,我们也不需要修改命名。 + + +#### 继承 +继承最大的一个好处就是**代码复用**。 + + +继承的概念很好理解,也很容易使用。 +- 不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。 + - 为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。 + - 还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。 + + +# 多态(Polymorphism) + +多态是指,子类可以替换父类。 +- 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。 多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与 “可能改变的事物”分离开来。 +- 有点类似策略模式,根据类型,调用同一个方法,产生的效果不一样。 都是 叫的方法,猫和狗调用animal.shout()是不一样的 + + +### 哪些代码设计看似是面向对象,实际是面向过程 +1. 滥用getter、setter方法 + 1. 它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格 + 2. 在设计实现类的时候,除非真的需要,否则,尽量不要给属性定义 setter 方法。除此之外,尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器(比如例子中的 List 容器),也要防范集合内部数据被修改的危险。 +2. 滥用全局变量和全局方法 + 1. 问题 + 1. 常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。单例类对象在全局代码中只有一份,所以,它相当于一个全局变量。 + 2. 静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。 + 3. 而常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。 + 4. 静态方法一般用来操作静态变量或者外部数据。你可以联想一下我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。 + 5. 静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。 + 2. 常量类,如果只在自己类用到,可以定义到类里面。 如果其他可能用,也要做下区分MysqlConstants、RedisConfig,不要放在一个大的CommonConstants ,因为会有几个问题: + 1. 这样的设计会影响代码的可维护性。 + 2. 这样的设计还会增加代码的编译时间。 + 3. 这样的设计还会影响代码的复用性。 + 3. Utils类 + 1. **解决什么问题**:从业务含义上,A 类和 B 类并不一定具有继承关系,比如 Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能,但并不具有继承关系(既不是父子关系,也不是兄弟关系)。仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。如果不熟悉背后设计思路的同事,发现 Crawler 类和 PageAnalyzer 类继承同一个父类,而父类中定义的却是 URL 相关的操作,会觉得这个代码写得莫名其妙,理解不了。 + 2. 然后呢? 只包含静态方法不包含任何属性的 Utils 类,是**彻彻底底的面向过程的编程风格**。但这并不是说,我们就要杜绝使用 Utils 类了。实际上,从刚刚讲的 Utils 类存在的目的来看,它在软件开发中还是挺有用的,能解决代码复用问题。所以,这里并不是说完全不能用 Utils 类,而是说,要尽量避免滥用,不要不加思考地随意去定义 Utils 类。 + 3. 思考: + 1. 你真的需要单独定义这样一个 Utils 类吗?是否可以把 Utils 类中的某些方法定义到其他类中呢? + 2. FileUtils、IOUtils、StringUtils、UrlUtils不同用不同 +3. **定义数据和方法分离的类** + 1. 一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。 + 2. 实际上,这种开发模式叫作**基于贫血模型**的开发模式,也是我们现在非常常用的一种 Web 项目的开发模式 + + + + +# 设计原则 + + +## SOLID原则 + +### SRP 单一职责原则 + +### OCP 开闭原则 + + +### LSP 里式替换原则 + + +### ISP 接口隔离原则 + + +### DIP 依赖倒置原则 + +## 其他一些原则 + +**DRY 原则** + + +**KISS 原则** + + +**YAGNI 原则** + + + +**LOD 法则** + + + +# 设计模式 + +## 什么是设计模式 +- 设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套**解决方案或者设计思路**。 +- 大部分设计模式要解决的都是代码的**可扩展性**问题。 +- **重点**:了解它们都能解决哪些问题,掌握典型的应用场景,并且懂得不过度应用 + - 在开发初期,除非特别必须,我们一定不要过度设计,应用复杂的设计模式。而是当代码出现问题的时候,我们再针对问题,应用原则和模式进行重构。这样就能有效避免前期的过度设计。 + + +23 种经典的设计模式。它们又可以分为三大类:创建型、结构型、行为型。 + +1. 创建型 + - 常用的有:**单例模式**、工厂模式(工厂方法和**抽象工厂**)、建造者模式。 + - 不常用的有:原型模式。 +2. 结构型 + - 常用的有:代理模式、桥接模式、装饰者模式、适配器模式。 + - 不常用的有:门面模式、组合模式、享元模式。 + +3. 行为型 + - 常用的有:观察者模式、模板模式、**策略模式**、**职责链模式**、迭代器模式、状态模式。 + - 不常用的有:访问者模式、备忘录模式、命令模式、解释器模式、中介模式。 + + +# 编程规范 + + +# 重构 + +## 理论一: 什么情况下要重构? 到底重构什么? 又该如何重构? + +### 重构的目的:为什么要重构(why)? + +**重构的定义** + + +- 软件设计大师 Martin Fowler 是这样定义重构的:“重构是一种对软件**内部结构的改善**,目的是在**不改变软件的可见行为**的情况下,使其**更易理解,修改成本更低**。” +- 重构不改变外部的可见行为。 +- 在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。 + + +**为什么要进行代码重构?** + + +1. 首先,重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。 +2. 优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。 +3. 重构是避免过度设计的有效手段。 +4. 重构对一个工程师本身技术的成长也有重要的意义。 + - 重构实际上是对我们学习的经典设计思想、设计原则、设计模式、编程规范的一种应用。 + - 平时堆砌业务逻辑,你可能总觉得没啥成长,而将一个比较烂的代码重构成一个比较好的代码,会让你很有成就感。 +>重构能力也是衡量一个工程师代码能力的有效手段。 +>所谓“初级工程师在维护代码,高级工程师在设计代码,资深工程师在重构代码” + + +### 重构的对象:到底重构什么(what)? + + +1. **大型重构** + - 定义:是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构。 + - 重构的手段:分层、模块化、解耦、抽象可复用组件等等。 + - 重构的工具:设计思想、原则和模式。 + - 特点: 涉及的代码改动会比较多,影响面 比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。 + +2. **小型重构** + - 定义: 对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。 + - 手段: 利用编码规范。 + - 工具: + - 特点: 要修改的地方比较集中,比较简单,可操作性强,耗时会比较短,引入bug的风险相对较小。 + + + +### 重构的时机:什么时候重构(when)? + +> 当代码真的烂到出现“开发效率低,招了很多人,天天 加班,出活却不多,线上 bug 频发,领导发飙,中层束手无策,工程师抱怨不断,查找 bug 困难”的时候,基本上重构也无法解决问题了。 + +寄希望于在代码烂到一定程度之后,集中重构解决所有问 题是不现实的,我们必须探索一条可持续、可演进的方式。 + + +**持续重构** + +- 平时没有 事情的时候,你可以看看项目中有哪些写得不够好的、可以优化的代码,主动去重构一下。 +- 或者,在修改、添加某个功能代码的时候,你也可以顺手把不符合编码规范、不好的设计重 构一下。 +- 就像把单元测试、Code Review 作为开发的一部分,我们如果能把持续重构也作为开发的一部分,成为一种开发习惯,对项目、对自己都会很有好处。 + + +- 技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。 +- 时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。 +- 而那些看到别人代码有点瑕疵就一顿乱骂,或者花尽心思去构思一个完美设计的人,往往都是因为没有树立正确的代码质量观,没有持续重构意识。 + + +### 重构的方法:又该如何重构(how)? + + +**对于大重构** + +- 涉及模块多,代码多,耦合严重牵一发动全身。 本来一天能完成的重构,你会发现越改越多,越改越乱,如果新的业务有冲突,就可能半途而废,revert掉所有改动,失落地继续去堆砌烂代码。 +- 解决办法: 大型重构 + - 提前做好完善的重构计划,分阶段进行。 + - 每个阶段完成一小部分重构,然后提交、测试、运行,没问题再继续下一段。 + - 保证代码仓库中的代码一直处于可运行、逻辑正确的状态。 + - 控制重构影响到的代码范围,考虑好兼容老的代码逻辑,必要时还需要写一些兼容过渡代码。 + - 只有这样,才能让每个阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新功能开发冲突。 + +> 大规模高层次的重构一定是有组织、有计划,并且非常谨慎的,需要有经验、熟悉业务的资深同事来主导。 + + +**你写的每一行代码,都是你的名片。** + +一般是增加需求时,对关联的逻辑代码做的重构。这时需要考 虑自己当前的开发期限去决定重构的力度。 + +在保证“**营地比自己来时干净**”的前提下,量时重构。 + + +平时使用 source tree ,git r ebase 可以清晰地看到每一次提交,这样代码 review 起来就没什么压力了。 + +重构一定要在有比较完善的测试用例覆盖和回归用例库的情况下进行(可测试性),否则会相当危险。 + + +## 理论二:为了保证重构不出错,有哪些非常能落地的技术手段 + + +- 最可落地执行、最有效的保证重构 不出错的手段应该就是单元测试(Unit Testing)了。 + + +1. 集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录 功能是否正常,是一种端到端(end to end)的测试。 +2. 而单元测试的测试对象是类或者函 数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。 + + +写单元测试本身不需要什么高深技术。它更多的是考验程序员思维的缜密程度,看能否设计出覆盖各种正常及异常情况的测试用例,来保证代码在任何预期或非预期的情况下都能正确运行。 + + +### 为什么要写单元测试? + +1. 单元测试能有效地帮你发现代码中的 bug。 +>坚持为自己提交的每一份代码,都编写完善的单元测试。得益 于此,我写的代码几乎是 bug free 的。这也节省了我很多 fix 低级 bug 的时间,能够有时 间去做其他更有意义的事情,我也因此在工作上赢得了很多人的认可。 + +2. 写单元测试能帮你发现代码设计上的问题. +> 对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很吃力,需要依靠单元测试框架里很高级的特性才能完成,那往往就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函数、全局变量、代码高度耦合等。 + +3. 单元测试是对集成测试的有力补充。 +>程序运行的 bug 往往出现在一些边界条件、异常情况下,比如,除数未判空、网络超时。 而大部分异常情况都比较难在测试环境中模拟。 单测可以mock + +4. 写单元测试的过程本身就是代码重构的过程。 +> 编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,我们可以 发现一些设计上的问题(比如代码设计的不可测试)以及代码编写方面的问题(比如一些边 界条件处理不当)等,然后针对性的进行重构。 + +5. 阅读单元测试能帮助你快速熟悉代码。 +> 阅读代码最有效的手段,就是**先了解它的业务背景和设计思路,然后再去看代码**,这样代码读起来就会轻松很多。 + +6. 单元测试是 TDD 可落地执行的改进方案。 +>单元测试正好是对 TDD 的一种改进方案,先写代码,紧接着写单元测试,最 后根据单元测试反馈出来问题,再回过头去重构代码。 + + +### 如何编写单元测试? + +写单元测试就是针对代码设计覆盖各种输入、异常、边 界条件的测试用例,并将这些测试用例翻译成代码的过程。 + + +在把测试用例翻译成代码的时候,我们可以利用单元测试框架,来简化测试代码的编写。比 如,Java 中比较出名的单元测试框架有 Junit、TestNG、Spring Test 等。 + + +### 单元测试经验 + +1. 写单元测试真的是件很耗时的事情吗? + +- 尽管单元测试的代码量可能是被测代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗时。 +- 毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。 +- 不同测试用例之间的代码差别可能并不是很大,简单 copy-paste 改改就行。 + + +2. 对单元测试的代码质量有什么要求吗? +>单元测试代码的质量可以放低一些要求。命名稍微有些不规范,代码稍微有些重复,也都是没有问题的。 + +3. 单元测试只要覆盖率高就够了吗? +>更重要的是要看测试用例是否覆盖了所有可能的情况,特别是一些 corner case。 +>从过往的经验上来讲,一个项目的 单元测试覆盖率在 60~70% 即可上线。 + +4. 写单元测试需要了解代码的实现逻辑吗? +>单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。 +>我们切不可为了追求覆盖率,逐行阅读代码,然后针对实现逻辑编写单元测试。 不然在重构时,如果没改变外部行为,但是内部实现逻辑改了。单测失败,那就起不到为重构保驾护航的作用了。 + +5. 如何选择单元测试框架? +>团队内部统一框架。 不要为了适用不好的代码去找高级的单元测试框架。 + + + +**单元测试为何难落地执行?** + +1. 写单元测试确实是一件考验耐心的活儿。 + 1. 很多人往往会觉得写单元测试比较繁琐,并且没有太多挑战,而 不愿意去做。 + 2. 也许刚开始能坚持,但当开发任务紧了之后,就开始放低对单元测试的要求,一旦出现  破窗效应,慢慢的,大家就都不写了。 +2. 由于历史遗留问题,原来的代码都没有写单元测试,代码已经堆砌了十 几万行了,不可能再一个一个去补单元测试。 + + +## 理论三:什么是代码的可测试性?如何写出可测试性好的代码? + +### 什么是代码的可测试性? 如何写出可测试性好的代码? + +所谓代码的可测试性,就是针对代码编写单元测试的难易程度。 + + +单元测试写起来很费劲,需要依靠单元测试框架中很高级的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好。 + + + +**实战:** + + +**依赖注入是编写可测试性代码的最有效手段。** + + +外部依赖要mock, 框架mock就是简化手动mock,下面是一个手动mock的例子,通过继承 WalletRpcService 类,并且重写其中的 moveMoney() 函数的方式来实现 mock。 + +```java +public class MockWalletRpcServiceOne extends WalletRpcService { + public String moveMoney(Long id, Long fromUserId, Long toUserId, Double amount) { + return "123bac"; + } +} + +public class MockWalletRpcServiceTwo extends WalletRpcService { + public String moveMoney(Long id, Long fromUserId, Long toUserId, Double amount) { + return "123bac"; + } +} +``` + +**问题1**: 因为 WalletRpcService 是在 execute() 函数中通过 new 的方式创建的,我们无法动态地 对其进行替换。 也就是说这个类中的这个方法可测试性很差,需要重构让其变得更容易测试。 + + +通过依赖注入实例的方式:将 WalletRpcService 对象的创建反转给上层逻辑,在外部创建好之后,再注入 到 Transaction 类中。 + +```java +public class Transaction { + // 添加一个成员变量及其 set 方法 + private WalletRpcService walletRpcService; + + public void setWalletRpcService(WalletRpcService walletRpcService) { + this.walletRpcService = walletRpcService; + } + + public boolean execute() { + // 删除下面一行代码 + //WalletRpcService walletRpcService = new WalletRpcService(); + } +} + +``` + +然后,我们就可以在单元测试中,非常容易地将 WalletRpcService 替换成 MockWalletRpcServiceOne 或 WalletRpcServiceTwo 了。 +```java +public void testExecute() { + Long buyerId = 123L; + Long sellerId = 124L; + Long productId = 456L; + Long orderId = 765L; + Transaction transaction = new Transaction(null, buyerId, sellerId, productId, orderId); + // 使用mock对象来替代真正的RPC服务 + transaction.setWalletRpcServer(new MockWalletRpcServerOne()); + boolean executeResult = transaction.execute(); + assertTrue(executeResult); + assertEquals(STATUS.EXECUTED, executeResult.getStatus()); +} +``` + +**问题2**: 因为 RedisDistributedLock 是一个单例类。单例相当于一个全局变量,我们无法mock(无法继承和重写方法),也无法通过以来注入的方式来替换。 + +> 如果 RedisDistributedLock 是我们自己维护的,可以自由修改、重构,那我们可以将其改为非单例的模式,或者定义一个接口,比如 IDistributedLock,让 RedisDistributedLock 实现这个接口。 + + +如果是我们无法修改的外部接口, 我们还可以对 transaction 上锁这部分逻辑重新封装一下。 代码如下: +```java +public class TransactionLock { + public boolean lock(String id) { + reurn RedisDistributedLock.getSingleInstance().lockTransaction(id); + } + + public void unlock() { + RedisDistributedLock.getSingleInstance().unlockTransaction(id); + } +} + +public class Transaction { + private TransactionLock lock; + + public void setTransactionLock(TransactionLock lock) { + this.lock = lock; + } + + public boolean execute() { + try { + isLocked = lock.lock(); + } finally { + lock.unlock(); + } + } +} + +// 单测就可以隔离真正的Lock分布式锁这部分逻辑了 + +public void testExecute() { + Long buyerId = 123L; + //。。。。 + + TransactionLock mockLock = new TransactionLock() { + public boolean lock(String id) { + return true; + } + + public void unlock() {} + }; + + Transaction transaction = new Transaction(null, buyerId, ...); + transaction.setTransactionLock(mockLock); + //.... +} +``` +、**问题3**: 针对是否过期,判断时间这类未决行文的逻辑,我们如果没有对应的set方法,也不推荐去修改,更多的是把这种未决行为逻辑重新封装。 比如是否过期封装一个 isExpired()函数即可。 + +```java + +public class Transaction { + protected boolean isExpired() { + long executionInvokedTimestamp = System.currentTimestamp(); + return executionInvokedTimestamp - createdTimestamp > 14days; + } + + public boolean execute() { + // ... + if (isExpired()) { + this.status = STATUS_EXPIRED; + return false; + } + // ... +} + +// 单测 +public void testExecute() { + Long buyerId = 123L; + //。。。。 + + TransactionLock mockLock = new TransactionLock(null, buyerId, sellerId, ...) { + protected boolean isExpired() { + return true; + } + }; + + boolean actualResult = transaction.execute(); + //.... +} + + + +``` + + +### 有哪些常见的不好测试的代码? + +1. 未决行为 +>所谓的未决行为逻辑就是,代码的输出是随机或者说不确定的,比如,跟时间、随机数有关的代码。 + + +2. 全局变量 + + +3. 静态方法 +>主要原因是静态方法也很难 mock。 + + +4. 复杂继承 +>相比组合关系,继承关系的代码结构更加耦合、不灵活,更加不易扩展、不易维护。 + + +5. 高耦合代码 +>如果一个类职责很重,需要依赖十几个外部对象才能完成工作,代码高度耦合,那我们在编 写单元测试的时候,可能需要 mock 这十几个依赖的对象。 不合理。 + + + +## 理论四:如何通过封装、抽象、模块化、中间层等解耦代码? + +对于大型重构来说,今天我们重点讲解最有效的一个手段,那就是“**解耦**”。 + + +解耦的目的是**实现代码高内聚、松耦合**。关于解耦,我准备分下面三个部分来给你讲解。 + + * “解耦”为何如此重要? + * 如何判定代码是否需要“解耦”? + * 如何给代码“解耦”? + + +### “解耦”为何如此重要? + + +1. 过于复杂的代码往往在可读性、可维护性上都不友好。 +2. 解耦保证代码松耦合、高内聚,是控制代码复杂度的有效手段。 +3. 代码高内聚、松耦合,也就是意味着,代码结构清晰、分层模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。 + + + +### 如何判断代码是否需要解耦? + + +- 间接的衡量标准有很多,比如,看修改代码是否牵一发而动全身。 +- 直接的衡量标准是把模块与模块、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构。 + + +### 如何给代码“解耦”? + +- 方法:封装与抽象、中间层、模块化。 +- 设计思想与原则:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则。 +- 设计模式: 观察者模式 + + +## 理论五:让你最快速地改善代码质量的20条编程规范 + + +### 命名与注释(Naming and Comments) + +#### 命名 + +大到项目名、模块名、包名、对外暴露的接口,小到类名、函数名、变量名、参数名,只要是做开发,我们就逃不过“起名字”这一关。 + + +命名的好坏,对于代码的可读性来说非常重要,甚至可以说是起决定性作用的。 + + +**肯花时间,要重视**: 对于影响范围 比较大的命名,比如包名、接口、类名,我们一定要反复斟酌、推敲。实在想不到好名字的 时候,可以去 GitHub 上用相关的关键词联想搜索一下,看看类似的代码是怎么命名的。 + + +**1. 命名多长最合适?** + +尽管长的命名可以包含更多的信息,更能准确直观地表达意图,但是,如果函数、变量的命名很长,那由它们组成的语句就会很长。在代码列长度有限制的情况下,就会经常出现**一条语句被分割成两行的情况,这其实会影响代码可读性**。 + + +- 实际上,在足够表达其含义的情况下,命名当然是越短越好。 +- 但是,大部分情况下,短的命 名都没有长的命名更能达意。 + + +- 对于一 些默认的、大家都比较熟知的词,我比较推荐用缩写。 +>sec 表示 second、str 表示 string、num 表示 number、 doc 表示 document。 +- 对于作用域比较小的变量,我们可以使用相对短的命名,比如一些函数内的临时变量。 +- 对于**类名**这种作用域比较大的,我更推荐用长的命名方式。 + +> 命名的一个原则就是以能准确达意为目标。 + + +命名要学会换位思考,假设自己不熟悉这块代码,从代码阅读者的角度去考量,命名是否足够直观。 + + +**2. 利用上下文简化命名** + +1. 类名包含的含义,不需要在属性里重复,比如User,里面的名字就用name,不需要userName,因为调用的时候也是 user.getName(),能理解是什么含义。 +2. 函数参数也可以借助函数名的上下文来简化命名。 比如 +```java +private void uploadUserAvatarImageToAliyun(String userAvatarImageUri); +// 利用上下文简化 +private void uploadUserAvatarImageToAliyun(String imageUri); + +``` + + +**3. 命名要可读、可搜索** + +- 不要生僻难读的单词。 +- 要统一风格,方便联想不全。 比如大家都用get,你就不要用query, add或insert等等。 + + +**4. 如何命名接口和抽象类?** + +对于接口和抽象类, 选择哪种命名方式都是可以的,只要项目里能够统一就行。 + + + +#### 注释 + +命名再好,毕竟有长度限制,不可能足够详尽,而这个时候,注释就是一个很好的补充。 + + +**1. 注释到底该写什么?** + +- 注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。 +- 总结一下,注释的内容主要包含这样三个方面:**做什么、为什么、怎么做。** +- 有人认为,注释是要提供一些代码没有的额外信息,所以不要写“做什么、怎么做”,这两方面在代码中都可以体现出来,只需要写清楚“为什么”,表明代码的设计意图即可。 + + +但是,个人不是很认可这样的观点,理由有三: + +1. 注释比代码承载的信息更多。 +>对于类来说,包含 的信息比较多,一个简单的命名就不够全面详尽了。这个时候,在注释中写明“做什么”就 合情合理了。 + +2. 注释起到总结性作用、文档的作用. +> 在注释中,关于具体的代码实现思路,我们可以写一些总结性的说明、特殊情况的说明。这样能够让阅读代码的人通过注释就能大概了解代码的实现思路,阅读起来就会更加容易。 + +3. 一些总结性注释能让代码结构更清晰 +>对于逻辑比较复杂的代码或者比较长的函数,如果不好提炼、不好拆分成小的函数调用,那我们可以借助总结性的注释来让代码结构更清晰、更有条理。 + + + + +**2. 注释是不是越多越好?** + +- 类和函数一定要写注释,而且要写得尽可能全面、详细 +- 而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。 + + +Codelf(变量命名神器) https://unbug.github.io/codelf/ + + +### 代码风格(Code Style) + +#### 1. 类、函数多大才合适? + +- 太长: 一个类上千行,一个函数几百行,逻辑过于繁杂,阅读代码的时候,很容易就会看了后面忘了前面。 +- 太多: 类或函数的代码行数太少,在代码总量相同的情况下,被分割成的类和函数就 会相应增多,调用关系就会变得更复杂,阅读某个代码逻辑的时候,需要频繁地在 n 多类 或者 n 多函数之间跳来跳去,阅读体验也不好。 + + + + + +### 编程技巧(Coding Tips) + + +**1. 把代码分割成更小的单元块** + +只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。 + + +**2. 避免函数参数过多** + +我个人觉得,函数包含 3、4 个参数的时候还是能接受的,大于等于 5 个的时候就有点多了。 + + +**解决办法**: + +1. 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。 +2. 将函数的参数封装成对象。 +>如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。 + + +**3. 勿用函数参数来控制逻辑** + +不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的 时候走另一块逻辑。 + +这明显违背了单一职责原则和接口隔离原则。我建议将其拆成两个函 数,可读性上也要更好。 + + +如果函数是 private 私有函数,影响范围有限,或者拆分之后的两个函数经常同时被 调用,我们可以酌情考虑保留标识参数。 + + +还有一种“根据参数是否为 null”来控 制逻辑的情况。针对这种情况,我们也应该将其拆分成多个函数。拆分之后的函数职责更明 确,不容易用错。 + + +**4. 函数设计要职责单一** + + +**5. 移除过深的嵌套层次** + +我个人建 议,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身 理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句 超过一行的长度而折成两行,影响代码的整洁。 + + +**6. 学会使用解释性变量** + +- 常量取代魔法数字 +- 使用解释性变量来解释复杂表达式。 + + +## 学习如何发现代码质量问题 + + +### 如何发现代码质量问题? + + +**代码质量-常规checklist** + +从大处着眼的话,我们可以参考之前讲过的代码质量评判标准,看这段代码是否可读、可扩展、可维护、灵活、简洁、可复用、可测试等等。落实到具体细节,我们可以从以下几个方面来审视代码。 + * 目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”? + * 是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)? + * 设计模式是否应用得当?是否有过度设计? + * 代码是否容易扩展?如果要添加新功能,是否容易实现? + * 代码是否可以复用?是否可以复用已有的项目代码或类库?是否有重复造轮子? + * 代码是否容易测试?单元测试是否全面覆盖了各种正常和异常的情况? + * 代码是否易读?是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)? + + + +**业务需求checklist** + +>关注代码实现是否满足业务本身特有的功能和非功能需求 + +- 代码是否实现了预期的业务需求? +- 逻辑是否正确?是否处理了各种异常情况? +- 日志打印是否得当?是否方便 debug 排查问题? +- 接口是否易用?是否支持幂等、事务等? +- 代码是否存在并发问题?是否线程安全? +- 性能是否有优化空间,比如,SQL、算法是否可以优化? +- 是否有安全漏洞?比如输入输出校验是否全面? + + + +# 行为型模式 + +**设计模式要干的事情就是解耦。** + +- 创建型模式是将创建和使用代码解耦, +- 结构型模式是将不同功能代码解耦, +- 行为型模式是将不同的行为代码解耦, + - 具体到观察者模式,它是将观察者和被观察者代码解耦。 + +解决 类与对象之间的交互 问题 + +## 观察者模式 + +### 解释 + +**在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。** + + +一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者 (Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不 同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、 EventEmitter-EventListener、Dispatcher-Listener。 + + +### 示例 + +```java +public interface Subject { + void registerObserver(Observer observer); + void removeObserver(Observer observer); + void notifyObservers(Observer observer); +} + +public interface Observer { + void update(Message message); +} + +public class ConcreteSubject implements Subject { + private List observers = new ArrayList<>(); + + @Overide + public void registerObserver(Observer observer) { + observers.add(observer); + } +} +``` + +--- +## 迭代器模式 + +### 相比直接遍历集合数据,使用迭代器有哪些优势? + +**迭代器模式的原理和实现** + +迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。 + + +- 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。 +- 一个完整的迭代器模式一般会涉及**容器和容器迭代器**两部分内容。 + + +**Iterator 接口定义** + +```java +// 方式1 +public interface Iterator { + boolean hasNext(); + // next() 函数用来将游标后移一位元素 + void next(); + // currentItem() 函数用来返回当前 游标指向的元素 + E currentItem(); +} + +// 方式2 +public interface Iterator { + boolean hasNext(); + // 返回当前元素与后移一位这两个操作,要放到同一个函 数 next() 中完成 + E next(); +} +``` + +- 方式1 定义更加灵活一些,比如我们可以多次调用 currentItem() 查询当前元素,而不移动游标。 + +实现: + +```java +public class ArrayListIterator implements Iterator { + private int cursor; + private ArrayList arrayList; + + public ArrayListIterator(ArrayList arrayList) { + this.cursor = 0; + this.arrayList = arrayList; + } + + @Override + public boolean hasNext() { + return cursor < arrayList.size(); + } + + @Override + public void next() { + this.cursor++; + } + + @Override + public currentItem() { + if (this.cursor >= this.arrayList.size()) { + throw new NoSuchElementException(); + } + return arrayList.get(this.cursor); + } +} + +//使用 +Iterator iterator = new ArrayIterator(names); +while (iterator.hasNext()) { + System.out.println(iterator.currentItem()); + iterator.next(); +} + +// 上面的使用需要将待遍历的容器对象,通过构造函数传递给迭代器类。 +// 实际上,为了封装迭代器的创建细节,我们可以在容器中定义一个 iterator() 方法,来创建对应 的迭代器。 +// 为了能实现基于接口而非实现编程,我们还需要将这个方法定义在 List 接口 中。 + +public interface List { + Iterator iterator(); +} + +public class ArrayList implements List { + @Override + public Iterator iterator() { + return new ArrayListIterator(this); + } +} + +// 使用 +Iterator iterator = names.iterator(); +while (iterator.hasNext()) { + System.out.println(iterator.currentItem()); + iterator.next(); +} +``` + + +for 循环遍历方式比起迭代器遍历方式,代码看起来更加简洁。那我们 **为什么还要用迭代器来遍历容器呢?** + + +1. 首先,迭代器模式**封装集合内部**的**复杂**数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可; + - 比 如,针对图的遍历,我们就可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分 别来实现深度优先遍历和广度优先遍历。 +2. 其次,迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一; 将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。 +3. 最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,**基于接口而非具体的实现编程**。 + - 当需要切换新的遍历算法的时候,比如,从前往后遍历链表切换成从后往前遍历链 表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改,更容易。 + - 添加新的遍历算法,我们只需要扩展新的迭代器类,也更**符合开闭原则**。 + + + +**在 Java 中,如果在使用迭代器的同时删除容器中的元素,会导致迭代器报错,这是为什 么呢?如何来解决这个问题呢?** + +- 使用 for-each 或者 iterator 进行迭代删除 remove 时,容易导致 next() 检 测的 modCount 不等于 expectedModCount 从而引发 ConcurrentModificationException。 + - 在单线程下,推荐使用 next() 得到元素,然后直接调用 remove(),注意是无参的 remove; + - 多线程情况下还是使用并发容器吧 +- 因为在迭代器中保存的游标和集合有一致性关系(大小,元素位置)。迭代器外部删除集合元素将导致其保存的游标位置与集合当前状态不一致。 + - 解决方法是由迭代器本身提供删 除方法,这样可以感知到删除操作以便调整本身保存的游标。 + - java的迭代器中,容器size是保存在迭代器的变量里面的,如果remove则会导致size变 化,所以fail-fast了 + + +### 遍历集合的同时,为什么不能增删集合元素? + + +**在遍历的同时增删集合元素会发生什么?** + + +在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素 被重复遍历或遍历不到。 + +不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为**结果不可预期行为或者未决行为**。 + + +**如何应对遍历时改变集合导致的未决行为?** + + +两种比较干脆利索的解决方案:一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。 + +- 第一种解决方案比较难实现,我们要确定遍历开始和结束的时间点。遍历开始的时间节点我们很容易获得,但结束时间很难确定。 +- 第二种解决方法更加合理。Java 语言就是采用的这种解决方案,增删元素之后, 让遍历报错。 + + +**怎么确定在遍历时候,集合有没有增删元素呢?** + - 我们在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。 + - 当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们**把 modCount 值传递给迭代器的 expectedModCount 成员变量**,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查**集合上的 modCount 是否等于 expectedModCount**,也就是看,在创建完迭代器之后,modCount 是否改变过。 + + +### 如何设计实现一个支持“快照”功能的 iterator? + + +**方法一** + +- 在迭代器类中定义一个成员变量 snapshot 来存储快 照。 +- 每当创建迭代器的时候,都拷贝一份容器中的元素到快照中,后续的遍历操作都基于这 个迭代器自己持有的快照来进行。 +- **缺点**:浪费空间,每多一个迭代器就要多存储一份容器。 + + +**方法二** + +1. 在容器中,为每个元素保存两个时间戳,一个是添加时间戳 addTimestamp,一个是删除时间戳 delTimestamp。 +2. 当元素被加入到集合中的时候,我们将 addTimestamp 设置为当前时间,将 delTimestamp 设置成最大长整型值(Long.MAX_VALUE)。 +3. 当元素被删除时,我们将 delTimestamp 更新为当前时间,表示已经被删除。 + +- 每个迭代器也保存一个迭代器创建时间戳 snapshotTimestamp,也就是迭代器对应 的快照的创建时间戳。当使用迭代器来遍历容器的时候,只有满足 addTimestamp < snapshotTimestamp < delTimestamp 的元素,才是属于这个迭代器的快照。 +- 只迭代属于本次迭代器的元素,不符合时间判断的跳过。 +- **缺点**:又引入了另外一个问题:ArrayList 底层 依赖数组这种数据结构,原本可以支持快速的随机访问,在 O(1) 时间复杂度内获取下标为 i 的元素,但现在,删除数据并非真正的删除,只是通过时间戳来标记删除,这就导致无法支持按照下标快速随机访问了。 + + +**怎么让容器既支持快照遍历,又支持随机访问?** + +> 可以在 ArrayList 中存储两个数组。一个支持标 记删除的,用来实现快照遍历功能;一个不支持标记删除的(也就是将要删除的数据直接从数组中移除),用来支持随机访问。 + + +把专栏里的**每个开篇问题都当做面试题,自己去思考一下,然后再看解答**。 +这样整个专栏学下来,对能力的锻炼就多了,再遇到算法面试也就不会一点思路都没有了。 diff --git "a/\344\273\243\347\240\201\350\247\204\350\214\203\345\222\214\347\273\217\351\252\214.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\344\273\243\347\240\201\350\247\204\350\214\203\345\222\214\347\273\217\351\252\214.md" similarity index 99% rename from "\344\273\243\347\240\201\350\247\204\350\214\203\345\222\214\347\273\217\351\252\214.md" rename to "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\344\273\243\347\240\201\350\247\204\350\214\203\345\222\214\347\273\217\351\252\214.md" index a49a83b..9b74949 100644 --- "a/\344\273\243\347\240\201\350\247\204\350\214\203\345\222\214\347\273\217\351\252\214.md" +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\344\273\243\347\240\201\350\247\204\350\214\203\345\222\214\347\273\217\351\252\214.md" @@ -220,7 +220,7 @@ >说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次 序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。 - 【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains 方法进行遍历、对比、去重操作. -### **1.1.6 并发处理** +### 1.1.6 并发处理 【强制】 1. 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。 >说明:资源驱动类、工具类、单例工厂类都需要注意. diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\350\256\276\350\256\241\346\250\241\345\274\217\346\250\241\346\235\277.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\350\256\276\350\256\241\346\250\241\345\274\217\346\250\241\346\235\277.md" new file mode 100644 index 0000000..2ab6c82 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\350\256\276\350\256\241\346\250\241\345\274\217&\347\274\226\347\240\201\350\247\204\350\214\203/\350\256\276\350\256\241\346\250\241\345\274\217\346\250\241\346\235\277.md" @@ -0,0 +1,124 @@ + +# 单例模式 + +## 双重否定 + +```java + +public class Singleton { + private static volatile Singleton instance = null; + + private Singleton() {}; + + public static getSingleInstance() { + if(instance == null) { + synchronized(Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} + +``` + +## 内部静态类 + +利用内部类持有静态对象的方式实现。 + +**理论**: 对象初始化过程中隐含的初始化锁 + + +```java +public class Singleton { + private Singleton(){} + public static Singleton getSingleton(){ + return Holder.singleton; + } + private static class Holder { + private static Singleton singleton = new Singleton(); + } + +} +``` + +## 实际一些简单的实现 +Java 核心类库自己的 单例实现,比如java.lang.Runtime: + +```java +private static final Runtime currentRuntime = new Runtime(); +private static Version version; + // ... +public static Runtime getRuntime() { + return currentRuntime; +} + /** Don't let anyone else instantiate this class */ 8 +``` + +静态实例被声明为 final,这是被通常实践忽略的,一定程度保证了实例不被篡改,也有有限的保证执行顺序的语义。 + + +# 责任链模式 + +```java +//定义 +public abstract class RouterHandler { + protected RouterHandler next; + + public void setNext(RouterHandler next) { + this.next = next; + } + + public abstract RouterResponse doHandler(RouterContext context); + + public static class Builder { + private RouterHandler head; + private RouterHandler tail; + + public Builder addChain(RouterHandler chain) { + if(this.head == null) { + this.head = this.tail = chain; + return this; + } + this.tail.setNext(chain); + this.tail = chain; + return this; + } + + public RouterHandler build() { + return this.head; + } + } +} + +// 实现类 +public class DirectQueryDBRouterHandler extends RouterHandler { + + private static final Logger logger = LoggerFactory.getLogger(DirectQueryDBRouterHandler.class); + + @Override + public RouteResponse doHandler(ClusterRouterContext context) { + boolean bool = context.getBool(); + if (bool) { + return this.next.doHandler(context); + } + + RouteResponse response = new RouteResponse(); + ... + return response; + } +} + + +// 初始化 +RouterHandler routerHandler = new RouterHandler.Builder() + .addChain(new DirectQueryDBRouterHandler()) + .addChain(new SpecifyClusterRouterHandler()) + .build(); + +// 调用 +RouteResponse response = routerHandler.doHandler(context); + +``` \ No newline at end of file diff --git "a/\345\276\256\346\234\215\345\212\241&SOA/zookeeper\345\205\245\351\227\250\345\255\246\344\271\240.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237/zookeeper\345\205\245\351\227\250\345\255\246\344\271\240.md" similarity index 97% rename from "\345\276\256\346\234\215\345\212\241&SOA/zookeeper\345\205\245\351\227\250\345\255\246\344\271\240.md" rename to "\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237/zookeeper\345\205\245\351\227\250\345\255\246\344\271\240.md" index 1cd9713..93d12b5 100644 --- "a/\345\276\256\346\234\215\345\212\241&SOA/zookeeper\345\205\245\351\227\250\345\255\246\344\271\240.md" +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237/zookeeper\345\205\245\351\227\250\345\255\246\344\271\240.md" @@ -73,7 +73,9 @@ ZooKeeper在实现这些服务时, * 当master挂掉后,其他集群节点收到节点删除事件,进行重新选举 * 重复步骤二到四 ->有人说,zookeeper可以做分布式配置中心、分布式消息队列,看到这里的小伙伴们,你们觉得合适么? +>有人说,zookeeper可以做分布式配置中心、分布式消息队列,看到这里的小伙伴们,你们觉得合适么? + +- 注册中心不合适的原因: 保证一致性,但是可用性差,比如为了一致性,重新选主可能会停止服务十几秒,这导致服务调用宕机,不可接受。 ## 高性能高可用强一致性保障 @@ -156,6 +158,29 @@ ZK server根据其身份特性分为三种:Leader,Follower,Observer,其 增加follower节点,投票等待时间会变长,导致zookeeper集群写操作吞吐量下降。 而引入了不参与投票的服务器Observer, 提升了读请求吞吐量,对写操作没有影响。 + +3. **做分布式锁时,如何解决羊群效应**? + 1. 羊群效应是锁释放时,同时有多个锁去竞争,会影响性能。 + 2. 解决办法: 监听前一个等待锁的路径,公平锁,先到先得。 + + +# zookeeper的应用 + +## 分布式锁 + +用创建临时节点,去竞争,谁先创建是谁的锁,没抢到的监听该节点;拥有锁的做完后销毁, 就可以通知另外的节点, 它们可以从新创建。 + + +## 分布式协调 master worker + +只能有一个主创建成功master, 然后监听workers + +其他节点看主节点有没有数据, + +## 分布式队列 + +顺序节点 + # 常用命令 ## 删除 diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\347\237\245\350\257\206\347\202\271.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\347\237\245\350\257\206\347\202\271.md" new file mode 100644 index 0000000..5950819 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237/\345\210\206\345\270\203\345\274\217\347\237\245\350\257\206\347\202\271.md" @@ -0,0 +1,54 @@ + + + +# 问题 + +- 分布式系统的定义,有哪些应用场景: + - 多台计算机,通过网络协议通信,协同完成某些任务。 + - 场景:大规模的数据存储和处理、高并发访问、异构系统之间的数据交互 +- 分布式系统常见的技术问题有哪些? 如何解决这些问题可以通过什么方式解决? + - 数据一致性: 多个节点共同访问和修改数据,如何保证数据一致。 + - 两阶段提交、三阶段提交 + - Paxos算法、Raft算法 + - 高可用性、可靠性、容错性 + - 硬件:冗余备份,两套集群, + - 网络:监控,重试,幂等,降级,超时 + - 软件:容错设计、灰度发布,健康检查 + - 可扩展性 + - 负载均衡:请求分发到各个节点,如何平衡负载,提高系统性能和可靠性 + - 算法:轮询,加权轮询,随机,加权随机,最小连接数 + - 服务发现:如何找到可用服务节点,完成服务调用。 + - 实现方式:客户端的负载均衡、注册中心 + + + +分布式协议、分布式算法、分布式存储、分布式计算 + + +分布式事务 +- 在分布式系统中,多个节点之间共同完成一个事务时,如何保证事务的一致性和可靠性。 +- 解决方案: TCC事务、基于消息队列的最终一致性、分布式锁 + + + +**分布式锁** +- 如何保证多个节点对于共享资源的互斥访问。 +- 常见实现方式:基于数据库的锁、基于Redis的锁、基于Zookeeper的锁 + + +消息队列 +- 通过将消息存储到队列中,实现异步通信和解耦的一种方式。 +- 常见:Kafka、RabbitMQ、RocketMQ + + +设计分布式系统要考虑: 系统架构、负载均衡、数据分片、容错设计 + + +- 网络通信:CDN +- 统一的日志中心,ELK +- 高效传输:序列化技术,数据压缩,布隆过滤器、 + + +服务容灾备份 + + diff --git "a/\345\276\256\346\234\215\345\212\241&SOA/RMI.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/RMI.md" similarity index 100% rename from "\345\276\256\346\234\215\345\212\241&SOA/RMI.md" rename to "\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/RMI.md" diff --git "a/\345\276\256\346\234\215\345\212\241&SOA/Service Mess\345\205\245\351\227\250.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/Service Mess\345\205\245\351\227\250.md" similarity index 100% rename from "\345\276\256\346\234\215\345\212\241&SOA/Service Mess\345\205\245\351\227\250.md" rename to "\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/Service Mess\345\205\245\351\227\250.md" diff --git "a/\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\346\200\235\346\203\263.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\346\200\235\346\203\263.md" similarity index 100% rename from "\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\346\200\235\346\203\263.md" rename to "\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\346\200\235\346\203\263.md" diff --git "a/\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\347\232\204\344\275\277\347\224\250\346\224\273\347\225\245.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\347\232\204\344\275\277\347\224\250\346\224\273\347\225\245.md" similarity index 100% rename from "\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\347\232\204\344\275\277\347\224\250\346\224\273\347\225\245.md" rename to "\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\345\276\256\346\234\215\345\212\241&SOA/\345\276\256\346\234\215\345\212\241\347\232\204\344\275\277\347\224\250\346\224\273\347\225\245.md" diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\343\200\212Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\343\200\212Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..4a23a1b --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\343\200\212Java \345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\343\200\213\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,130 @@ + +# 一、并发理论基础 + +## 并发程序幕后的故事 + +CPU、内存、I/O 设备速度存在差异,如何解决? + + +为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为: + +- CPU 增加了**缓存**,以均衡与内存的速度差异; +- 操作系统增加了**进程、线程**,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异; +- 编译程序**优化指令执行次序**,使得缓存能够得到更加合理地利用。 + + +速度是快了很多,但并发程序很多诡异问题的根源也在这里。 + + +### 源头之一:缓存导致的可见性问题 + +- 单核CPU缓存是一个,可见性没问题 +- 多核CPU缓存不一样,但内存是同一个。 导致覆写 + + +### 源头之二:线程切换带来的原子性问题 + + +任务切换的时机大多数是在时间片结束的时候,我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条 CPU 指令完成,例如上面代码中的count += 1,至少需要三条 CPU 指令。 + +1. 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器; +2. 指令 2:之后,在寄存器中执行 +1 操作; +3. 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。 + + +操作系统做任务切换,可以发生在任何一条 CPU 指令执行完,是 CPU 指令,而不是高级语言里的一条语句。 + + +我们潜意识里面觉得 count+=1 这个操作是一个不可分割的整体,就像一个原子一样,线程的切换可以发生在 count+=1 之前,也可以发生在 count+=1 之后,但就是不会发生在中间。 + +我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。 + + +CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。 + + +### 源头之三:编译优化带来的有序性问题 + +有序性指的是程序按照代码的先后顺序执行。 + +编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中: +```java +a=6; +b=7; +``` +编译器优化后可能变成 +```java +b=7;a=6; +``` +在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。 + +不过有时候编译器及解释器的优化可能导致意想不到的 Bug。 + + +举例:**利用双重检查创建单例对象** +```java +public class SingletonDemo { + private SingletonDemo instance = null; + + public static SingletonDemo getInstance(){ + if(instance == null){ + //锁的是类 + synchronized(SingletonDemo.class){ + if(instance == null){ + return new SingletonDemo(); + } + } + } + return instance; + } +} +``` + +- 假设两个线程A/B都要获取实例, + - 都发现`instance==null`, + - 然后想加锁,这时候只有一个可以加锁成功。 + - 假设是A加锁成功,B就卡在加锁那一步。 + - 然后A就**new了一个实例**,释放锁。 + - B加锁成功,判断`instance!=null`,就直接跳出去`return instance`了。 + - 这一步有歧义:线程在synchronized块中,发生线程切换,锁是不会释放的。 所以这里情况不会发生。 + - B也可以在第一个判空就发现instance != null,而此时A进行到给instance赋地址但未初始化,发生了时间片切换,但不会释放锁。 B无法获取锁,但发现不为null,直接返回未初始化的数据。 + +这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?出在**new 操作**上: + +我们以为的 new 操作应该是: + +1. 分配一块内存 M; +2. 在内存 M 上初始化 Singleton 对象; +3. 然后 M 的地址赋值给 instance 变量。 + +但是实际上优化后的执行路径却是这样的: + +1. 分配一块内存 M; +2. 将 M 的地址赋值给 instance 变量; +3. 最后在内存 M 上初始化 Singleton 对象。 + +这样的顺序调整就可以出现: +- 假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上; +- 如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 **instance != null** ,所以直接返回 instance +- 而此时的 **instance 是没有初始化过**的,如果我们这个时候访问 instance 的成员变量就可能触发**空指针异常**。 + + +**静态内部类的单例模式方法**: + +```java +public class Singleton{ + + private static class SingletonHandler{ + private static singleton = new Singleton(); + } + + private MySingleton(){}; + + public Singleton getInstance(){ + return SingletonHandler.singleton; + } +} + +``` + + diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\343\200\212Java\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\346\210\230\343\200\213\347\254\224\350\256\260.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\343\200\212Java\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\346\210\230\343\200\213\347\254\224\350\256\260.md" new file mode 100644 index 0000000..fa0d544 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\343\200\212Java\346\200\247\350\203\275\350\260\203\344\274\230\345\256\236\346\210\230\343\200\213\347\254\224\350\256\260.md" @@ -0,0 +1,235 @@ + +# 概述 + +## 1 | 如何制定性能标准? + + +### 为什么要做性能调优? + +- 有些性能问题是时间累积慢慢产生的,到了一定时间自然就爆炸了;- 而更多的性能问题是由访问量的波动导致的,例如,活动或者公司产品用户量上升; + +>现在假设你的系统要做一次活动,产品经理或者老板告诉你预计有几十万的用户访问量,询问系统能否承受得住这次活动的压力。 + +所有的系统在开发完之后,多多少少都会有性能问题, + +我们首先要做的就是想办法把问题暴露出来,例如进行压力测试、模拟可能的操作场景等等,再通过性能调优去解决这些问题。 + + +**好的系统性能调优不仅仅可以提高系统的性能,还能为公司节省资源。** ——比如有大神把服务器的数量缩减到了原来的一半,系统的性能指标,反而还提升了。 + + +### 什么时候开始介入调优? + +- 在项目开发的初期,我们没有必要过于在意性能优化,这样反而会让我们疲于性能优化,不仅不会给系统性能带来提升,还会影响到开发进度。 + +- 只需要在代码层面保证有效的编码: + - 减少磁盘 I/O 操作、降低竞争锁的使用以及使用高效的算法。 + - 遇到比较复杂的业务,我们可以充分利用设计模式来优化业务代码。例如,设计商品价格的时候,往往会有很多折扣活动、红包活动,我们可以用装饰模式去设计这个业务。 + + +在**系统编码完成之后**,我们就可以对系统进行性能测试了。 + +- 产品提供线上预期数据,开发进行**压测,通过性能分析、统计工具来统计各项性能指标**,看是否在预期范围之内。 + +在项目成功上线后,我们还需要根据线上的实际情况,**依照日志监控以及性能统计日志**,来观测系统性能问题。 + + +### 有哪些参考因素可以体现系统的性能? + +#### 基础内容 +**CPU**::有的应用需要大量计算,他们会长时间、不间断地占用 CPU 资源,导致其他资源无法争夺到 CPU 而响应缓慢,从而带来系统性能问题。 +- 代码递归导致的无限循环 +- 正则表达式引起的回溯 +- JVM 频繁的 FULL GC +- 以及多线程编程造成的大量上下文切换等 + +>系统负载代表单位时间内正在运行或等待的进程或线程数,代表了系统的繁忙程度,CPU利用率则代表单位时间内一个线程或进程实时占用CPU的百分比。 + +- 一个进程或者线程在运行时,未必都在实时的利用CPU的。 +- 比如,在CPU密集型的情况下,系统的负载未必会高,但CPU的利用率肯定会高,一个线程/进程一直在计算,它对CPU的实时利用率是100%,而系统负载是0.1; + + + + +**内存**:Java 程序一般通过 JVM 对内存进行分配管理,主要是用 JVM 中的堆内存来存储Java 创建的对象。 +- 当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。 + + +**磁盘I/O**: 磁盘相比内存来说,存储空间要大很多,但磁盘 I/O 读写的速度要比内存慢。 + + +**网络**:带宽过低的话,对于传输数据比较大,或者是并发量比较 +大的系统,网络就很容易成为性能瓶颈。 + + +**异常:**Java 应用中,抛出异常需要**构建异常栈,对异常进行捕获和处理**,**这个过程非常消耗系统性能**。 +如果在高并发的情况下引发异常,持续地进行异常处理,那么系统的性能就会明显地受到影响。 + +>**打日志是否带exception?**, 如果没有生成堆栈追踪信息,不会有性能问题。一般业务异常避免生成堆栈追踪信息,我们知道这个异常是什么原因,所以直接返回字符串就好了。而系统异常,一般都会生成堆栈追踪信息,以便追踪源头,更好的排查问题。 + + +**数据库** +大部分系统都会用到数据库,而数据库的操作往往是涉及到磁盘 I/O 的读写。 + +大量的数据库读写操作,会导致磁盘 I/O 性能瓶颈,进而导致数据库操作的延迟性。 + + +**锁竞争:** + +锁的使用可能会带来上下文切换,从而给系统带来性能开销。JDK1.6 之后,Java 为了降低锁竞争带来的上下文切换,对 JVM 内部锁已经做了多次优化,例如,新增了偏向锁、自旋锁、轻量级锁、锁粗化、锁消除等。 + +#### 业务性能指标 + +**响应时间** + +>响应时间是衡量系统性能的重要指标之一,响应时间越短,性能越好,一般一个接口的响应时间是在毫秒级。 + +- **数据库响应**时间:数据库操作所消耗的时间,往往是整个请求链中最耗时的。 + +- **服务端响应**时间:服务端包括 Nginx 分发的请求所消耗的时间以及服务端程序执行所消耗的时间; + +- **网络响应**时间:这是网络传输时,网络硬件需要对传输的请求进行解析等操作所消耗的时间; + +- **客户端响应**时间:对于普通的 Web、App 客户端来说,消耗时间是可以忽略不计的,但如果你的客户端嵌入了大量的逻辑处理,消耗的时间就有可能变长,从而成为系统的瓶颈 + + + + +**吞吐量** + +>在测试中,我们往往会比较注重系统接口的 TPS(每秒事务处理量),因为 TPS 体现了接口的性能,TPS 越大,性能越好。 + +- **磁盘吞吐量** + - 一种是 IOPS(Input/Output Per Second),即每秒的输入输出量(或读写次数),这种是指单位时间内系统能处理的 I/O 请求数量,I/O 请求通常为读或写数据操作请求,关注的是随机读写性能。适应于随机读写频繁的应用,如小文件存储(图片)、OLTP 数据库、邮件服务器。 + - 数据吞吐量,这种是指单位时间内可以成功传输的数据量。对于大量顺序读写频繁的应用,传输大量连续数据,例如,电视台的视频编辑、视频点播 VOD(Video OnDemand),数据吞吐量则是关键衡量指标。 +- **网络吞吐量** + - 指网络传输时没有帧丢失的情况下,设备能够接受的最大数据速率。 + - 网络吞吐量不仅仅跟带宽有关系,还跟 CPU 的处理能力、网卡、防火墙、外部接口以及 I/O 等紧密关联。 + - 而吞吐量的大小主要由网卡的处理能力、内部程序算法以及带宽大小决定。 + + + +**计算机资源分配使用率** + +- 通常由 CPU 占用率、内存使用率、磁盘 I/O、网络 I/O 来表示资源使用率。 +- 这几个参数好比一个木桶,如果其中任何一块木板出现短板,任何一项分配不合理,对整个系统性能的影响都是毁灭性的。 + + +**负载承受能力** + +当系统压力上升时,你可以观察,系统响应时间的上升曲线是否平缓。这项指标能直观地反馈给你,系统所能承受的负载压力极限。 + + +**监控上述指标的组合、变化趋势、是否有突变等** + +不仅仅是比较吞吐量、响应时间、负载能力等直接指标了,还需要比较系统资源的 CPU 占用率、内存使用率、磁盘I/O、网络 I/O 等几项间接指标的变化。 + + + + +## 2 | 如何制定性能调优策略? + +**“测试 - 分析 - 调优”三步走** + +### 性能测试攻略 + +**1. 微基准性能测试** + + +微基准性能测试可以精准定位到某个模块或者某个方法的性能问题,特别适合做一个功能模块或者一个方法在不同实现方式下的性能对比。 + +>例如,对比一个方法使用同步实现和非同步实现的性能。 + + +**2. 宏基准性能测试** + + +宏基准性能测试是一个综合测试,需要考虑到测试环境、测试场景和测试目标。 + +- 测试环境: 需要模拟线上的真实环境。 +- 测试场景: 要确定在测试某个接口时,是否有其他业务接口同时也在平行运行,造成干扰。如果有,请重视,因为你一旦忽视了这种干扰,测试结果就会出现偏差。 +- 测试目标: 以通过吞吐量以及响应时间来衡量系统是否达标。不达标,就进行优化;达标,就继续加大测试的并发数,探底接口的TPS(最大每秒事务处理量) + + +**一些需要注意的问题** + + +1. **热身问题** + +>随着代码被执行的次数增多,当虚拟机发现某个方法或代码块运行得特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会通过即时编译器(JIT compiler,just-in-time compiler)把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后存储在内存中,之后每次运行代码时,直接从内存中获取即可。 + + + +2. **2. 性能测试结果不稳定** + +>伴随着很多不稳定因素,比如机器其他进程的影响、网络波动以及每个阶段JVM 垃圾回收的不同等等 + + +3. **多 JVM 情况下的影响** + +>任意一个 JVM 都拥有整个系统的资源使用权。 该尽量避免线上环境中一台机器部署多个JVM 的情况 + + +### 合理分析结果,制定调优策略 + + +#### 分析问题 + +在完成性能测试之后,需要输出一份性能测试报告,帮我们分析系统性能测试的情况。 其中测试结果需要包含测试接口的平均、最大和最小吞吐量,响应时间,服务器的 CPU、内存、I/O、网络 IO 使用率,JVM 的 GC 频率等。 + + +通过观察这些调优标准,可以发现性能瓶颈,我们再通过**自下而上**的方式分析查找问题。 + +1. 先从操作系统层面,查看系统的 CPU、内存、I/O、网络的使用率是否存在异常,再通过命令查找异常日志,最后通过分析日志,找到导致瓶颈的原因; +2. 再从 Java 应用的 JVM层面,查看 JVM 的垃圾回收频率以及内存分配情况是否存在异常,分析日志,找到导致瓶颈的原因。 +3. 最后可以查看**应用服务业务层**是否存在性能瓶颈,例如 Java 编程的问题、读写数据瓶颈等。 + + +>某个性能问题可能是一个原因导致的,也可能是几个原因共同导致的结果。 +>我们分析查找问题可以采用自下而上的方式,而我们解决系统性能问题,则可以采用自上而下的方式逐级优化。 + + +#### 解决问题 + +- **应用调优** + - 应用层的问题代码往往会因为耗尽系统资源而暴露出来。 + >例如: 我们某段代码导致内存溢出,往往是将 JVM 中的内存用完了,这个时候系统的内存资源消耗殆尽了,同时也会引发JVM 频繁地发生垃圾回收,导致 CPU 100% 以上居高不下,这个时候又消耗了系统的CPU 资源。 +- 其他一些非代码问题导致的性能问题,比较难发现 + +**1. 优化代码** + + +**2. 优化设计** + + +**3. 优化算法** + + +- **系统调优**: + 1. 操作系统调优 + 2. 组件调优 + 3. JVM调优 + + +**4. 时间换空间** + + +**5. 空间换时间** + + +**6. 参数调优** + + +### 兜底策略,确保系统稳定性 + +**什么是兜底策略?** + + +第一,限流,对系统的入口设置最大访问限制。这里可以参考性能测试中探底接口的 TPS +。同时采取熔断措施,友好地返回没有成功的请求。 + +第二,实现智能化横向扩容。智能化横向扩容可以保证当访问量超过某一个阈值时,系统可 +以根据需求自动横向新增服务。 + +第三,提前扩容。这种方法通常应用于高并发系统,例如,瞬时抢购业务系统。这是因为横 +向扩容无法满足大量发生在瞬间的请求,即使成功了,抢购也结束了。 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\346\200\247\350\203\275\346\265\213\350\257\225\346\226\271\346\263\225.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\346\200\247\350\203\275\346\265\213\350\257\225\346\226\271\346\263\225.md" new file mode 100644 index 0000000..1612605 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\346\200\247\350\203\275\346\265\213\350\257\225\346\226\271\346\263\225.md" @@ -0,0 +1,3 @@ + + +# 基准测试 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\346\200\247\350\203\275\351\227\256\351\242\230\346\216\222\346\237\245\346\214\207\345\215\227\357\274\210Java\357\274\211.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\346\200\247\350\203\275\351\227\256\351\242\230\346\216\222\346\237\245\346\214\207\345\215\227\357\274\210Java\357\274\211.md" new file mode 100644 index 0000000..eb7b3c9 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\200\247\350\203\275\344\274\230\345\214\226\345\270\210/\346\200\247\350\203\275\351\227\256\351\242\230\346\216\222\346\237\245\346\214\207\345\215\227\357\274\210Java\357\274\211.md" @@ -0,0 +1,116 @@ + +[TOC] +# 命令汇总备忘录 +## 系统工具命令 +- **top** + - top -Hp pid 查看具体线程使用系统资源情况 +- **vmstat**:指定采样周期和次数的功能性检测工具,统计内存使用情况、观察CPU使用率、swap适应情况。主要用于**观察进程上下文切换** + - vmstat 1 3 一秒一次,统计三次。 出现是的数据解析如下 + ![enter image description here](/tencent/api/attachments/s3/url?attachmentid=232697) + - r:等待运行的进程数; + - b: 处于非中断睡眠状态的进程数 + - swpd: 虚拟内存使用情况; + - free: 空闲的内存 + - buff:用于缓冲的内存数 + - si:从磁盘交换到内存的交换页数量; + - so:从内存交换到磁盘的交换页数量; + - bi:发送到块设备的块数; + - bo:从块设备接收到的块数; + - in:每秒中断数; + - cs:每秒上下文切换次数; + - us:用户 CPU 使用时间; + - sy:内核 CPU 系统使用时间; + - id:空闲时间; + - wa:等待 I/O 时间; + - st:运行虚拟机窃取的时间 +- **pidstat** : Sysstat 中的一个组件,也是一款功能强大的性能监测工具,**深入到线程级别**。 + >可以通过命令:`yum install sysstat` 安装该监控组件 + + - pidstat -help **查看参数**,解析如下: + - -u:默认的参数,显示各个进程的 cpu 使用情况; + - -r:显示各个进程的内存使用情况; + - -d:显示各个进程的 I/O 使用情况; + - -w:显示每个进程的上下文切换情况; + - -p:指定进程号; + - -t:显示进程中线程的统计信息。 + - 通过ps、jsp获取进程ID,然后 `pidstat -p 345 -r 1 3`, 获取的数据解析如下: + - Minflt/s:任务每秒发生的次要错误,不需要从磁盘中加载页; + - Majflt/s:任务每秒发生的主要错误,需要从磁盘中加载页; + - VSZ:虚拟地址大小,虚拟内存使用 KB; + - RSS:常驻集合大小,非交换区内存使用 KB。 + +## JDK工具命令 + +### jstack 堆栈分析 +- jstack pid 查看线程堆栈信息,结合top -Hp pid查看现场状态,也经常用来排查一些死锁异常。 + - 线程 ID、线程的状态(wait、sleep、running 等状态)以及是否持有锁 + +### jmap 堆内存 +**作用**: +- 堆内存初始化配置信息以及堆内存使用情况 +- 堆内存中对象的信息:包括产生了哪些对象,对象数量多少 + +**举例**: +- jmap -heap pid 查看堆内存初始化配置信息以及堆内存的使用情况 +![enter image description here](/tencent/api/attachments/s3/url?attachmentid=232773) +- jmap -histo:live pid 查看堆内存中的对象数目、大小统计直方图,如果带live则只统计存活对象。 +![enter image description here](/tencent/api/attachments/s3/url?attachmentid=232778) +- dump到文件中:`jmap -dump:format=b,file=/tmp/heap.hprof pid`, 然后将文件下载下来,使用 **MAT** 工具打开文件进行分析。 + +# 系统篇 +参考Brendan Gregg提供的完整图谱: +![enter image description here](/tencent/api/attachments/s3/url?attachmentid=233820) + +## CPU + +## 内存 + + +## 网络 + +## 经验之谈 +- dstat命令是一个用来替换vmstat、iostat、netstat、nfsstat和ifstat这些命令的工具,是一个全能系统信息统计工具 + + +# JVM篇 + +## 运行时监控 +- 利用 JMC、JConsole 等工具进行运行时监控。 + + +## 工具分析 +- 利用各种工具,在运行时进行堆转储分析,或者获取各种角度的统计数据(如jstat - +gcutil 分析 GC、内存分带等)。 + +## GC日志分析 +- GC 日志等手段,诊断 Full GC、Minor GC,或者引用堆积等 + + +## Profiling +对于应用**Profiling**,简单来说就是利用一些侵入性的手段,收集程序运行时的**细节**,以定 +位性能问题瓶颈. +>所谓的细节,就是例如内存的使用情况、最频繁调用的方法是什么,或者上下文切换的情况等 +一般不建议生产系统进行 Profiling,大多数是在性能测试阶段进行。 + +但是,当生产系统确实存在这种需求时,也不是没有选择。我建议使用 JFR配合JMC来做 Profiling,因为它是从 Hotspot JVM 内部收集底层信息,并经过了大量优化,性能开销非常低,通常是低于 2% 的 + + +它的使用也非常方便,你不需要重新启动系统或者提前增加配置。例如,你可以在运行时启动 JFR 记录,并将这段时间的信息写入文件: +``` +Jcmd JFR.start duration=120s filename=myrecording.jfr +``` +然后,使用 JMC 打开“.jfr 文件”就可以进行分析了,方法、异常、线程、IO 等应有尽有,其功能非常强大。如果你想了解更多细节,可以参考[相关指南](https://blog.takipi.com/oracle-java-mission-control-the-ultimate-guide/) 。 + + +profiling收集程序运行时信息的方式主要有以下三种: +- 事件方法:对于 Java,可以采用 JVMTI(JVM Tools Interface)API 来捕捉诸如方法调用、类载入、类卸载、进入 / 离开线程等事件,然后基于这些事件进行程序行为的分析。统计抽样方法(sampling): 该方法每隔一段时间调用系统中断,然后收集当前的调用栈(call stack)信息,记录调用栈中出现的函数及这些函数的调用结构,基于这些信息得 + + + +# 实践案例 + +## 系统越来越慢? + + +### 经验 +1. 找繁忙线程时,top -h , 再jstack, 再换算tid比较累,而且jstack会造成停顿。推荐用vjtools里的vjtop, 不断显示繁忙的javaj线程,不造成停顿 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/TDD\346\265\213\350\257\225\351\251\261\345\212\250\345\274\200\345\217\221\345\256\236\346\210\230.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/TDD\346\265\213\350\257\225\351\251\261\345\212\250\345\274\200\345\217\221\345\256\236\346\210\230.md" new file mode 100644 index 0000000..490022d --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/TDD\346\265\213\350\257\225\351\251\261\345\212\250\345\274\200\345\217\221\345\256\236\346\210\230.md" @@ -0,0 +1,35 @@ + +# TDD 测试驱动开发 + +## 优点 + + +## 缺点 + + +## 实战 + +每2个工作日至少提交一次代码 + + +### 确认需求 + +- 理解一致 +- 概念明确 + - 工作日? 周末不算,员工请假? + +# 火星车 +- 初始化坐标,xy, 朝向。 +- 批量指令 +- 全部执行之后回报自己的坐标和朝向 +- 指令: 前/后/左/右 + +## case +- 初始化信息: 坐标(0,0), 朝向 N ,区域。 +- 批量指令:前进2,左转,后退3 +- 汇报位置: (2,-3)朝向S +- 前进/后退/转向,每个动作后都汇报下坐标, + - 朝向 + - 判断是否超出边界,边界报警。 +- move +- 汇报位置 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204.md" new file mode 100644 index 0000000..d207223 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204.md" @@ -0,0 +1,47 @@ + + +# 分布式系统 + +## 分布式系统五大关键技术 + +### 简述 + +**目的** + + +构建分布式系统的**目的**是 +- 增加系统容量: 大流量处理。通过集群技术把大规模并发请求的负载分散到不同的机器上。 +- 提高系统的可用性: 关键业务保护。提高后台服务的可用性,把故障隔离起来阻止多米诺骨牌效应(雪崩效应)。如果流量过大,需要对业务降级,以保护关键业务流转。 + +>一是提高整体架构的吞吐量,服务更多的并发和流量,二是为了提高系统的稳定性,让系统的可用性更高。 + + +**提高架构的性能** + + +- 提高系统性能的常用技术 + - 加缓存 + - 负载均衡 + - 异步调用 + - 数据镜像 + - 数据分区 + + + +### 全栈监控 + + + + +### 资源、服务调度 + + + + +### 状态、数据调度 + + +### 流量调度 + + +### 水到渠成: 开发和运维的自动化 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/\345\234\272\346\231\257\351\235\242\350\257\225\351\242\230.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/\345\234\272\346\231\257\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..cd42158 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\346\236\266\346\236\204\345\270\210/\345\234\272\346\231\257\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,203 @@ + +# 数据库 + + +## 数据库迁移如何同步保证增量数据不丢? + +参考: +- https://www.cnblogs.com/czsy/archive/2022/12/15/16985912.html + +### 业务双写 + +**优缺点** + +- 优点: 1. 简单易操作 2.无需中间件支持 3.**无延迟** + +- 缺点: **对业务侵入大**,需要在新老系统维护对应的数据同步逻辑 + + +**操作步骤**: + +1. 同步全量(选个时间点) + 1. 通过sql扫表,如果表比较大可以按月或者按天扫,优点是操作简单,缺点是需要人工一直介入,且扫表会对数据库造成一定压力,影响业务功能的稳定性 + 2. 同步离线表取数,将离线表的数据发送到mq或者kafaka,消费后进行数据同步 + 3. 存量数据同步的时候也需要注意的是数据的版本问题,如果不存在就新增,如果存在就判断时间戳或版本号,总之就是将最新的数据更新进去,老版本数据抛弃。 + + +2. 同步增量(增量先跑,避免丢数据) + 1. 将老系统中所有新增、更新的地方都写上数据同步逻辑. + 2. 在同步数据的时候,除了新老模型字段的映射逻辑以外,当操作是**新增**的时候,直接新增新表数据,当操作是**更新**的时候,**如果新表没有对应信息,那么查一下老表的数据**,然后映射到新模型数据结构再新增到新表 + 3. 更新新模型的时候可能存在并发问题,这时候我们插入的时候要检查时间戳或者版本号,如果库内数据早于自己,就更新,否则就丢弃。 + + +同步代码的复用性差,业务代码内部维护的增量逻辑和存量同步的逻辑不能复用,需要重复开发。 + +此方案只适合公司没有中间件支持并且又要做改造的情况下使用。 + +>如果数据有时效性,比如只需要保存2个月的数据,可以直接写增量2个月。 + +### binlog,数据双向同步 + +>当数据变更的时候(新增、更新、删除),db 都会记录变更日志,并且同步到各个从库中,这个日志就是我们耳闻能详的 binlog。 + +开源的工具主要有:Canal、otter等,基本原理就是解析binlog日志,然后发送到消息中间件,客户端消费后进行处理。 + + +**insert** + +操作可能出现的异常场景: + +1. insert 操作还没插入新表,老系统就对该记录操作了一次更新,然后也吐出了一条 binlog,这时候这条 binlog 先被客户端消费,由于更新的时候如果新表内没有数据,会更新失败,更新失败后会走数据订正逻辑。 +2. 数据订正的时候如果新表没有数据则会新增。这时候再操作 insert 操作就会报主键冲突。新增失败的时候也会走数据订正逻辑。 + +>在消费到的时候根据数据的主键ID加一把分布式锁能不能解决问题。 答案是不能解决,因为更新操作可能会更先被消费到,这时候还是会报主键重复,并且如果数据量大的情况下,加锁还会导致数据同步性能问题。 + + +**update** + +不是无脑更新的,需要加个乐观锁(where update_time < #{updateTime}),如果表里数据已经比你新了,那么就不更新,更新失败,走数据订正逻辑。如果表里数据比较老,那么更新成功。 + + +**数据修订** + +不管是 insert 操作还是 update 操作,当操作失败了以后,我们都要进行数据订正,这是为了保证最终数据一致性。 + +数据订正的整个过程都需要根据主键ID来进行**加分布式锁**,这是因为数据订正的时候是拿主键ID去新老表查数据,然后进行比对后才决定如何进行订正。 + +> 也可以不加锁,直接比较时间,看谁更新。 取值比较时,记得带上version,保证待会写入时,版本号没变化(cas比较值),避免这个比较过程中,有更新数据已经写入。 + + +**优缺点**: 采用binlog同步的优点就是针对所有的dml操作集中处理,解耦业务、可发挥空间大;缺点就是需要中间件支持,并且具有一定的延迟性。 + + +**适用场景**: 当我们老系统多张表,融合到新系统只有一张表;或者老系统一张表,拆到新系统多张表;那么这种场景就很适合用这种方式来同步,只需要在数据同步逻辑根据关联的字段查出对应的信息进行insert或者update即可。 + + +>做好监控 +>迁移的回退工作评估 + +# 数据结构 + + + +# 架构设计 + + +# 高并发 + +## 线程 + +### 线程池参数如何确定->设计监控线程池运行状况及实时调整参数的工具? + +ThreadPoolExecutor类可设置的参数主要有: + +- **corePoolSize**:核⼼线程 + + 1. 核⼼线程会⼀直存活,及时没有任务需要执⾏ + + 2. 当线程数⼩于核⼼线程数时,即使有线程空闲,线程池也会优先创建新线程处理 + + 3. 设置allowCoreThreadTimeout=true(默认false)时,核⼼线程会超时关闭 + +- **queueCapacity**:任务队列容量(阻塞队列) + + - 当**核⼼线程数**达到最⼤时,新任务会放在队列中排队等待执⾏ + +- **maxPoolSize**:最⼤线程数 + + 1. 当线程数**>=corePoolSize**,且**任务队列已满**时。线程池会创建新线程来处理任务 + + 2. 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务⽽抛出异常 + +- **keepAliveTime**:线程空闲时间 + + 1. 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize + 2. 如果allowCoreThreadTimeout=true,则会直到线程数量=0 + +- **rejectedExecutionHandler**:任务拒绝处理器 + + - 发送时机: + 1. 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务 + 2. 当线程池被调⽤shutdown()后,会等待线程池⾥的任务执⾏完毕,再shutdown。如果在调⽤shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。 + - 拒绝策略 + 1. 默认是AbortPolicy,会抛出异常。 + 2. CallerRunsPolicy 执⾏任务 + 3. DiscardPolicy 忽视,什么都不会发⽣ + 4. DiscardOldestPolicy 从队列中踢出最先进⼊队列(最后⼀个执⾏)的任务 + 5. 实现RejectedExecutionHandler接⼝,可⾃定义处理器 + + +**合理设置参数** + +⾸先确定有以下⼏个相关参数: + +1. tasks,程序每秒需要处理的最⼤任务数量(假设系统每秒任务数为100~1000) + +2. tasktime,单线程处理⼀个任务所需要的时间(每个任务耗时0.1秒) + +3. responsetime,系统允许任务最⼤的响应时间(每个任务的响应时间不得超过2秒) + + +**coreSize** + +- 从并发和延迟考虑,比如并发查询请求100-1000, 每次请求耗时0.1s,那么单线程,1s能处理10个请求(保证在1s内返回),那么10-100个线程就可以满足需求。 core设置为10. + +- 具体数字最好根据8020原则,即80%情况下系统每秒任务数,若系统80%的情况下任务数⼩于200,最多时为1000,则corePoolSize可设置20 + + +**queue队列** + +任务队列的长度要根据核⼼线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为(corePoolSize/tasktime)responsetime(20/0.1)2=400,即队列长度可设置为400 + + +若结合CPU的情况,⽐如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能⼒,应设法降低每个任务的处理时间(tasktime)。 + + +**CUP密集型**: 线程数设置为 N + 1 +**IO密集型**: 线程数设置为2N + + +动态配置: +- 修改core核心数 +- 修改max核心数 +- 队列大小一般是final, 美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue 的队列(主要就是把LinkedBlockingQueue的capacity 字段的final关键字修饰给去掉了,让它变为可变的) + + +**美团** + +动态化线程池的核心设计包括以下三个方面: + +1. 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种: + (1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。 + (2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。 + +>所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。 + + +2. 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。 + - 在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。 + - 将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。 + +3. 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。 + + - 动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等. + + +**监控** + + +1. 负载监控和告警 + +- 基于当前线程池参数分配的资源够不够。 + +- **事前**,线程池定义了“活跃度”这个概念,来让用户在发生Reject异常之前能够感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。这个公式代表**当活跃线程数趋向于maximumPoolSize的时候,代表线程负载趋高**。 + +- 事中,也可以从两方面来看线程池的过载判定条件,一个是**发生了Reject异常,一个是队列中有等待任务**(支持定制阈值)。 + + +2. 运行时实时查看 + 1. 用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数。 存活数量、最大值、完成任务数量、队列等待数量 + + + +>参考原文链接:https://blog.csdn.net/weixin_45560850/article/details/124946074 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\350\265\204\346\267\261\345\274\200\345\217\221\345\267\245\347\250\213\345\270\210/\343\200\212\351\207\215\346\236\204\343\200\213\350\257\273\344\271\246\347\254\224\350\256\260.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\350\265\204\346\267\261\345\274\200\345\217\221\345\267\245\347\250\213\345\270\210/\343\200\212\351\207\215\346\236\204\343\200\213\350\257\273\344\271\246\347\254\224\350\256\260.md" new file mode 100644 index 0000000..d6d6e10 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\350\265\204\346\267\261\345\274\200\345\217\221\345\267\245\347\250\213\345\270\210/\343\200\212\351\207\215\346\236\204\343\200\213\350\257\273\344\271\246\347\254\224\350\256\260.md" @@ -0,0 +1,75 @@ +# 《重构·改善既有代码的设计》 +[TOC] + +# 举个栗子(重构是什么) + +>如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性 + +1. 复制一遍代码似乎不算太难,但却给未来留下各种隐患:一旦计费逻辑发生变化,我就得同时修改两个地方,以保证它们逻辑相同。 + +2. 作为一个经验丰富的开发者,我可以肯定:不论最终提出什么方案,他们一定会在6个月之内再次修改它。毕竟,需求通常不来则已,一来便会接踵而至。 + +3. 只要还能运行,不用改没有需求变化的代码。能改进之当然很好,但若没人需要去理解它,它就不会真正妨碍什么。 + +4. 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。 + a. 我将测试视为bug检测器,它们能保护我不被自己犯的错误所困扰 + b. 尽管编写测试需要花费时间,但却为我节省下可观的调试时间。 + +5. 重构的步骤 + a. 无论每次重构多么简单,养成重构后即运行测试的习惯非常重要 + b. 犯错误是很容易的——至少我知道我是很容易犯错的。做完一次修改就运行测试,这样在我真的犯了错时,只需要考虑一个很小的改动范围,这使得查错与修复问题易如反掌。 +>精髓:小步修改,每次修改后就运行测试; 永远将函数的返回值命名为“result”,这样我一眼就能知道它的作用。 + +6. 傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。 + +7. 只要改名能够提升代码的可读性,那就应该毫不犹豫去做。 + +8. 当我分解一个长函数时,我喜欢将play这样可以通过另外一个变量计算得到的局部变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。这里我要使用的重构手法是以查询取代临时变量(178) +```java + +play = playFor(pref); +thisAmount = amountFor(perf, play); + +``` +>重构前,查找play变量的代码在每次循环中只执行了1次,而重构后却执行了3次。我会在后面探讨重构与性能之间的关系 + +9. 在做任何提炼前,我一般都会先移除局部变量 + +10. format未能清晰地描述其作用。formatAsUSD很表意,但又太长,特别它仅是小范围地被用在一个字符串模板中 + +11. 好的命名十分重要,但往往并非唾手可得。只有恰如其分地命名,才能彰显出将大函数分解成小函数的价值。有了好的名称,我就不必通过阅读函数体来了解其行为。 +>但要一次把名取好并不容易,因此我会使用当下能想到最好的那个。如果稍后想到更好的,我就会毫不犹豫地换掉它 + +12. 移动语句(223)手法将变量声明挪动到紧邻循环的位置 + +13. volumeCredits。处理这个变量更加微妙,因为它是在循环的迭代过程中累加得到的。第一步,就是应用拆分循环(227)将volumeCredits的累加过程分离出来 + +14. for循环里面做了好几件事情,为了提炼函数,将这几次事情都分别for循环。 有人可能会对对此修改可能带来的性能问题感到担忧,很多人本能地警惕重复的循环。但大多数时候,**重复一次这样的循环对性能的影响都可忽略不计。** + +15. 因此对于重构过程的性能问题,我总体的建议是:大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化。 + +16. 我们移除volumeCredits的过程是多么小步。整个过程一共有4步,每一步都伴随着一次编译、测试以及向本地代码库的提交: + a. 使用拆分循环(227)分离出累加过程; + b. 使用移动语句(223)将累加变量的声明与累加过程集中到一起; + c. 使用提炼函数(106)提炼出计算总数的函数; + d. 使用内联变量(123)完全移除中间变量 + +>我得坦白,我并非总是如此小步——但在事情变复杂时,我的第一反应就是采用更小的步子。怎样算变复杂呢,就是当重构过程有测试失败而我又无法马上看清问题所在并立即修复时,我就会回滚到最后一次可工作的提交,然后以更小的步子重做。 + + +## 重构的原则(为什么应该重构) + + +## 代码坏味道(该在什么地方重构) + +## 测试体系 + +## 重构名录(类型) + +- 【106】 提炼函数 + - 首先,我需要检查一下,如果我将这块代码提炼到自己的一个函数里,有哪些变量会离开原本的作用域。 + - [ ] 不会被修改,那么我就可以将它们以参数方式传递进来。 + - [ ] 更关心那些会被修改的变量,以将它从函数中直接返回。 + +- 【123】 内敛变量 + - 这个变量后面不再改动,直接用方法代替这个变量出现在使用位置。 减少局部变量。 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\350\265\204\346\267\261\345\274\200\345\217\221\345\267\245\347\250\213\345\270\210/\345\206\231\347\273\231\345\267\245\347\250\213\345\270\210\347\232\204\345\207\240\346\235\241\345\273\272\350\256\256.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\350\265\204\346\267\261\345\274\200\345\217\221\345\267\245\347\250\213\345\270\210/\345\206\231\347\273\231\345\267\245\347\250\213\345\270\210\347\232\204\345\207\240\346\235\241\345\273\272\350\256\256.md" new file mode 100644 index 0000000..e843078 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\350\265\204\346\267\261\345\274\200\345\217\221\345\267\245\347\250\213\345\270\210/\345\206\231\347\273\231\345\267\245\347\250\213\345\270\210\347\232\204\345\207\240\346\235\241\345\273\272\350\256\256.md" @@ -0,0 +1,55 @@ +此文为阅读某文章的摘要:https://mp.weixin.qq.com/s/vNLEvP4dJLOmeprxyhlp3g +推荐书目:《原则》、史蒂芬·柯维在《高效能人士的七个习惯》 + +新人常有困境: 几乎每天晚上11点多才走,很累很辛苦,但依然拿不到想要的结果。很多同学在不停地重复犯着自己当年类似的错误。他们并不是不努力,到底是哪里出了问题? + +我们大多数同学在工作中缺乏原则的指导。原则,犹如指引行动的“灯塔”,它连接着我们的价值观与行动。 每个人都应该有自己的原则,当我们需要作出选择时,一定要坚持以原则为中心。 + +本文总结了十条精进原则: + +原则一:Owner意识 +“Owner意识”主要体现在两个层面:一是认真负责的态度,二是积极主动的精神。 + +认真负责是工作的底线。 +● 首先,要对交付的结果负责。 + ○ 项目中每一个设计文档、每一行代码,都要对它的质量负责。 + ○ 如果设计文档逻辑混乱,代码没有注释,测试时发现一堆Bug,影响的不仅仅是RD的工程交付质量,还会对协同工作的RD、QA、PM等产生不好的影响。 +● 其次,要对开发的系统负责。 + ○ 系统的架构是否需要改进,接口文档是否完善,日志是否完整,数据库是否需要扩容,缓存空间够不够等等 + +积极主动是“Owner意识”更高一级的要求。 +● RD每天要面对大量的工作,而且很多并不在计划内,这就需要具备一种积极主动的精神。 +● 面对运营产品提出的问题,及时响应,不要装作看不到或者不会就不管。 +● 正确的做法是:积极主动地推动问题的解决,如果时间无法排开或者不知道如何解决,可以直接将问题反馈给能解决的同学。 +● 我们在做好自己份内工作的同时,也应该积极主动地投入到“份外”的工作中去。一分耕耘一分收获,不要给自己设限。 + ○ 很多同学会自发地梳理负责服务的现状,根据接口在性能方面暴露的问题提出改进意见并持续推动解决; + ○ 也有同学在跨团队沟通中主动承担起主R的角色,积极发现问题、暴露问题,推动合作团队的进度,保证项目顺利推进。 + +原则二:时间观念 +RD的研发效率是一个公司硬实力的重要体现。项目的按期交付是一项很重要的执行能力,在很大程度上决定着领导和同事对自己靠谱程度的评价。 + +难度几乎相同的项目,为什么有的同学经常Delay,而有的同学每次都能按时上线?这些按时交付的同学往往具备如下两个特质:做事有计划,工作分主次。 + +工作安排要有计划性。 +在计划制定过程中,要尽可能把每一项拆细一点(至少到pd粒度)。事实证明,粒度越细,计划就越精准,实际开发时间与计划之间的误差就会越小。 + +此外,务必要规定明确的可检查的产出,并在计划中设置一些关键的时间点进行核对。周五交付和周五上午交付是两个概念,要明确,否则可能出现pd级别的误差。 + +工作安排要分清楚主次。 +把工作按照重要、紧急程度分成四象限。 +● 优先做重要紧急的事情; +● 重要不紧急的事情可以暂缓做,但是要持续推进; +● 紧急不重要的事情可以酌情委托给最合适的人做; +● 不重要不紧急的事情可以考虑不做。 +比如在开发中需要使用到ES,一些不熟悉ES的同学可能想系统性地学习一下这方面的知识,就会一头扎进ES的汪洋中。最后才发现,原本一天就能完成的工作被严重拖后。实际工作中,我们应当避免这种“本末倒置”的工作方式。在本例中,“系统性地学习ES”是一件重要但不紧急的事情。要学会分辨出这些干扰的工作项,保证重要紧急的事情能够按时交付。扎心了...以前经常犯这种错误。 + +原则三:以终为始 +先想清楚目标,然后努力实现。 + +要知道目标是什么,而不是只埋头干活。 很多同学季度总结的时候,罗列了很多项目,付出很多努力。但是具体这些项目取得了哪些收益,对业务有哪些提升,却很难说出来。 + +此外,很多同学在做需求的过程中,对于目标与收益关注不够,系统上线之后,也没有持续地跟进使用效果。这一点在技术优化项目中体现的尤为明显。要根据问题设定目标,再进行优化。 + +很多同学看过很多技术文章,但是总是感觉自己依然一无所知。很重要的一个原因,就是没有带着目标去学习。如果只是碎片化地接收各个公众号推送的文章,效果几乎可以忽略不计。 + +原则四:闭环思维 \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\351\253\230\345\271\266\345\217\221/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213.md" "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\351\253\230\345\271\266\345\217\221/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213.md" new file mode 100644 index 0000000..39286a8 --- /dev/null +++ "b/\350\256\241\347\256\227\346\234\272\350\277\233\351\230\266/\351\253\230\345\271\266\345\217\221/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230-\350\257\276\347\250\213.md" @@ -0,0 +1,185 @@ + + +## 02 Java内存模型:看Java如何解决可见性和有序性问题 + +导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。 + + +合理的方案应该是按需禁用缓存以及编译优化。 + +- 对于并发程序,何时禁用缓存以及编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。 +- 所以,为了解决可见性和有序性问题,只需要**提供**给程序员**按需禁用缓存和编译优化的方法**即可. + + +Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则. + + +### Happens-Before规则 + + +含义: 前面一个操作的结果对后续操作是可见的。 + +Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。 + +**示例代码** + +场景: 假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢? + +要看 Java 的版本,如果在低于 1.5 版本上运行,x 可能是 42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。 + +```java + +// 以下代码来源于【参考1】 +class VolatileExample { + int x = 0; + volatile boolean v = false; + public void writer() { + x = 42; + v = true; + } + public void reader() { + if (v == true) { + // 这里x会是多少呢? + } + } +} +``` + + +1. **程序的顺序性规则** + +指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。 + +程序前面对某个变量的修改一定是对后续操作可见的。 + + +2. **volatile 变量规则** + +这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。 +——这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下,就有点不一样的感觉了。 + + +特性: **传递性** + +这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。 + +![](../../img/Java并发编程实战-课程/Java并发编程实战-课程_2023-03-15-02-10.png) + +从图中,我们可以看到: +- “x=42” Happens-Before 写变量 “v=true” ,这是规则 1 的内容; +- 写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 2 的内容。 +- 根据传递性:“x=42” Happens-Before 读变量“v=true”。 + —— 这意味着:如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是说,线程 B 能看到 “x == 42”。 + +这就是 1.5 版本对 volatile 语义的增强,这个增强意义重大,1.5 版本的并发工具包(java.util.concurrent)就是靠 volatile 语义来搞定可见性的 + +>volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性,其性能往往是优于锁操作的。但是,频繁地访问 volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。 + + +3. **管程中锁的规则** + +对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。 + +那解锁之前的操作肯定也能在后续加锁操作的管程里看到! + +>在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。 + + +**管程**是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。 + +管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。 + +```java +synchronized (this) { //此处自动加锁 + // x是共享变量,初始值=10 + if (this.x < 12) { + this.x = 12; + } +} //此处自动解锁 +``` + + +4. **线程 start() 规则** + +这条是关于线程启动的。 + +它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。 + +如果线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。 + + +**看代码好理解** + +```java +Thread B = new Thread(()->{ + // 主线程调用B.start()之前 + // 所有对共享变量的修改,此处皆可见 + // 此例中,var==77 +}); +// 此处对共享变量var修改 +var = 77; +// 主线程启动子线程 +B.start(); +``` + + +5. **线程 join() 规则** + +这条是关于线程等待的。 + +它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。 + +以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 + + +>所谓的“看到”,指的是对共享变量的操作。 + + +6. **线程中断规则** + +对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。 + +7. **对象终结规则** + +一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。 + + + +**Java内存模型底层怎么实现的?** + + +主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。 + +对于编译器而言,内存屏障将限制它所能做的重排序优化。 + +而对于处理器而言,内存屏障将会导致缓存的刷新操作。 + +>比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。 + + +### 被我们忽视的 final + +**final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。** + +优化错了:问题类似于上一期提到的利用双重检查方法创建单例,构造函数的错误重排导致线程可能看到 final 变量的值会变化。 + +当然了,在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。 + +>**逸出**: 指的是对封装性的破坏。比如对一个对象的操作,通过将这个对象的this赋值给一个外部全局变量,使得这个全局变量可以绕过对象的封装接口直接访问对象中的成员,这就是逸出。 + + +在现实世界里,如果 A 事件是导致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件发生的,这个就是 Happens-Before 语义的现实理解。 + + +在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。 + +>例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。 + + +**Java内存模型底层怎么实现的?** + +主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。 + +对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓。 + +>比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。 \ No newline at end of file diff --git "a/\350\275\257\346\212\200\350\203\275/.DS_Store" "b/\350\275\257\346\212\200\350\203\275/.DS_Store" new file mode 100644 index 0000000..17b304e Binary files /dev/null and "b/\350\275\257\346\212\200\350\203\275/.DS_Store" differ diff --git "a/\350\275\257\346\212\200\350\203\275/\344\272\247\345\223\201/\344\275\240\347\232\204PPT\344\274\232\350\257\264\350\257\235.md" "b/\350\275\257\346\212\200\350\203\275/\344\272\247\345\223\201/\344\275\240\347\232\204PPT\344\274\232\350\257\264\350\257\235.md" new file mode 100644 index 0000000..20ad309 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\344\272\247\345\223\201/\344\275\240\347\232\204PPT\344\274\232\350\257\264\350\257\235.md" @@ -0,0 +1,177 @@ +# 逻辑篇 + +- 参考笔记:http://ntsapps.oa.com/note/main/note-write/note-details?id=75e6e92cb5d446689581f318a48b1ae5 + +## 为什么要做PPT +>借助PPT,更好的展示和表达,更有效的沟通 +> - 信息视觉化 +> - 内容结构化 + +用途: +- 晋升答辩 +- 项目汇报 +- 竞聘演讲 +- 运营手册 +- 产品手册/培训分享/数据分析 + + +## 如何让PPT说话 + +### 打造PPT的外在 + +**你不能这样做:** +- 字太多没图 +- 主次不分明 +- 图表罗列缺少结论 +- 颜色太多太杂 + +>不好的PPT:杂乱繁过 + +好的PPT:**简洁清晰** + + +把握视觉心理学: +- 从上到下,从左到右 +- 先标题后段落:标题可以加粗或者颜色,段落可以靠自己说。 +- 先看图后文本 +- 先看突出部分,再注意次要部分: 段落内部分字加粗/变色 + + +**专业 + 清晰 + 简介 = 漂亮的PPT** +- 专业 + - 统一的背景 + - 专业的模板 + - 正确的使用 +- 清晰 + - 新颖的结构 + - 有力的逻辑 + + +会说话PPT秘笈箴言: +- 1点恶心:错别字等于苍蝇 +- 3色原则:颜色不过3种,字号不过3种 +- 12字箴言:能用图不用表,能用表不要用字 + +### 制作PPT的三大步骤 +1. 情景分析: + - 了解你的听众 + - 明确你的主题 +2. 结构设计 + - 缓缓相扣的页面 + - 提纲:大纲 + - 分段:章节划分 + - 转场:章节斜街 + - 特写:核心内容呈现 + + +### 1.1 目的-要达到什么效果 +- 目的要唯一,不要分散 +- 为听者服务,不要以自我为中心 + +**根据情景分类** +- 汇报一个项目 + - 目的明确 + - 环环相扣 + - 结论清晰 +- 论述一个论点 + - 逻辑清晰 + - 海量佐证 + - 据理力争 +- 推介一个产品 + - 语不惊人死不休 + - 华丽丽的设计 + - 数据支撑 +- 分享一个方法 + - 风格突出 + - 内容丰富 + - 实例辅助 + + +**情景分析**: +- 丈母娘问:小伙子,你谈过几次恋爱? +- 目的:关心你对她女儿是否是认真的,并不真的在意之前谈的情况 +- 回答:简答历史情况,不多不少,重点在表达对现任伴侣的真心 + + +### 1.2 受众-给谁看 +**了解你的听众** +- 领导:先给结论,再分述 +- 评委:围绕你来说,关注你的付出和成绩,而不是项目本身。 +- 客户:关注产品特性/优势等,开门见山 +- 同事:关注内容,有无收获。干货提炼,结合案例。 + + +**做PPT前问自己几个问题** +- 他们对主题了解多少?有多少观点? +- 共识和分歧是什么? +- 以听为主,还是以看为主? + + +### 1.3 主题-想说什么 +根据**应用场景**,找到**关注内容**,抓住**制作要求**。 + +**主题分类**: +- 数据分析报告 + - 关注:数据的指标、**结论** + - 制作要求:符合规范、图表说话 +- 项目进度汇报 + - 关注:进展、困难、计划 + - 制作要求:客观去自述化,有数据证明,关键案例说明 +- 职级答辩 + - 关注:项目成绩、自身优势、不足 + - 制作要求:能力举证、个人规划 +- 技能培训 + - 关注:实际操作、干货 + - 制作要求:结合案例 + + +**情景**:XXX产品业绩review +- 产品的关键指标怎么样 +- 大家关心的问题现在有没有解决 +- 问题是怎么解决的 +- 没解决需要领导做什么 +- 有什么隐藏的问题和风险 + + +### 1.4 场景-在什么样的场合下说 +- **场合** + - PPT是发给别人看?还是现场演讲? + - 谁写谁讲? + - 现场场地多大? + - 多少人听? + - 屏幕大不大? +- **时机** + - 演讲是上午还是下午? + - 午饭前,晚饭前? +- **顺序** +- 有无其他演讲者? + - 上台顺序是否对你有影响? +- **互动** + - 是否需要加入互动? + - 场地、设施、道具等? + + +### 1.5 时间-要说多长时间 + +思考每一页的预定时长 + +示例:部门月会,给GM汇报进度 + + +### 总结输出 + +**通过情景卡片呈现**: +- 目的:汇报进度,展示成果,体现团队价值;预警风险,申请资源 +- 受众:GM,非首次月会,对项目期望**,为达到**目的愿意投资源 +- 主题:月度进展汇报 + 1. 现状?之前的策略是否发挥作用? + 2. 有无风险和问题,团队是否有解决方案? + 3. 请求领导决策下一步,有哪些需要领导支持或决策? +- 场景:会议室、大投影,专项汇报,邮件同步团队 +- 时间:45min(30min汇报,15min答疑) + + +**定下PPT框架**: +1. 目标回顾及关键进展 50%(8P) +2. 项目风险及解决方案 40%(6P) +3. 下一步计划和资源申请 10%(2P) \ No newline at end of file diff --git "a/\350\275\257\346\212\200\350\203\275/\345\206\231\344\275\234/\345\246\202\344\275\225\347\274\226\345\206\231\346\212\200\346\234\257\346\226\207\347\253\240.md" "b/\350\275\257\346\212\200\350\203\275/\345\206\231\344\275\234/\345\246\202\344\275\225\347\274\226\345\206\231\346\212\200\346\234\257\346\226\207\347\253\240.md" new file mode 100644 index 0000000..0d06748 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\206\231\344\275\234/\345\246\202\344\275\225\347\274\226\345\206\231\346\212\200\346\234\257\346\226\207\347\253\240.md" @@ -0,0 +1,233 @@ + + + + + + +# 如何写好一篇文章 + +能尽量在深度和广度上进行拓展,而不是只写问题本身,直到已经到达自己的知识盲区为止。这样一来,可以体现作者对于该领域知识掌握的全面程度,可以提升文章的level。 + +加入一些基于自己实践的理解,而不是纯翻译。能大大提高你的文章的价值,毕竟对于互联网科技来说,获取原始知识的最好途径仍然是阅读英文资料。 + + +## 写好标题 + +1)标题新颖吸引,引入注意,适合技术科普; + +2)标题贴近生活,引起共鸣,适合技术实践; + +3)标题权威专业,适合深度剖析; + + + +而标题的拟定则可以遵循4U公式 + +1)Urgent 紧迫性,给读者一个立即点开的理由;比如,《你的成功在下班后8小时》如果把标题换成《30岁之前学什么,才能避免40岁失业?》从年龄的维度,给读者一种紧迫感 + +2)Unique 独特性,用全新的角度去描述一件事情;比如,《网易运维知识入门方法论》可以换成《2022年开年第一场 -- 网易运维知识方法论》。 + +3)Ultra-Specific 明确性,用明确的语言给读者带去可以确定的预期;比如,《2022年,前端最流行的10个Node框架》通过数字以及“最流行”明确告诉阅读者文章所讲的内容,让阅读者提前有心理预期。 + +4)Useful 实用性,暗示明确的益处,给读者带去价值;比如,《微信事件中心 - 高可靠、高可用的事务消息平台(附大会PPT下载)》在标题中加入“大会PPT下载”几个字之后,读者可以直观地感受到价值,可以拿到一手资料。 + + +### 标题技巧 +- 强调干货型: 从标题上就向读者灌输这篇文章的高质量,例如:《学习XXX,看这一篇就足够了》、《全网最全的XXX,没有之一》。 +- 直戳痛点型: 《用了这么多次,为什么你还是记不住正则表达式》、《工作三年,为什么你只会写CRUD代码》。 +- 蹭热点型: 《从艳照门引发的安全存储思考》、《吴亦凡事件背后的微博优化之路》。 +- 悬念型: 首屏耗时从2秒到0.5秒,我是怎么做到的》、《支持千万人参与的秒杀活动,背后核心技术竟是...》。 +- 反差型: 《10年资深前端,却被这个正则难住了》。 +- 人群标签型: 《写给TS小白的入门教程》、《给PHP初学者的忠告》。 +- 故意否定型: 故意否定读者,激发读者的胜负欲,例如《你真的懂XXX吗》、《你不知道的XXX内幕》。 + + + + + + +## 编写大纲(金字塔原理) + +### 冠上履下 +先声夺人:文章的开头就需要点明主题,让读者一目了然: + +1)这篇文章主张什么观点; + +2)通过这篇文章可以学习到哪些知识; + +3)这篇文章所描述的内容解决了哪些问题; + +4)这篇文章可以带来哪些附加价值; + + + +文末总结:文章的结尾需要总结全文,归纳要素,升华主题,引人思考,加深读者的印象。 + +### 以上统下 +首先需要归纳出一个核心论点,再将核心论点拆分成数个二级论点,每个二级论点又可以进一步细分,形成理论体系,环环相扣,以面覆盖点,以点连接线,把整个文章塑造成一个整体。 + +具体做法可以遵从**MECE原则** + + +“相互独立,完全穷尽”:即对于一个重大的议题,能够做到不重叠、不遗漏的分类,而且能够藉此有效把握问题的核心,并解决问题的方法。 + + +MECE原则有**五种分类法**。 + +- 二分法, 是指可以按照对立方式分类,比如白天和黑夜 + +- 过程法,是指按照事情发展的时间、流程、程序,对信息进行逐一的分类。 + +- 要素法,是指把一个整体分成不同的构成部分,可以是从上到下,从外到内,从整体到局部。 比如一个小区包括几栋楼,一栋楼包括几层。 + +- 公式法,可以按照公式设计的要素去分类,比如销售额=流量×转化率×客单价×复购率 + +- 矩阵法,通过二维矩阵的方式来分类,比如时间管理中把你的工作分成以重要紧急、重要不紧急、不重要但紧急、不重要也不紧急。然后可以把它们填到4个象限当中去,这种分类方式就叫做矩阵法。 + + +### 承上启下 +技术文章中论点与论点之间,段落与段落之间,需要有合理的过渡,避免出现逻断层或者表达断层,造成读者的不适。 + + +## 写作流程 + +### 写草稿 +先把『字』写出来,这一步应该不难,然后不断修改完善: + +1. 把素材放入对应章节, +2. 用自己语言简单组织文章 + + +润色: +1. 仔细阅读,对内容修改完善 +2. 增加承上启下的文字描述 + + +**增加配图** +1. 增加对应的图表、流程图、架构图 +2. 增加对应的插图、辅助表达 + + +**审稿**: +1. 发给同事朋友看,征询修改建议 +2. 发给审稿人 +3. 发给专业人士 + + +**发布** + + + +## 写作技巧 + +- 从写开始: 只要你提笔开始写,用自己熟悉的方式,**像聊天一样写作**,发现并没那么难,期间遇到问题再解决,就能逐步提升写作技巧。 + + +- 模仿: 在写文章的领域,可以从模仿你喜欢的作者、模仿你喜欢的写作风格开始,这是起步最快的方法,很多大V都是从模仿开始写作的。 + + +- 滑梯理论: 读了第一句之后,就想接着读第二句,读了第二句之后,就想接着读第三句。坚持这样的刻意练习,我们文章的可读性就会提升很大一块。 + + +- 故事开头或痛点吸引: + + +- 打磨意识: + + +### 一图胜千言 +大部分人都是先看图再看字——哪怕图里只有字。 + +1. 增加架构图,推荐使用:https://excalidraw.com/ +2. 增加图表,增强可信度。 查外网收集资料数据来源。 +3. 插入漫画或者表情包,增加趣味性 + 1. [漫画制作工具](https://www.gaoding.com/) + 2. [表情包制作工具](https://www.wakatool.com/maker) + + +### 工具分享 +1. 源码处理格式化: https://carbon.now.sh/ +1. 封面生成工具: + 1. https://pixabay.com/ + 2. https://www.pexels.com/zh-cn/discover/ + + + +## 写作规范 + +### 标题 + +原则: + +1、一级标题下,不能直接出现三级标题; + +2、标题要避免孤立编号(即同级标题只有一个); + +3、下级标题不重复上一级标题的名字; + +4、谨慎使用四级标题,尽量避免出现,保持层级的简单,防止出现过于复杂的章节; + + +### 文本 +参考:https://github.com/ruanyf/document-style-guide + +**字间距** + + +全角中文字符与半角英文字符之间,应有一个半角空格 + +全角中文字符与半角阿拉伯数字之间,有没有半角空格都可,但必须保证风格统一,不能两种风格混杂。 + +半角英文字符和半角阿拉伯数字,与全角标点符号之间不留空格。 + +**句子** + + +避免使用长句。 + +尽量使用简单句和并列句,避免使用复合句。 + +避免使用双重否定句。 + + +**段落** + +1、一个段落只能有一个主题,或一个中心句子; + +2、段落的中心句子放在段首,对全段内容进行概述。后面陈述的句子为核心句服务; + +3、一个段落的长度不能超过七行,最佳段落长度小于等于四行; + +4、段落的句子语气要使用陈述和肯定语气,避免使用感叹语气; + +5、段落之间使用一个空行隔开; + +6、段落开头不要留出空白字符; + + +## 写作案例分享 + +### 技术介绍型文章 + +这一类文章,往往是是偏向于技术科普,如《web3是互联网的未来?》、《VR、AR、MR、XR分不清楚》等。 + +它们的共同特点是将新技术讲清楚,让读者对文章描述的技术内容有全面的了解,一般包含以下内容: + +1)新技术产生背景 + +2)与现有技术对比 + +3)举例说明新技术的特点 + +4)介绍新技术的实践 + +5)总结其特点,并阐述其未来走向 + + +### 问题定位型文章 + +1. 问题介绍 +2. 针对该问题的常见原因分析,给出猜想 +3. 问题分析及排查过程 +4. 总结 + diff --git "a/\350\275\257\346\212\200\350\203\275/\345\244\204\344\272\213/\343\200\212\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\343\200\213\350\257\276\347\250\213\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/\350\275\257\346\212\200\350\203\275/\345\244\204\344\272\213/\343\200\212\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\343\200\213\350\257\276\347\250\213\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..c2f5749 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\244\204\344\272\213/\343\200\212\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\343\200\213\350\257\276\347\250\213\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,100 @@ + +# 引言 + +**学会如何工作,和学习技术同等重要** + + +**以利益为视角,以换位思考为手段,以现实例子为场景来给你讲。** + + +- 以利益为视角 + - 弄清楚是事情背后的“原动力” + - 除了看得见的金钱,我们可以从工作中获取的“钱”还有很多,比如升职、学习、实战的积累、好的成长机会、可复用的资源、公司内外的声誉 +- 换位思考为手段 + - 换位思考可以帮助我们自己更高效地工作,因为它能有效地帮我们避免一些麻烦,在做事情之前预知随之而来的后果。 +- 现实的场景为例子 + - 开始讲之前,你可以去想想,如果你遇到了这个问题,你是怎么想的,你会怎么解决. + - 再看看我的做法,是不是和你的一样。这样对比的过程,你慢慢就能明白,我们的思考问题的方式,有啥不一样。 + + + + +--- +# 职业素养 +>如何培养良好的工作习惯 + +- 你以为邮件仅仅只是发送信息的吗? +- 邮件的背后有什么样的工作逻辑? +- 来自领导和部门的任务那么多,我们要如何根据任务的性质划分出它们的重要性? + + +## 安排工作优先级 + +在工作中,事情的优先级的标准,是要让公司受益,让老板满意,让同事认可 + + +## 学会使用邮件 + + +## 一定要多说多做 + + +## 要把控好责任的边界 + + +--- +# 职业选择 +>工作选择,以及与领导之间的关系 + +- 如何选择和自己“聊得来”的领导? +- 他们都有怎样的特质? +- 在此基础上,如何选择心仪的公司? +- 你是更适合创业公司还是更适合成熟的大公司呢? + + +## 如何看待与领导关系 + + +## 面试要准备什么 + + +## 如何看待外包和外派 + + +## 如何进行职业规划 + + +--- +# 职场情商 +>职场政治 + +- 为啥领导更喜欢他?工作能力也许是一个原因,但绝不是唯一的原因,在这背后,个人的输出、看问题的方式和立场、工作的方式也同样重要。 + + +## 怎样才能升职加薪 + + +## 职场政治与你有关吗 + + +## 以正确的姿势加班 + + +--- +# 技术相关 + +- 在提升技术能力的道路上,我们需要注意哪些坑? +- 技术是一种手段,也是一种观念,技术观为何如此重要? +- 它能指导我们什么? + + +## 让技术为自己提供舒适区 + + +## 培养正确的技术观 + + +## 成为系统架构师 + + +## 搞定系统集成 \ No newline at end of file diff --git "a/\350\275\257\346\212\200\350\203\275/\345\244\204\344\272\213/\344\270\200\344\272\233\346\234\211\345\220\257\345\217\221\347\232\204\351\227\256\351\242\230.md" "b/\350\275\257\346\212\200\350\203\275/\345\244\204\344\272\213/\344\270\200\344\272\233\346\234\211\345\220\257\345\217\221\347\232\204\351\227\256\351\242\230.md" new file mode 100644 index 0000000..45a56ef --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\244\204\344\272\213/\344\270\200\344\272\233\346\234\211\345\220\257\345\217\221\347\232\204\351\227\256\351\242\230.md" @@ -0,0 +1,83 @@ + + +## X-Y问题 + +对于X-Y Problem的意思如下: + +1)有人想解决问题X +2)他觉得Y可能是解决X问题的方法 +3)但是他不知道Y应该怎么做 +4)于是他去问别人Y应该怎么做? + +简而言之,没有去问怎么解决问题X,而是去问解决方案Y应该怎么去实现和操作。于是乎: + +1)热心的人们帮助并告诉这个人Y应该怎么搞,但是大家都觉得Y这个方案有点怪异。 +2)在经过大量地讨论和浪费了大量的时间后,热心的人终于明白了原始的问题X是怎么一回事。 +3)于是大家都发现,Y根本就不是用来解决X的合适的方案。 + +X-Y Problem最大的严重的问题就是:在一个根本错误的方向上浪费他人大量的时间和精力! + + +>你试图做X,并想到了用Y方案(甚至你觉得Y是最好搞定X的方法。)。所以你去问别人Y,但根本不提X。于是,你可以会错过本来可能有更好更适合的方案,除非你告诉大家X是什么。 + + +X-Y Problem又叫“过早下结论”:提问者其实并不非常清楚想要解决的X问题,他猜测用Y可以搞定,于是他问大家如何实现Y。 + + +**变种**: + +- 很多团队Leader都喜欢制造信息不平等,并不告诉团队某个事情的来由,掩盖X,而直接把要做的Y告诉团队,导致团队并不真正地理解,而产生了很多时间和经历的浪费。 + + +- 本来我们想达成的X是做出更好和更有价值的产品,但最终走到了Y:通过各种手段提升安装量,点击量,在线量,用户量来衡量。 +- 大多数人有时候,非常容易把手段当目的,他们会用自己所喜欢的技术和方法来反推用户的需求,于是很有可能就会出现X-Y Problem – 也许解决用户需求最适合的技术方案是PC,但是我们要让他们用手机。 + +- 对于个人的职业发展,X是成长为有更强的技能和能力,这个可以拥有比别人更强的竞争力,从而可以有更好的报酬,但确走向了Y:全身心地追逐KPI。 + + +- 产品经理有时候并不清楚他想解决的用户需求是什么,于是他觉得可能开发Y的功能能够满足用户,于是他提出了Y的需求让技术人员去做,但那根本不是解决X问题的最佳方案。 + + +- 因为公司或部门的一些战略安排,业务部门设计了相关的业务规划,然后这些业务规划更多的是公司想要的Y,而不是解决用户的X问题。 + + +## 你会问问题吗? +参考链接: +- 摘要提取:https://coolshell.cn/articles/3713.html +- 宝藏原文:https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md + + +## 问题 + +- 问的问题太简单了甚至太白痴了,比如你自己试一试或是读读文档就知道了的问题,或是问这个问题直接表明了你的无知或是懒惰。这种问题会相当影响别人对你的印象。 + + + +### 办法 + +- 提问前先自己尝试查找答案,读读文档、手册,看看有没有相似的问题,看看那些方法能不能帮你解决问题,自己去试一试。如果你是程序员,你应该先学会自己调查一下源代码。。我有时候很不愿意回答这样的问题,因为我觉得问问题的人把我当成了他的小跟班了。 +- 提问的时候,找正确的人或是正确的论坛发问。向陌生人或是不负责的人提问可能会是很危险的。不正确的人,会让你事倍功半。 +- 问的问题一定要是很明确的,并且阐述**你做了哪些尝试**,你一定要简化你的问题,这样可以让你的问题更容易被回答。对于一些问题,**最好提供最小化的重现问题的步骤**。 +- 你一定要让问题变得**简单易读**,这和写代码是一样的。只有简单易读的邮件,人们才会去读,试想看到**一封巨大无比的邮件,读邮件的心情都没有了**。而且,内容越多,可能越容易让人理解错了。 + +- 你问问题的态度应该是以一种讨论的态度,即不是低三下四,也不是没有底气。只有这样,你和你的问题才能真正被人看得起。要达到这个状态,不想让别人看不起你,你就一定需要自己去做好充足的调查。 + >问题 问得好的话,其实会让人觉得你很有经验的,能想到别人想不到的地方。 + +- 不要过早下结论。 + - 比如:“我这边的程序不转了,我觉得是你那边的问题,你什么时候能fix?”,或是“太难调试了,gdb怎么这么烂?!”。 + >当你这么做的时候,你一定要有足够的信息和证据,否则,你就显得很自大。 +- 好的问题应该是: + - “我和你的接口的程序有问题,我输入了这样的合法的参数,但是XX函数却总是返回失败,我们能一起看看吗?” + - “我看了一下gdb的文档,发现我在用XXX命令调试YYY的时候,有这样ZZZ的问题,是不是我哪里做错了?” + + + + +## 人生的意义? + +学徒(赚钱的手艺)->发展(可能不是自己孩时最喜欢的)-> 找回学徒时期丢失的兴趣-> + + +1. 一旦你通过了艰难的学徒训练期,你被限制、被束缚,但同时也得到了发展。 你走过荆棘,面前就是康庄大道,你的人生将拥有更多崭新的机遇。 +2. 荣格认为: 人后半生的正确发展道路,是重新找回在学徒时遗落的自己。 这样你既拥有了成就,又能重新获得发展的潜能。 +3. \ No newline at end of file diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/.DS_Store" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/.DS_Store" new file mode 100644 index 0000000..5008ddf Binary files /dev/null and "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/.DS_Store" differ diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/Up\351\242\230\347\233\256\345\255\246\344\271\240.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/Up\351\242\230\347\233\256\345\255\246\344\271\240.md" new file mode 100644 index 0000000..5346163 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/Up\351\242\230\347\233\256\345\255\246\344\271\240.md" @@ -0,0 +1,80 @@ + +## Java + +### 基础 +- 内存泄漏 +- volatile +- 死锁,悲观锁乐观锁 +- gc +- 双亲委派 +- hashMap +- + +### 设计模式 +- 项目用到的设计模式 + + +## GO + + + + +## 网络 +- http和https的区别 +- TCP如何实现可靠传输 +- 浏览器输入url发生了什么 +- cookie、session 的详细问题 + + + +## 操作系统 +- 进程和线程 + + +## 数据库 + + + + +## 分布式 + + +# 工作&软技能 +- 如果排期只有一周的任务,但是以你的能力需要两周才能完成,你会怎么办? + + +# 项目 +- 项目的简要介绍,以及重难点如何攻坚(谈的比较久) + + +# 算法题 + +- 有重复项数组的所有排列(现场给题,可以不成功但要体现思路!! +- leetcode 373. 查找和最小的 K 对数 +- 查找和最大的K对,是一道leetcode中等难度。算是经典的多路归并问题。 + - 类似的题目可以学习一下多路归并: + + 264. 丑数 II + + 313. 超级丑数 + + 373. 查找和最小的K对数字 + + 632. 最小区间 + + 719. 找出第 k 小的距离对 + + 786. 第 K 个最小的素数分数 + + 1439. 有序矩阵中的第 k 个最小数组和 + + 1508. 子数组和排序后的区间和 + + 1675. 数组的最小偏移量 + +- 多叉树的所有子节点之和 +- hard 《无重复字符的最长字串》 +- 计算两个链表倒序相加的,比较简单 +- medium 难度 :链表排序(要求空间复杂度为常量),我用插入排序写出来了,面试官让我再用归并排序写,我稍微讲了一下思路,没写出来 +- LeetCode hard 难度:n 皇后问题 + diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/pin\345\233\260\346\210\267.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/pin\345\233\260\346\210\267.md" new file mode 100644 index 0000000..6db91a8 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/pin\345\233\260\346\210\267.md" @@ -0,0 +1,13 @@ + + + +我是赵家明的儿子,不好意思快过年了打扰您,我这一回来就去医院照顾我妈了,一直没得空,今天刚刚接我妈回来过年,初三又要继续去医院做放疗。 + +我这次打电话主要是想咨询想关于我家目前是属于脱贫户的事情,因为现在我妈得了这个疾病,花销比较大,后续也不知道还要投入多少,我了解到建档立卡户是有相关的医疗报销优惠的,想问下我们家2023年是否还有这个政策覆盖,想减少一些压力,也让我妈别总想放弃治疗。 + +如果没有了,是否可以根据这种大病重新申请一些帮扶措施,感觉快返贫,卖房子了。。 +现在房子还有一堆房贷,也不好卖,不知道收回来的钱够不够,又担心我妈苦了一辈子为了这个家,因为她得这个病把好不容易一家子凑的房子卖掉,我怕她心理过不去,治疗起来不配合或者不积极。 + +我看目前我们家还属于脱贫户,不知道是否还能享受一些医疗报销比率的优惠政策, + +脱贫户是建档立卡户? \ No newline at end of file diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\343\200\212\347\237\245\350\257\206\347\256\241\347\220\206\350\256\262\345\272\247\343\200\213.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\343\200\212\347\237\245\350\257\206\347\256\241\347\220\206\350\256\262\345\272\247\343\200\213.md" new file mode 100644 index 0000000..585f4b0 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\343\200\212\347\237\245\350\257\206\347\256\241\347\220\206\350\256\262\345\272\247\343\200\213.md" @@ -0,0 +1,210 @@ +## IPO + +- Input + - 每天听书1-3本,不限主题 + - 深度研究主题 + - 养成习惯:每日整理 +- Process + - 听书900本书 + - 笔记 +- Output + - 目标是出书 + - 做讲座 + + + +## 为什么焦虑 + +35岁危机焦虑? + +- 大脑缺陷 + - 记忆下降 + - 不爱思考,偏爱懒惰 + - 快慢思考 +- 知识大爆炸 + - 信息过载 + - 碎片化 + - 稀缺性如何体现? + + +**怎么解决?** +- 电脑借助外力: 硬盘加大,APP筛选更好 +- 大脑: 存储up、提取up + + + +**如何做才能成功?** +- 逆向思维: 我不赌博不吸毒不得病。。。。然后就能成功 + + + +## 知识管理三座大山 + +### 自大 +**信息茧房** + +- 改变态度: 打开大脑 + + +### 松鼠病 +**信息过载**: 上百本书,各种教程,购买了就是自己的? + +- 认知改变: 处理知识 + + +### 懒癌 +缺乏持续执行力 + + +- 行为改变: + +## 解决办法 + +**做到知行合一**? +心力、体力、脑力。 合一 + + +**道术器** +- 道:为啥 +- 术是核心:方法论 + - 系统论: 要素 + **连接** + 要素 => 目标 + - 动态的:反馈? + - GTD + - 收集、处理、归类、复盘、分类执行 + - 时间管理和知识管理结合 + - 清空大脑 + - 卡片笔记法 + - 清空大脑 + - 建立有序的只是卡片: 永久笔记、闪念笔记、文献笔记、项目笔记 + - 可操作性强 +- 器:工具 + + +## 输出产品 O + +知识管理五步法: +- 问 +- 集 +- 理 +- 享 +- 思 + + + +**准备** +- 耐心 +- 必要难度理论:不整理的知识都可以删除 +- 全平台全场景知识 +- xx + + + +**问**:打开心扉 +- 问题比答案更重要 + - 好奇心:多为为什么 + - 敬畏心: 知识的边界,能力圈: 热爱、擅长、赚钱的 + - 耐心: 复利骗局? 1.01*0.99*1.01.。。。。。最后是多少? + + + +**集**: +- 区分知识和信息: + - why: 智慧 + - how: 知识 + - what、when、where: 信息 + + +**读母书**: 胜过读100本子书 + +- 包围收集 +- 快读慢读,听书(摘出来的) + + + +印象笔记 +- 企业微信 机器人? + + +关掉不看的公众号,5个未读 +屏蔽质量不高的朋友圈 +搜索为主,排行版次之 + +para体系? + + +## 理 + +做T型人才: +- 攻克一个好领域,再攻克另外一个领域 + + + +5+2能力: +- 阅读 +- 演讲 +- 写作 +- 时间管理 + +产品设计和数据分析 + +**标签** +分类 + + +## 输出 +写、讲、分享 + +印象笔记 +B站 +公众号 +演讲: 自华读书会、行家课程 + + +**行动力**: +暗示、简单行为、奖励 + + +建立微习惯: +- 每天5分钟,然后打开读书笔记 +- 穿上跑鞋,运动概率大 +- 手机首屏全是学习的 +- 茶杯放在做寿险 + + +**找小伙伴** +- 读书会 +- 跟着Up主 + + +**复盘**: +- 行动 +- 经验 +- 规律: 别人的经验、自己的经验 +- 反馈到行动 + + +**原则+执行** +找到验证的原则规律,持续执行 + + + +**建立自己的点线面** +- 点 + - 终生学习 + - 高绩效工作 + - 生活心智成熟 +- 线 + - 中庸 + - 知行合一 + - 归纳演绎 +- 层次 + - 三个阶段,三点成面 + +- 选择 + - 四象限 + - 权衡 + + +## 总结 + + diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\344\270\252\344\272\272\346\231\213\345\215\207\346\214\207\345\214\227.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\344\270\252\344\272\272\346\231\213\345\215\207\346\214\207\345\214\227.md" new file mode 100644 index 0000000..d07ef39 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\344\270\252\344\272\272\346\231\213\345\215\207\346\214\207\345\214\227.md" @@ -0,0 +1,70 @@ + +# 晋升体系 + +## 简单分下类 +1. 职级体系划分为**专业线**和**管理线**。 +2. 专业线按照设计特点划分为两类:**跨越式职级**和**阶梯式职级**。 +3. 管理线一般不会划分领域,一般是专业线达到一定级别才转管理(比如阿里到P9后才能转管理),需要管理者也懂一些专业知识,避免外行管内行。双通道发展模式被证明存在问题:比如投入大、不好评估员工能力、外行管内行。 + +### **跨越式职级**: +阿里、百度、滴滴、头条。 级别鸿沟。 + +以阿里职级体系为例: +- P5应届生 高工 +- P6 资深 M1主管 +- P7 高级技术专家 M2经理 +- P8 高级技术专家 M3高级经理 +- P9 资深技术专家 M4总监 +- P10 研究员 M5 资深总监 +- P11 高级研究员 M6副总裁 + +**特点**: +1. 第一个特征是,相邻两个级别的差异比较大。 因此,晋级的时候不是简单地要求能力“有提升”就可以了,而是要求有“本质的提升”。 +- 晋升大级别时,提升要有明显的差别,能力要获得质的提升。 +>比如腾讯8升9是从工程师变为高级工程师,11升12是高工变为专家,14升15是专家变资深专家,17是权威专家。 这些大级别不在是简单的积累年限或者普通的提升,所以会比较难晋升。 +2. 晋升机会少 +3. 同级别回报差异大 + +### 阶梯式职级 +腾讯, 5/6/7/8 + +1. 相邻级别差异小(某些大级别除外) +2. 晋升机会多 +3. 同级别回报差异小 +- 核心缺点是:很难客观定义和评估两个等级之间的差异。 也有级别鸿沟。 + +## 晋升流程 +1. 提名、报名 + - 满足硬性条件: 绩效条件、年限条件、红线、附加(影响力、培训) + - 能力是否达到要求,主管负责判断审核你是否满足晋升条件 + - 如果你觉得自己可以晋升,主管不认可,你就需要主动与主管真诚沟通,听一下真实评价,如果确实有充分理由判断自己能力不足,要请主管给出**明确的指导意见**,以及后续**有针对性的工作安排**。 + - **主管也不是很明确的情况**,不太好判断:怕太严格,其他组能力差不多的提了过了,你没机会;怕太送,后续答辩拉胯,影响团队声誉。 + >主动跟他提晋升想法,表达积极进取的意愿和规划,最好是有绩效综述。 如果最后报名的人少也许可以补位,就算超额,下次晋升也会优先考虑你。 +2. 预审 + - 目的:针对提名晋级名单进行一次。部门内的横向拉通对比。 + - 防止主管放水,提名太多 + - 防止主管之间能力评价标准相差太远 + - 预审方式 + - 书面预审,级别低的。通过提名材料来审核。 管理者直接查看材料关于你能力和项目的描述,在结合自己平时对你的了解来评估。 **所以,提名材料写得好不好就很关键了。** + - 管理者会议预审,让其他主管一起审核,主管介绍自己提名的员工,接收其他人的质询。**因此,各个主管对你的了解程度就很关键。** +4. 评审: 最关键,高级别需要答辩。 + - 需要向评委团展现自己的能力,并接收他们的考察。 + - 主要环节: 准备答辩PPT--晋升自述、展现亮点--回答评委问题--评委判断,评价能力--综合评委意见,确定结果 + - 评审:**集体讨论**,有熟人帮说好坏话,对公平性有点影响;**独立投票**, 2/3通过啥的,看名额多少。 +6. 复审: 通过总体数据,和晋升通过率,确定过关人数。 有大小年之分 +8. 审批: 高层审批,确定涨薪幅度和激励啥的 +9. 沟通: 主管和HR反馈结果,通过和不通过都会沟通。 + +> 自己参与前三个阶段, 前四个阶段都可能被刷下来。 +> 运气: 评审团不确定性,公司通过率指标不确定性,部门通过率调控不确定性 +> 对其他主管客气点,跟合作伙伴留好影响。 + +# 职级详解 + +# 学习方法 + +# 做事方法 + +# 专项提升 + +# 答辩面试技巧 \ No newline at end of file diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206-\351\241\271\347\233\256\345\200\222\345\272\217+\347\262\276\347\256\2001.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206-\351\241\271\347\233\256\345\200\222\345\272\217+\347\262\276\347\256\2001.md" new file mode 100644 index 0000000..f32481c --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206-\351\241\271\347\233\256\345\200\222\345\272\217+\347\262\276\347\256\2001.md" @@ -0,0 +1,74 @@ +# 我的简历 + +## 联系方式 +赵锦波 18074844827 fengziboboy@126.com +微信:fengziboboy + +## 工作&教育 +2019.7-**至今** 腾讯AMS/搜狗 搜索广告平台开发组 后端开发 + +2016-2019 西安电子科技大学 | 计算机技术 | 研究生 +2011-2015 中国矿业大学 | 计算机科学与技术 | 本科 + +## 综合评价 +1. 丰富的Java开发经验(熟悉集合、多线程、JVM、GC、Spring),了解Go语言。 +2. 熟悉MySQL(索引)、Zookeeper,了解Flink、Kafka、Hadoop等工具。 +3. 熟悉Solr全文检索引擎(SolrCloud集群部署、查询),了解Elasticsearch。 +4. 了解分布式服务架构,具有高可用、可伸缩分布式架构的系统开发经验。 +5. 有良好的编程习惯,注重代码质量。了解基本的OOP、DDD、TDD思想,熟悉单例、策略、责任链等设计模式。 +6. 有3年半的搜索广告投放平台开发经验(商品库、广告物料检索)。 + + +## 项目经验 + +##### 项目一:大规模物料检索系统3.0-迁移升级&高可用优化 | 核心开发/模块负责人 | 2022.06-2023.02 + + +【**项目描述**】 +将搜索服务中台迁移到腾讯AMS广告投放体系中,完成业务场景梳理、技术选型、方案设计,开发重点包括倒排索引引擎、增量实时通路、全量离线通路、查询通路等核心模块。 同时,针对延迟、QPS、吞吐等各项指标进行持续优化,保障系统高可用;通过实时监控、数据对账、降级查库等策略保证业务数据一致性。 + +【**个人工作**】 +- 负责重构**高可用**查询网关服务,自定义DSL解析查询,支持多层嵌套、函数查询等复杂筛选场景,使接口通用化,并完成trpc协议改写,为查询请求提供鉴权、路由、**细粒度降级**等核心服务治理功能,实现秒级返回。 +- 负责倒排索引引擎的搭建和维护:增加solr&zk双重鉴权,重新搭建solrCloud集群,完成离线加载功能迁移改造。 +- 负责关键词倒排索引平台查询性能优化(solrCloud分布式查询优化),查询高可用、数据一致性保证和快速恢复功能实现。 + +【**难点收获**】 +- **查询高可用**:提供双集群热备,面对单机群宕机、增量延迟、超时等多场景支持自动路由切换,保障服务可用。 支持**账户维度**实现分场景的**细粒度查询降**级。平台整体可用性 >=99.95%。 +- **数据一致性**:实现定时端到端数据对账,可感知15分钟内的数据不一致,并上报不一致数据用于数据修复。 +- **收获**:作为分布式物料检索系统的核心成员,负责通用化的查询层和定制化的底层引擎层的设计和开发,积累了较丰富的分布式系统开发经验,理解分布式架构的高可用性与可扩展性。 + + +##### 项目二:大规模物料检索系统2.0-搜索服务中台 | 核心开发/模块负责人 | 2021.01-2022.04 + + +【**项目描述**】 +搜索服务中台是CQRS(类读写分离)**分布式架构**的全文检索系统(**Solr**引擎),为搜索广告中关键词、创意、商品等近百亿物料的**多维**筛选场景,提供**跨层级多条件**组合筛选**实时**检索服务,支持全文检索。同时将该系统**平台化**,支持多业务各类物料形式低成本**快速**接入,已对接4部门7个业务场景。 + +【**个人工作**】 +- 负责查询接口层的**重构**,主要包括对接业务需求,引入spring-data-solr完成接口**通用化易用性**设计和开发,降低开发新接口的人力成本。 +- 负责索引在线**分布式加载**模块的实现(索引合并、分布式协调索引加载),以及**底层检索引擎**solrCloud模块升级。 +- 负责集群近百台机器的部署维护,搭建监控指标,处理报警,实现solr集群故障由月均1次降为半年**0故障**。 + +【**难点收获**】 +- 积累了一定的分布式系统问题排查经验, 比如: 通过zk同步通知,解决索引分布式多shard异步加载过程数据一致性问题; 通过限速、随机分流等机制解决高并发下加载偶发失败问题。 + +- 针对索引加载整体重试耗时长的问题,通过**幂等设计**实现分shard分副本的细粒度加载能力,**降低**重试加载60%的耗时。 + +- 通过对solrCloud分布式自主恢复功能的理解,主动编写Shell脚本调用定制接口,增加重试超时机制,实现自动滚动部署不中断,**减少**人工干预成本,将全集群部署耗时减少2/3(9小时->3小时)。 + +##### 项目三 | 商品库/快投系统迭代开发 | 核心开发/模块负责人 | 2019.07-2021.06 + +【**项目描述**】 +商品库是一站式的商品广告物料对接、存储、管理和输出的平台,可整合对接到广告投放平台,支持直接使用商品物料进行搜索广告投放。 + +【**个人工作**】 +- 负责商品库、商品集合系统的功能迭代和维护,独立完成商品直投、商品筛选等核心项目,有效支持商品广告投放,消耗占大盘比从1/7提升到1/5。 + +- 负责快投、直投相关数据报告/样式的开发和对外(大客户)API接口开发,从新人成长为模块owner。 + +【**难点收获**】 +- 主动跟踪慢查询定位查询瓶颈,通过SQL索引优化、异步并发、业务定制化,将接口耗时从12s降低到3s,提升用户体验。 + +- 针对商品定时筛选任务接口,平均耗时分钟级、系统卡顿问题,通过增加jvm、性能指标监控,分析内存压力大、频繁FullGC问题,优化代码并行化处理,将平均耗时从**5分钟降低到<2s**。 + +- **收获**:积累了MySQL索引、**GC内存优化**经验,锻炼了独立排查问题能力,形成了数据驱动思维和产品思维,可独立负责中型系统的迭代和维护。 diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206.md" new file mode 100644 index 0000000..3752339 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206.md" @@ -0,0 +1,165 @@ + +# 我的简历 + +## 联系方式 +赵锦波 30岁 +手机:18074844827 +Email:fengziboboy@126.com +微信:fengziboboy + +## 个人信息 +2019.7-至今 腾讯AMS/搜狗 搜索广告平台开发组 后端开发 + +2016-2019 西安电子科技大学 计算机技术 研究生 +2011-2015 中国矿业大学 计算机科学与技术 本科 + +## 项目经验 + +项目一: 商品库/快投/商品直投系统迭代开发 核心开发/模块负责人 2019.07-2021.06 + +* 具体功能 +* 技术难点 + - 通过慢查询定位 MySQL 数据库查询瓶颈,通过 SQL 优化以及修改索引将查询时间从 1200ms 降低到 40ms + - 解决查询慢,GC,内存耗费高 + - 填充率? 判空操作? 数据倾斜 + - 大客户分片,查询没有排序操作,是不受影响的 +* 效果如何 + - 首页查询耗时从10多s优化到3秒以内。 + - 大规模数据量,对内存的优化,接口耗时优化。 + +项目二: 大规模物料检索系统-第二代重构项目 核心开发/模块负责人 2021.1-2021.12 + +为搜索广告管理关键词列表查询提供跨层级的多条件组合筛选实时检索服务(查询维度包括计划、组、关键词),支持关键词全文检索。 + +技术目标 +●支持百亿级关键词 +●数据同步时延p99小于1s +●实时检索查询平均响应时间<100ms +●并发更新TPS > 3w, 并发查询QPS > 100? +服务可用性 > 99.99%,半小时窗口内数据不一致条数小于10。分级别报警机制(不一致条数>10发群提示,>100提示告警;>512严重告警),分钟级修复。 + +- 有项目云服务化或者相关开发以及项目调优,部署经验。熟悉大规模、高并发系统架构设计,能独立完成系统的设计及开发。 + +- Java内存管理,看看是不是可以减少一些虚拟机上内存的压力。 +- 分布式冲突问题解决。 错峰, 削峰 +- 高并发压力 +- 查询框架、不通用。 +- 分布式系统部署,运维,替换机器,下线机器 + - 分布式,高并发问题 + - 上百台机器如何安全的自动化部署? + +项目三: 大规模物料检索系统-第三代迁移&高可用优化 核心开发/模块负责人 + +作为软件开发团队的核心成员,为大数据应用设计开发高质量的软件平台。 +对微服务架构组件有实践经验,理解常见架构的高可用性与可扩展性。 + +- 查询高可用、数据一致性保证、数据对账修复 + + +## 专业技能 +熟练使用Java(集合、多线程、JVM、GC),了解Go(集合、协程)等编程语言。 +熟悉使用Spring、SpringBoot开发框架。 +熟悉MySQL(索引)、Zookeeper,了解Flink、Hadoop、Hive等工具。 +熟悉Solr检索引擎(SolrCloud集群部署、查询),了解Elasticsearch。 +了解高可用、高并发,高负载的架构,具有分布式架构的系统开发经验。 +了解单例、策略、责任链等设计模式,有良好的编程规范和数据驱动思维。 +了解Linux系统基本操作、Shell编写和常用命令。 +了解网络基础知识(tcp/ip,http, https, UDP等)以及运行机制。 + + + + +## 自我介绍&&其他信息 +有良好的代码编程习惯,注重代码质量,了解OOP、DDD、TDD等编程思想。 +在独立负责小型项目、大中型项目迭代经验,在性能优化,疑难问题排查等方面有一定攻坚能力。 +具有一定的文档编写、画图能力 +性格开朗,好相处,喜欢旅行,会点摄影。具有良好的沟通、协作能力能力。 + + + +--- + +# 如何写好简历 + +熟悉(推荐使用)> 掌握(推荐使用)> 了解(推荐使用) + +所以简历上写着熟悉哪一门语言,在准备面试的时候重点准备,其他语言几乎可以不用看了,面试官在面试中通常只会考察一门编程语言。 + +不要为了简历上看上去很丰富,就写很多内容上去,内容越多,面试中考点就越多。 + + +**项目** + +项目经验中要突出自己的贡献,不要描述一遍项目就完事,要突出自己的贡献,是添加了哪些功能,还是优化了那些性能指数,最后再说说受益怎么样。 + +例如这个功能被多少人使用,例如性能提升了多少倍。 + +首先是做项目的时候时刻保持着对难点的敏感程度,很多我们费尽周折解决了一个问题,然后自己也不做记录,就忘掉了,此时如果及时将自己的思考过程记录下来,就是面试中的重要素材,养成这样的习惯非常重要。 + + + +项目经验是面试官一定会问的,那么不是每一个面试都是主动问项目中有哪些亮点或者难点,这时候就需要我们自己主动去说自己项目中的难点。 + + + +建议【项目经验】分 「项目描述」「个人工作」「个人收获」这三块来写,不要堆在一起。例如这样: +如果能写一写「项目难点」,那就更好了。(如果实在写不出「项目难点」,那就写「个人收获」就好) + +尽可能把项目的全貌和自己的理解展示出来。 项目中的技术栈可写可不写 + +不写 「技术栈」的话,就在 「个人收获」 写一写自己使用某一技术的心得,这样把面试问题缩小到自己可以把控的点上。 + +其实面试官问的问题,基本集中在 「项目难点」 和 「个人收获」上。 + +你的 「项目难点」「个人收获」写了啥,面试官大概率就会问啥,所以建议大家吃透项目中的一两个技术点就够了,然后在「项目难点」 和 「个人收获」上重点写自己吃透的技术点。 + + +我在此项目负责了哪些工作,分别在哪些地方做得出色/和别人不一样/成长快,这个项目中,我最困难的问题是什么,我采取了什么措施,最后结果如何。这个项目中,我最自豪的技术细节是什么,为什么,实施前和实施后的数据对比如何,同事和领导对此的反应如何。 + +独自负责客户端从无到有的产品设计,研发,流程图及开发文档, + + + +- 善于发现以及解决问题,持续改进 XXX 系统的架构和核心技术,保证系统的稳定性、高性能、高可用性和可扩展性; +- 善于利用工具和代码减少重复性劳动,开发了 XXX 工具提高团队的工作效率。 +- 能持续的关注和优化项目。 +- 主动学习 + + +快投-直投-商品库 + + +CQRS 是“命令查询责任分离”(Command Query Responsibility Segregation)的缩写。在基于 CQRS 的系统中,命令(写操作)和查询(读操作)所使用的数据模型是有区别的。命令模型用于有效地执行写/更新操作,而查询模型用于有效地支持各种读模式。通过领域事件或其他各种机制将命令模型中的变更传播到查询模型中,让两个模型之间的数据保持同步。 + + +CQRS 并没有规定这两个模型如何保持同步。同步可以通过同时更新两个模型来同步实现,也可以通过消息代理(如 Kafka)将命令从命令模型传输到查询模型来异步实现。后一种比较常用,因为它让系统更加可伸缩,尽管它需要在写操作和读操作的最终一致性方面做出权衡。 + + + + + +面试官最喜欢问的相关问题: + +- 在项目中遇到的**最大的技术挑战**是什么,而**你是如果解决的** + +- 给出一个项目问题来让面试者分析 + + + + +**变被动为主动** + +一个面试中如何变被动为主动的技巧,例如自己的项目是一套分布式系统,我们在介绍项目的时候主动说:“项目中的难点就是分布式数据一致性的问题。”。 + +此时就应该知道面试官定会问:“你是如何解决数据一致性的?”。 + +真正好的简历是 当同学们把自己的简历递给面试官的时候,基本都知道面试官看着简历都会问什么问题,然后将面试官的引导到自己最熟悉的领域, + + + + +通过FAB模式来增强其说服力。 + +Feature:是什么 +Advantage:比别人好在哪些地方 +Benefit:如果雇佣你,招聘方会得到什么好处 diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2062024.5.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2062024.5.md" new file mode 100644 index 0000000..7f7ee68 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2062024.5.md" @@ -0,0 +1,79 @@ +# 我的简历 + +## 联系方式 +赵锦波 18074844827 fengziboboy@126.com +微信:fengziboboy + +## 工作&教育 +2023.6-至今 拼多多 TEMU-活动团队 后端开发 +2019.7-2023.6 腾讯AMS/搜狗 搜索广告平台开发组 后端开发 + +2016-2019 西安电子科技大学 | 计算机技术 | 研究生 +2011-2015 中国矿业大学 | 计算机科学与技术 | 本科 + +## 综合评价 +1. 丰富的Java开发经验(熟悉集合、多线程、JVM、GC、Spring),了解Go语言。 +2. 熟悉MySQL(索引)、Zookeeper,了解Flink、Hadoop等工具。 +3. 熟悉Solr全文检索引擎(SolrCloud集群部署、查询),了解Elasticsearch。 +4. 了解分布式服务架构,具有高可用、可伸缩分布式架构的系统开发经验。 +5. 有良好的编程习惯,注重代码质量。了解基本的OOP、DDD、TDD思想,熟悉单例、策略、责任链等设计模式。 +6. 有近4年的搜索广告投放平台开发经验(商品库、广告物料检索)、1年电商拉新/留存活动项目开发经验。 + +## 项目经验 + +**2019.07-2021.06 | 商品库/快投系统迭代开发 | 核心开发/模块负责人** + +【**项目描述**】 +商品库是一站式的商品广告物料对接、存储、管理和输出的平台,可整合对接到广告投放平台,支持直接使用商品物料进行搜索广告投放。 + +【**个人工作**】 +- 负责商品库、商品集合系统的功能迭代和维护,独立完成商品直投、商品筛选等核心项目,有效支持商品广告投放,消耗占比从大盘1/7提升到1/5。 +- 负责快投、直投相关数据报告/样式的开发和对外(大客户)API接口开发,从新人成长为模块owner。 + +【**难点收获**】 +- 主动跟踪慢查询定位查询瓶颈,通过SQL索引优化、异步并发、业务定制化,将接口耗时从12s降低到3s,提升用户体验。 + +- 针对商品定时筛选任务接口,平均耗时分钟级、系统卡顿问题,通过增加jvm、性能指标监控,分析内存压力大、频繁FullGC问题,优化代码并行化处理,将平均耗时从5分钟降低到<2s。 + +- **收获**:积累了MySQL索引、**GC内存优化**经验,锻炼了独立排查问题能力,形成了数据驱动思维和产品思维,可独立负责中型系统的迭代和维护。 + + +**项目二: 大规模物料检索系统第二代-搜索服务中台** | **核心开发/模块负责人 | 2021.01-2022.04** + + +【**项目描述**】 +搜索服务中台是CQRS(类读写分离) OLAP**分布式架构**的全文检索系统(**Solr**引擎),为搜索广告中关键词、创意、商品等近百亿数据的列表查询等**多维**筛选场景,提供**跨层级多条件**组合筛选**实时**检索服务,支持全文检索。同时将该系统**平台化**,支持多业务各类物料形式快速接入,已对接4部门7个业务场景。 + +【**个人工作**】 +- 负责查询接口层的重构,主要包括对接业务需求,引入spring-data-solr完成接口**通用化易用性**设计和开发,降低开发新接口的人力成本。 +- 负责索引在线**分布式**加载模块的实现(索引合并、分布式协调索引加载),以及**底层检索引擎**solrCloud模块升级。 +- 负责集群近百台机器的部署维护,跟OP配合排查清零了集群月均2次的宕机现象,增量消费积压报警从日均50+降为个位数。 + +【**难点收获**】 +- 面对索引在线加载多shard分布式异步过程出现数据缺失的复杂问题,通过将reload分为unload和load,并利用zookeeper的通知同步功能,保证了索引数据的完整性。 +- 针对**高并发**下zk导致在线加载偶发失败的问题,通过在上游限速、下游访问zk处参考活锁解决思路进行随机分流,解决了该偶发问题,实现solr集群半年内**零故障**。 +- 针对索引加载整体重试耗时长的问题,通过幂等设计实现分shard分副本的细粒度加载能力,**降低**重试加载60%的耗时。 +- 通过对solrCloud分布式自主恢复的理解,主动编写Shell脚本调用定制接口,增加重试超时机制,实现自动滚动部署不中断,**减少**人工干预成本,将全集群部署总耗时从12小时降低到3小时。 + + + +**项目三:大规模物料检索系统第三代-迁移升级&高可用优化 | 核心开发/模块负责人 | 2022.06-2023.02** + + +【**项目描述**】 +搜索服务中台迁移到腾讯AMS广告投放体系中,完成业务梳理、技术选型、方案设计。 重点包括倒排索引引擎、增量实时通路、全量离线通路、查询通路等核心模块。 + +服务升为一级服务、持续优化高可用:设计支持百亿物料、同步时延p99小于1s,并发更新TPS >3W, 查询平均耗时<100ms, 并发查询QPS>100, 服务可用性争取达到99.99%,部署双链路同时提供服务,通过实时监控、数据对账、降级查库等策略保证业务数据一致性。 + +【**个人工作**】 +- 负责重构**高可用**查询网关服务,自定义DSL解析查询,将查询接口通用化精简,完成trpc协议改写,为查询请求提供鉴权、路由、降级等核心服务治理功能,实现秒级返回。 +- 负责倒排索引引擎的搭建和维护:增加solr&zk双重鉴权,重新搭建solrCloud集群,完成离线加载功能迁移改造。 +- 负责关键词倒排索引平台查询性能优化(solrCloud分布式查询优化),查询高可用、数据一致性保证和快速恢复功能实现。 + +【**难点收获**】 +- 通过自主设计DSL解析将查询接口通用化,支持多层嵌套、函数查询等复杂筛选场景,满足关键词列表多维度筛选需求。 +- **查询高可用**:提供双集群热备,面对单机群宕机、增量延迟、超时等多场景支持自动路由切换,保障服务可用。 支持账户维度实现分场景的细粒度查询降级。平台整体可用性 >=99.95%。 +- **数据一致性**:实现定时端到端数据对账,可感知15分钟内的数据不一致,并上报不一致数据用于数据修复。 +- **收获**:作为分布式物料检索系统的核心成员,负责通用化的查询层和定制化的底层引擎层的设计和开发,积累了较丰富的分布式系统开发经验,理解分布式架构的高可用性与可扩展性。 + + diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2063-1.pdf" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2063-1.pdf" new file mode 100644 index 0000000..9dbf27e Binary files /dev/null and "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2063-1.pdf" differ diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2063.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2063.md" new file mode 100644 index 0000000..64ef29d --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\2063.md" @@ -0,0 +1,78 @@ +# 我的简历 + +## 联系方式 +赵锦波 18074844827 fengziboboy@126.com +微信:fengziboboy + +## 工作&教育 +2019.7-**至今** 腾讯AMS/搜狗 搜索广告平台开发组 后端开发 + +2016-2019 西安电子科技大学 | 计算机技术 | 研究生 +2011-2015 中国矿业大学 | 计算机科学与技术 | 本科 + +## 综合评价 +1. 丰富的Java开发经验(熟悉集合、多线程、JVM、GC、Spring),了解Go语言。 +2. 熟悉MySQL(索引)、Zookeeper,了解Flink、Hadoop等工具。 +3. 熟悉Solr全文检索引擎(SolrCloud集群部署、查询),了解Elasticsearch。 +4. 了解分布式服务架构,具有高可用、可伸缩分布式架构的系统开发经验。 +5. 有良好的编程习惯,注重代码质量。了解基本的OOP、DDD、TDD思想,熟悉单例、策略、责任链等设计模式。 +6. 有3年半的搜索广告投放平台开发经验(商品库、广告物料检索)。 + +## 项目经验 + +**2019.07-2021.06 | 商品库/快投系统迭代开发 | 核心开发/模块负责人** + +【**项目描述**】 +商品库是一站式的商品广告物料对接、存储、管理和输出的平台,可整合对接到广告投放平台,支持直接使用商品物料进行搜索广告投放。 + +【**个人工作**】 +- 负责商品库、商品集合系统的功能迭代和维护,独立完成商品直投、商品筛选等核心项目,有效支持商品广告投放,消耗占比从大盘1/7提升到1/5。 +- 负责快投、直投相关数据报告/样式的开发和对外(大客户)API接口开发,从新人成长为模块owner。 + +【**难点收获**】 +- 主动跟踪慢查询定位查询瓶颈,通过SQL索引优化、异步并发、业务定制化,将接口耗时从12s降低到3s,提升用户体验。 + +- 针对商品定时筛选任务接口,平均耗时分钟级、系统卡顿问题,通过增加jvm、性能指标监控,分析内存压力大、频繁FullGC问题,优化代码并行化处理,将平均耗时从5分钟降低到<2s。 + +- **收获**:积累了MySQL索引、**GC内存优化**经验,锻炼了独立排查问题能力,形成了数据驱动思维和产品思维,可独立负责中型系统的迭代和维护。 + + +**项目二: 大规模物料检索系统第二代-搜索服务中台** | **核心开发/模块负责人 | 2021.01-2022.04** + + +【**项目描述**】 +搜索服务中台是CQRS(类读写分离) OLAP**分布式架构**的全文检索系统(**Solr**引擎),为搜索广告中关键词、创意、商品等近百亿数据的列表查询等**多维**筛选场景,提供**跨层级多条件**组合筛选**实时**检索服务,支持全文检索。同时将该系统**平台化**,支持多业务各类物料形式快速接入,已对接4部门7个业务场景。 + +【**个人工作**】 +- 负责查询接口层的重构,主要包括对接业务需求,引入spring-data-solr完成接口**通用化易用性**设计和开发,降低开发新接口的人力成本。 +- 负责索引在线**分布式**加载模块的实现(索引合并、分布式协调索引加载),以及**底层检索引擎**solrCloud模块升级。 +- 负责集群近百台机器的部署维护,跟OP配合排查清零了集群月均2次的宕机现象,增量消费积压报警从日均50+降为个位数。 + +【**难点收获**】 +- 面对索引在线加载多shard分布式异步过程出现数据缺失的复杂问题,通过将reload分为unload和load,并利用zookeeper的通知同步功能,保证了索引数据的完整性。 +- 针对**高并发**下zk导致在线加载偶发失败的问题,通过在上游限速、下游访问zk处参考活锁解决思路进行随机分流,解决了该偶发问题,实现solr集群半年内**零故障**。 +- 针对索引加载整体重试耗时长的问题,通过幂等设计实现分shard分副本的细粒度加载能力,**降低**重试加载60%的耗时。 +- 通过对solrCloud分布式自主恢复的理解,主动编写Shell脚本调用定制接口,增加重试超时机制,实现自动滚动部署不中断,**减少**人工干预成本,将全集群部署总耗时从12小时降低到3小时。 + + + +**项目三:大规模物料检索系统第三代-迁移升级&高可用优化 | 核心开发/模块负责人 | 2022.06-2023.02** + + +【**项目描述**】 +搜索服务中台迁移到腾讯AMS广告投放体系中,完成业务梳理、技术选型、方案设计。 重点包括倒排索引引擎、增量实时通路、全量离线通路、查询通路等核心模块。 + +服务升为一级服务、持续优化高可用:设计支持百亿物料、同步时延p99小于1s,并发更新TPS >3W, 查询平均耗时<100ms, 并发查询QPS>100, 服务可用性争取达到99.99%,部署双链路同时提供服务,通过实时监控、数据对账、降级查库等策略保证业务数据一致性。 + +【**个人工作**】 +- 负责重构**高可用**查询网关服务,自定义DSL解析查询,将查询接口通用化精简,完成trpc协议改写,为查询请求提供鉴权、路由、降级等核心服务治理功能,实现秒级返回。 +- 负责倒排索引引擎的搭建和维护:增加solr&zk双重鉴权,重新搭建solrCloud集群,完成离线加载功能迁移改造。 +- 负责关键词倒排索引平台查询性能优化(solrCloud分布式查询优化),查询高可用、数据一致性保证和快速恢复功能实现。 + +【**难点收获**】 +- 通过自主设计DSL解析将查询接口通用化,支持多层嵌套、函数查询等复杂筛选场景,满足关键词列表多维度筛选需求。 +- **查询高可用**:提供双集群热备,面对单机群宕机、增量延迟、超时等多场景支持自动路由切换,保障服务可用。 支持账户维度实现分场景的细粒度查询降级。平台整体可用性 >=99.95%。 +- **数据一致性**:实现定时端到端数据对账,可感知15分钟内的数据不一致,并上报不一致数据用于数据修复。 +- **收获**:作为分布式物料检索系统的核心成员,负责通用化的查询层和定制化的底层引擎层的设计和开发,积累了较丰富的分布式系统开发经验,理解分布式架构的高可用性与可扩展性。 + + diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206\350\214\203\344\276\213.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206\350\214\203\344\276\213.md" new file mode 100644 index 0000000..d979e2a --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\347\256\200\345\216\206\350\214\203\344\276\213.md" @@ -0,0 +1,89 @@ +# 我的简历 + +## 联系方式 +赵锦波 18074844827 fengziboboy@126.com +微信:fengziboboy + +## 个人信息 +2019.7-至今 腾讯AMS/搜狗 搜索广告平台开发组 后端开发 + +2016-2019 西安电子科技大学 | 计算机技术 | 研究生 +2011-2015 中国矿业大学 | 计算机科学与技术 | 本科 + +## 项目经验 + +**项目一: 商品库/快投系统迭代开发 | 核心开发/模块负责人 | 2019.07-2021.06** + +【**项目描述**】 +商品库是一站式的商品广告物料对接、存储、管理和输出的平台,商品库中的商品信息可以整合对接到广告投放平台,如快投商品推广平台,支持直接使用商品物料进行搜索广告投放。商品规模2亿左右。 + +【**个人工作**】 +- 负责商品库、商品集合系统的功能迭代和维护,独立完成商品直投、商品筛选等核心项目,有效支持商品广告投放,消耗占比从大盘1/7提升到1/5。 +- 负责快投、直投相关数据报告/样式的开发和对外(大客户)API接口开发,模块owner。 + +【**难点收获**】 +- 主动跟踪慢查询定位查询瓶颈,通过SQL索引优化、异步并发、业务定制化,将接口耗时从12s降低到3s,提升用户体验。 + +- 针对商品定时筛选任务接口,平均耗时分钟级、系统卡顿问题,通过增加jvm、性能指标监控,分析内存压力大、频繁FullGC问题,优化代码并行化处理,将平均耗时从5分钟降低到<2s。 + +- **收获**:积累了MySQL索引、GC内存优化经验,锻炼了独立排查问题能力,形成了数据驱动思维和产品思维,可独立负责中型系统的迭代和维护。 + + +**项目二: 大规模物料检索系统第二代-搜索服务中台** | **核心开发/模块负责人 | 2021.01-2022.04** + + +【**项目描述**】 +搜索服务中台是CQRS(类读写分离) OLAP分布式架构的全文检索系统(Solr引擎),为搜索广告中关键词、创意、商品等近百亿数据的列表查询等多维筛选场景,提供**跨层级多条件**组合筛选**实时**检索服务,支持全文检索。同时将该系统平台化,支持多业务各类物料形式快速接入,已对接4部门7个业务场景。 + +【**个人工作**】 +- 负责查询接口层的重构,主要包括对接业务需求,引入spring-data-solr完成接口通用化易用性设计和开发,降低开发新接口的人力成本。 +- 负责索引在线分布式加载模块的实现(索引合并、分布式协调索引加载),以及**底层检索引擎**solrCloud模块升级。 +- 负责集群近百台机器的部署维护,跟OP配合排查清零了集群月均2次的宕机现象,增量消费积压报警从日均50+降为个位数。 + +【**难点收获**】 +- 面对索引在线加载多shard分布式异步过程出现数据缺失的复杂问题,通过将reload分为unload和load,并利用zookeeper的通知同步功能,保证了索引数据的完整性。 +- 针对高并发下zk导致在线加载偶发失败的问题,通过在上游限速、下游访问zk处参考活锁解决思路进行随机分流,解决了该偶发问题,实现solr集群半年内零故障。 +- 针对索引加载整体重试耗时长的问题,通过幂等设计实现分shard分副本的细粒度加载能力,降低重试加载60%的耗时。 +- 通过对solrCloud分布式自主恢复的理解,主动编写Shell脚本调用定制接口,增加重试超时机制,实现滚动部署不中断,减少人工干预成本,将全集群部署总耗时从12小时降低到3小时。 + + + +**项目三:大规模物料检索系统第三代-迁移升级&高可用优化 | 核心开发/模块负责人 | 2022.06-2023.02** + + +【**项目描述**】 +搜索服务中台迁移到腾讯AMS广告投放体系中,完成业务需求梳理、技术选型、方案设计。 重点包括底层索引引擎、增量实时通路、全量通路、查询通路等核心模块。 + +服务升为一级服务、高可用持续优化:设计支持百亿物料、同步时延p99小于1s,并发更新TPS >3W, 查询平均耗时<100ms, 并发查询QPS>100, 服务可用性争取达到99.99%,部署双链路同时提供服务,通过实时监控、数据对账、降级查库等策略保证业务数据一致性。 + +【**个人工作**】 +- 重构高可用查询网关服务,自定义DSL解析查询,将查询接口通用化精简,完成trpc协议改写,为查询请求提供鉴权、路由、降级等核心服务治理功能,实现秒级返回。 +- 负责倒排索引引擎的搭建和维护:增加solr&zk双重鉴权,重新搭建solrCloud集群,完成离线加载功能迁移改造。 +- 负责关键词倒排索引平台查询性能优化(solrCloud分布式查询优化),查询高可用、数据一致性保证和快速恢复功能实现。 + +【**难点收获**】 +- 通过自定义DSL将查询接口通用化,支持多层嵌套、函数查询等复杂筛选场景,满足关键词列表多维度筛选需求。 +- 查询高可用:提供双集群热备,面对单机群宕机、增量延迟、超时等多场景支持自动路由切换,保障服务可用。 支持账户维度实现分场景的细粒度查询降级。平台整体可用性 >=99.95%。 +- 数据一致性:实现定时端到端数据对账,可感知15分钟内的数据不一致,并上报不一致数据用于数据修复。 +- **收获**:作为分布式物料检索系统的核心成员,负责通用化的查询层和定制化的底层引擎层的设计和开发,积累了较丰富的分布式系统开发经验,理解分布式架构的高可用性与可扩展性。 + + +## 专业技能 +熟练使用Java(集合、多线程、JVM、GC),了解Go(集合、协程)。 +熟悉使用Spring MVC、Spring Boot。 +熟悉MySQL(索引)、Zookeeper,了解Flink、Hadoop等工具。 +熟悉Solr检索引擎(SolrCloud集群部署、查询),了解Elasticsearch。 +了解高可用、高并发,高负载的架构,具有分布式架构的系统开发经验。 +了解基本的OOP、DDD、TDD思想,熟悉单例、策略、责任链等设计模式。 +了解Linux系统基本操作、Shell编写和常用命令。 +了解基础网络知识(TCP, UDP,HTTP等)。 + + +## 自我介绍&其他信息 +有良好的编程习惯,注重代码质量。 +具有一定的文档编写、画图能力,有一定的产品直觉。 +性格开朗,好相处,喜欢旅行,会点摄影。 +具有良好的沟通协作能力,与OP、QA、PM同事合作都有不错风评。 +(有回应、有反馈、有轻重缓急、有理有据) + + diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221-\351\241\271\347\233\256\351\241\272\345\272\217.pdf" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221-\351\241\271\347\233\256\351\241\272\345\272\217.pdf" new file mode 100644 index 0000000..8d8b54b Binary files /dev/null and "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221-\351\241\271\347\233\256\351\241\272\345\272\217.pdf" differ diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221.pdf" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221.pdf" new file mode 100644 index 0000000..0564e27 Binary files /dev/null and "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221.pdf" differ diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221pre1.pdf" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221pre1.pdf" new file mode 100644 index 0000000..9fb9f4e Binary files /dev/null and "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\350\265\265\351\224\246\346\263\242-\350\205\276\350\256\257-\346\220\234\347\264\242\345\271\277\345\221\212\345\271\263\345\217\260-\345\220\216\347\253\257\345\274\200\345\217\221pre1.pdf" differ diff --git "a/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\351\235\242\350\257\225\351\202\243\344\272\233\344\272\213.md" "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\351\235\242\350\257\225\351\202\243\344\272\233\344\272\213.md" new file mode 100644 index 0000000..74f9340 --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\345\255\246\344\271\240/\351\235\242\350\257\225\351\202\243\344\272\233\344\272\213.md" @@ -0,0 +1,598 @@ + + +# 面试 +面试的核心,对于应聘者,是想方设法让面试官认可自己对职位有用的那些优点。 + +- 用真诚的态度,有效地表达自己的能力和价值,建立相互的信任和认可。 +- 面试时双向选择,面试官想要个符合要求的,你也想要个自己能做得舒服或者能有成长的公司。 + - 我也不想找一个自己无法胜任,我也想找个匹配度较高,我也能更好的发挥自己能力的地方 + + +## 面试考察点 + +基础知识的考察会**比较重视深度及广度**,可能会问很多细节,也可能会**结合具体场景**去提问,一定不要让面试官觉得您的基础知识缺失过多~ + + +- 其次是关于过往**项目**的考察:这一块是**重点**。 + - 一定要尽量拿出**比较有难度跟亮点**的项目阐述,您最近也可以再梳理一下,尽量是**业务场景较为复杂**的,如果项目没有明显难度或是整体架构比较简单(没有**分布式**,基本是单机)的话就缺乏亮点跟竞争力。 + - 简历中**提到的基础组件的原理一定要深入了解**,**不能只了解使用层面**。 + + +- 最后会考**一道到两道**算法题,算法题的话你最近再准备一下 + - 一般都是leetcode前100的题,或者是medium难度的, + - 最重要的是**思路要快**,如果**完全没有思路代码实现不出来的话是不可以的** + - 如果出现错误或者边界处理有些问题,但是经过面试官提示或者检查最终改正,也是ok的,不会太致命。 + + +- 然后想强调的是,您对于过去的工作项目一定要**有落地及产出**,也要及时去**复盘技术方案哪里有缺陷**,可以**给出优化方案**, +- 对于工作一定要有**个人的理解与思考**,知道**业务的困难**在哪里,**关键数据指标**尽量都清楚,比如业务的目标在哪里,业务的收入来源是什么,需要提升哪些关键指标; +- **不要**让面试官觉得您只是**单纯的执行**角色或者做的事情都是**业务需求驱动**开发的,一定要**突出个人的潜力**,凸显**工程能力**跟**架构能力**。 + + +**快速学习的能力** + + + +**答不出问题怎么办?** + + +如果面试官一直在一个你不懂的领域周旋,你可以通过坦诚相告自己在这块的能力和经验,并问询实际工作中对这块的要求程度,表明你的学习能力等方式,尽量消除你的这份能力差距带来的影响。 + + +**回答系统性,说清楚技术** + + +解释一个知识点,可以从 多个角度来讲。展开的角度,直接显示了你的知识面和认知深度: +- 从内部原理到外部应用, 从问题出发讲多个可选方案,从技术出发讲若干应用场景;- 还可以把多个点,通过不同的维 度,串起来讲,讲体系的横向对比,纵向发展史。 + + +**分析和解决一个场景问题** + + +- 一个方案,不要只考虑成功的一面,还要考虑到失败后的应对方法。 +- 进而,选择和评判标准不要只按正反两种情况分析,而应该是灰度的。 +- 不要只按自己的视角去分析,应该考虑到影响的多方受众,换位思考。 +- 对于边界模糊的问题,是不是需要放到具体的情景中去讨论,才能有的放矢。 +- 多个问题的回答中不要出现自相矛盾,观点前后要照应。 + + +**如何讲得全面而且不啰嗦呢?** + + +**啰嗦**是指,听者已经明白的内容,你还在继续反复阐述,或者你的表达逻辑有 +问题,脉络不清楚,和这里的思路其实两码事儿。 + +建议你讲出体系,**注意逻辑和有效交互**。 + + +### hr面 +要从技术氛围,职业发展,公司潜力等等方面来说自己为什么选择这家公司 + +职业规划: 尽量从技术的角度规划自己。 + +#### 优缺点 + +优点: + - 工作态度,认真负责,owner意识。 团队意识,出现问题会主动询问,产品反馈线上故障,熬夜看到线上报警。 + + +缺点: +- 技术追求钻牛角尖。 能用,妥协,先把问题快速解决。 要留buff + + +#### 了解我们公司吗 +飞猪 +- 票务占比第二,酒店占比2021年只有7.3%,第五。 +- 全球化是初衷,疫情不得不本地化,现在如何? +- 年轻化, 对手携程有去哪儿,美团酒旅低端占比高 +- 全球化?出境游,度假 +- 近集成 OpenAI API 技术,为旅行者提供实时帮助,如实时接收量身定制的旅游路线、行程和旅行 预订建议等全方位的旅游解决方案。 攻略资源? +- 社区 + + +#### 谈薪资 + +薪水(物质保障水平)、成就感(精神满足水平)、成长(职业发展速度) + +- 薪水和你的贡献价值成比例,你让公司觉得你对职位贡献的价值越高,可能得到的薪水也越高,从而扩大绿色三角形。 + +- 成就感和成长,与公司、团队、老板,以及职位做的事情、发展空间相关,公司要让你尽可能多地了解这些方面的优势,从而扩大给你的红色三角形,而不是只靠拉高薪水。 + +- 经过双方充分沟通,两个三角形尽可能地靠拢,你情我愿,Offer 也就谈拢了。否则,即 使签了 Offer,双方的期待存在差异,矛盾迟早会在工作中暴露出来。 + + +**矛盾**: +如果应聘者谈成的薪水比其他同事高,但过了工作适应期,贡献度却比其他同事低。这种薪水倒挂现象,是一种矛盾。 + + +**怎么谈?** + + +* 深入了解**这个职位要求你必须做到什么**,以及达到优秀的话,需要做到什么。 +* **强调**你能**为公司做什么,带来什么价值**。 + * 我有较丰富的开发经验,分布式系统开发经验,经历大规模数据,技术难题解决经验。 + * 我沟通能力还行,对技术有追求,在持续学习。 对产品有兴趣,不是一味的工具人 +* 了解职位的发展空间,以及**对你的成长期望**。 +* 表达你的**发展诉求**,以及所需要的资源和支持。 + * 这个薪资是我横向对比了同届小伙伴的平均水平, 特别是经历过跳槽的小伙伴。 + * 这个薪水能满足我当下的期望,能让我安心稳定的去做为公司服务,去打磨自己,在这个平台得到更大的提升,我其实是一个比较稳定的。 薪资倒挂,外加上最近组织变动频繁,这种不确定性,影响工作效率,工作重点方向都会变化。 +* 了解该职位的薪酬水平,和薪水发展水平。着眼大局,而不是眼下的数字。 + + +>不是每次谈 Offer 都能成功的,即使谈不拢,也可以保留将来合作 的机会。 + +**工资构成** +- 月base? 几个月? 年终奖多少? 有最低吗,或者一般情况 +- 期权股票? +- 职级? 下次晋升? 产出 + + +**福利&公积金** +- 公积金比例和基数 +- 年假 +- 商业保险 +- 其他福利(打车、团建、住房、租房补贴、买房贷款) + +## 项目介绍 + + +## 软技能展示 + +### 沟通&团队精神 +**沟通**: 沟通是指为了设定的目标,秉持合作共赢的理念,以他人愿意接受的方式,把信息、思想和情感在个人或群体间传递,并达成共同协议的过程。 + +合作利他 + + +以他人愿意接受的方式沟通 +``` +你不能直接对一个程序员说:“你的代码有bug。” +他的第一反应是:“1、你的环境有问题;2、你的使用方法不对” +如果你委婉地说:“你这个程序和预期有些不一致,你看看是不是我的使用方法有问题。” +他会本能的想:“是不是出bug了?赶紧看看” +``` + + + +### 项目管理 & Owner意识 +项目Owner +1. 关注目标需求:帮助产品优化需求,提升体验 +2. 主导拆解任务: 尽量多check,防止功能遗漏 +3. 主导制定需求实现方案: 评估工作量和项目风险,分配好工作 +4. 制定项目计划: 根据工作量和项目依赖关系,制定项目计划 +5. 关注计划执行:发现并控制变更,同步项目进展 +6. 项目总结: 总结得失 +7. 关注项目效果: 关注上线后项目数据和后续维护 + +### 主动学习、积极主动 + + +### 质量意识 + +1. 需求阶段: 不要想当然,对于模糊边界和可能的异常情况,跟产品确认清楚,理解一致。多轮沟通 +2. 方案设计阶段: 不要一上来就写代码,完整思考设计方案后,积极邀请大佬帮忙评审、完善方案,找问题,确保方案完备,避免返工 +3. 编码测试阶段: 代码风格,代码质量 +4. 发布阶段: 个人的和项目的checkList: 配置版本、镜像包版本 +5. 运营阶段: 建立完善的监控体系、日志链路, 确保能快速发现问题、快速定位问题,尽量避免问题出现提前报警,出现问题也要比客户更早发现并修复。 + + +### 产品思维 +- 用户价值、商业价值 vs 技术至上 +- Why? vs How? +- 全局观 vs 关注细节 +- 完成比完美更重要 vs 完美情节 +- 想小白一样思考 vs 像专家一样思考 +- 体验 vs 功能 +- Benefit vs Feature + +比如有些开发写的代码、文档、邮件格式等,更多站在自己的角度,怎么快怎么来,较少去考虑阅读者的用户体验。特别是一切长期的维护的项目,代码、文档阅读花的时间要远超过编写时间,如果体验不好,其实非常浪费自己和他人的时间。 + + +* 为什么要做这个产品?核心是为了解决什么问题、痛点? +* 用户为什么要选择这个产品?相对其他产品有什么不一样? +* 用户是谁?他们有什么心理特征、社会特征、行为特征、认知特征? +* 用户是在什么情景下使用该产品?如何通过产品和用户建立连接? +* 如何提升用户体验?帮助用户快速达成目标 + + +#### 成长方向 + +对于擅长而且热爱技术的同学,初级阶段是做到**技术精湛**; + +渐渐地开始关注业务问题,因为只有**用技术解决好用户痛点**等业务问题,才能显示出你的“终端”价值; + +再往后就要带领团队用好技术,解决更大的业务问题,把产品或服务变得对用户更“有用”更“好用”,这时,就可以算是技术领导者了。 + +**想发展为技术领导者的朋友,需要具备三方面的基本功:扎根技术,着眼业务,懂得 管理。** + + +#### 如何向面试官提问? + +面试官之所以让应聘者提问,一是表达对应聘者的尊重,显示平等的对话关系; + +二是通过你的关注点,**了解你的需求和动机**,进而判断当下的职位是否适合你; + +三是通过审视你提的问题,**考查你的思考广度和深度**,以及经验能力。 + + +面试中,面试官**让你主导对话的机会**不多,第一次是自我介绍,第二次就是让你提问的环节。 + +- 获取关于**团队、职位或者项目**的一手信息,这是你在网上或者从朋友同学那里拿不到的。 这些信息对你判断当前职位的匹配度非常重要。 +- 表现自己的思考方式、认知水平和经验能力,以此提升面试效果,锦上添花。 + + +**应聘者的问题,最好是与职位相关的,并且是应聘者、面试官都关注的信息。** + +- 职位需要的信息,包括公司、团队、角色责任、技能要求、工作压力、发展机会; +- “我能得到什么资源来学习和提高”“我能得到什么晋升机会”,面试官会觉得你只关注个人成长,而不是工作本身; +- 而如果你只问面试官和职位需要的信息,而忽略了自己的关注点,就难免会失去了解团队和职位的机会,而且显得有些讨好面试官的意思。 + + +**不适合的问题** +- 有关薪酬待遇的细节。如果面试官没有主动开始这个话题,最好不要自己问起。 +- 有关涨薪升职的条件。 +- 我面试表现得怎么样。对面试官没有价值。 +- 面试官您在这个团队里是什么角色。 + + +**不错的问题** + +- 有关团队的现状和发展前景,要解决的挑战和问题等。 +- 有关项目或产品的业务、价值、**技术栈**、**流程工具**等。 +- 有关职位的工作对象、工作环境、方法工具等。 +- 该职位的考核标准、职位期望。 +- 我来之后,如何开展工作? 针对我的岗位,我还需要什么技能? 因为之前用到了很多技能,有点浅尝辄止 + +>表明你能换位思考,从老板的角度去理解他的期望; +>同时也说明你是个注重实干的人,你希望了解职位的考核标准,以及所需要的各种能 力; +>你也是个干劲十足的人,你做事结果导向,有目标感。 +>确认这个职位的需求和自己的能力是否契合,以及你是否真正有意愿做这个工作。 + + +**有一定危险性的问题** + +- 该职位的职业上升空间。 + - 表明你关注职业发展,有上进心。 + - 但是如果你只问这一个问题,可能会认为你只关注个人成长,如果上升空间有限,离职率高。 +- 该职位对应的培训。 + - 说明你有学习和提高的意识。 + - 更喜欢直接上手的, 说明你不自信,更注重个人成长。 +- 公司的主营业务和竞争力。 + - 虽然显示你对公司的关心, + - 但如果是公开信息的话,也反映出你面试前没有做足功课。 + - 公司的组织结构和业务模型的关系? 现在的主攻方向?该团队在公司组织架构中起到什么作用?该团队做的业务如何支持公司战略? +- 补充之前没有回答好的问题。 + - 如果之前回答整体不错,只是个别问题没回答好,能起到一些完善的作用,面 试官也会喜欢你这种精益求精的精神; + - 反之,没啥用。 + + +**如何加分**: + +- 尽量展示正向的态度和观点,适当表示负面的担忧和建议。 +- 问合适的人。 +- 问题带着自己的洞见。 + - 问一个不是技术相关的问题, 发力点,我将进入的团队在这里起到什么作用,需要完成什么。 +- 注意情势。 + - 前面提到,你要根据前面面试部分的自我评价,采用合适的问题弥补或者提升 面试效果。 + - 良好的沟通,态度(QA、OP), 有回应,有反馈,有轻重缓急,有理有据。 +- 问题面不要太宽泛,让人抓不住你的关注点。 + - 如何做好工作?--> “要做好这个职位的工 作,最需要具备哪些特殊的能力和素质?” +- 问题要简洁精准,不要有恭维或者冗余。 +- 问题提出去,要聆听面试官的回答,反馈你的理解,或者引出下一个问题。 + + +# 我的面试大纲 + +## 自我介绍 + +**面试官能从自我介绍里得到什么** + +- 经历概括,从而了解你的职业发展路径。 + - 应聘者在这么短的时间内提及的项目、角色和 职位,肯定是他觉得非常重要的,面试官接下来会重点考查。 +- 经验和技能总结, 从而简单评价应聘者的经验面和技能等级。 +- 表达风格和气场。 +- 简历内容之外的信息,比如职业规划、跳槽动机、其他亮点等。 + + + +**面试官不想听**,或者不会在意的一些自我介绍信息有下面这些: + + * **简单地重复**简历上的条目; + * 自己的**主观自评**; + * 用口号化的语言来表白对这份工作的向往; + * 项目和技术**细节**(此时还没到考查细节的时候,面试官会打断你); + * 其他与个人经历不相关的信息。 + + +成功的自我介绍,应该达到下面三层效果: +1. 满足面试官对信息的期待; +2. 产生好感; +3. 记住你。 + + +记住你,说明对你印象深刻,说明你与众不同。 + +面试结束以后,面试官们很可能会对比一下 候选人的情况。即使想不起来应聘者的名字,也会用标签化的代号来提及你,比如“那个清 华毕业的 XXX”,或者“那个能跳绳的”。 如果大家不看简历,都想不起你来,说明你没亮点。 + +>你想让面试官记住你什么? + + +**第一层,满足面试官对信息的期待。** + +- 把个人信息、主要经历、经验和技能有条理地组织起来,有逻辑地讲出来。 +- 对于经历丰富的应聘者,需要找出多段经历的关联性和发展变化,形成连贯的职业发展和能力上升路线。 + - 最开始商品库,从执行者,变成模块owner, 之后项目稳定后做了一段时间数据报告。 + - 有个T10的大佬离职,其负责的物料检索模块, 我当时主动表示自己对这个感兴趣。由一个T9技术,我跟另外一个同一年毕业的小伙伴一起加入。T9的技术人带,开始学习重构该项目,平台化。 所以我是在1.0的基础上重构。 之后到腾讯就开始3.0升级。 + - 从头重构搜索中台。 + - 面临各种分布式的技术难题和性能问题、稳定性,刚开始那会不断熬夜。 问题收敛,成就感。 + + +**第二层,产生好感。** + +- 首先,态度要诚恳可信。 +- 另外,自我介绍要和面试官形成友好的互动。 + - 一是控制详略。 + - 比如当面试官略微侧过头,仔细听你讲时,很可能是产生了兴趣,你需要多讲些相关信息,帮助面试官理解得更充分些。 + - 如果面试官连续点头,口称“好的,好的”,很可能是催你赶紧说下一话题,这一段信息量足够了。 + - 二是扭转局面。 + - 一旦你发现面试官有怀疑或者反对的迹象,比如皱眉,或者表情严肃起来,不要慌,想想是什么引起对方的怀疑或者反感,及时补充信息。 + + +**第三层,记住你** + +如何总结工作经历、 挖掘个人亮点,自我介绍时完全可以选择一两个亮点加进去。注意,这些亮点,**要有细节**。 细节能让自我介绍更生动、更让人信服,让你令人印象深刻。 + +- 这里说的细节不是几百字的起因经过结果,细节可以是精炼的数字或者例证。 +- 细节还可以是升华的感受或者评价。 + - 我觉得要想 快速提高自己,就要不怕挑战。 + - 你要想到自己的成长,还要想到团队的需要。 +- 细节还可以是转折和波澜。 + - 核心开发离职。 + +**要和面试官建立情感沟通,而不仅是内容沟通。** + +## 项目 + +- 面向业务,查询端,接口抽象 +- 面向底层检索,运维。 +- 负责一半核心场景,全量加载。 + +### 难点 + +- 分布式事务? 离线加载时异步的,没有事务控制?如何保证幂等?如何保证事务回滚?如何保证不重发? 设置状态码,zk,全局变量,挂机怎么办? 是否还有优化空间? +- + +### 可优化的点? + +- 根据业务场景,定制化构建索引: 不查的只存储 + + +### 知识面扩展 + + +#### solr vs es +- 返回计算后的字段,还能用该字段排序。 es使用脚本字段 script_field:{} +- null,不为空的筛选,写入时可以对null设置成“NULL” +- 大规模id返回 +- es自动以token处理,分词,替换词,修改删除。 正则,表情符号变成想要的词 +- 动态推断类型,可以通过设置比如字段名为is开头的都推断成boolean类型,或者字段为xx结尾的,都copy_to到一个新字段,用于检索,啥的 +- es的精确匹配可以用每个字段默认有个keyword,好像不是默认,但是可以设置? solr需要新建一个不分词的字段,专门用于精确匹配。 + +es Analyze的过程:char filter -》 tokenizer -》 token filter + + + +solr如何解决分布式事务,更深入的了解? + +不能仅停留在使用,还要有自己的探索 + +#### java跟go的区别 + + + +### 基础知识 + +#### 后端开发 + +消息中间件:Kafka + +微服务: 微服务架构、微服务治理、服务网格 + +网络: 网络拓扑结构、路由协议、传输协议 + +#### Java +Java基础、多线程、集合、IO + + +**集合**: +HashMap 源码,数据结构,如何避免哈希冲突,对比 HashTable +HashMap 源码中,计算 hash 值为什么有一个 高 16 位 和 低 16 位异或的过程? +为什么重写 equals 还要重写 hashCode,不重写会有什么问题 +ConcurrentHashMap 底层实现,扩容问题。 + +java hashmap、 为什么用红黑树、红黑树邻接点为啥是8 。 + + +**JVM** +有 jvm 调优的经验吗?实际工作中遇到过内存相关的问题吗?用过哪些堆栈工具调试? +JVM的内存结构,Eden和Survivor比例 + + +**GC** +- 更换GC的场景与问题 +- Java常见的垃圾收集器 + - g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择。 + + +场景题: cpu 打满且频繁 full GC,怎么解决? + + +#### Spring +Spring怎么解决循环依赖的? +说下Spring IOC? +说下Spring Bean的生命周期? +说下Spring AOP?他的底层实现是什么?(动态代理) + + +**动态代理** + + +#### Go +1.逃逸分析? +2.Channel 是被分配在了栈上还是堆上? +3.defer的原理? +4.CPU核数为2时开多少个线程比较合适? + +1.Go有哪些常见的并发原语? +2.Map是线程安全的吗? +3.如何设计一个线程安全的map? +4.SingleFlight 和 CyclicBarrier + + +#### 网络 +1.Cookie的参数有哪些? +2.Cookie和session的对比? +3.什么是token? +4.传递token的过程中有什么安全性的问题? + +http码 302 403 。 +https 加密过程。 +HTTP的长连接是什么意思。 + + + +(2)网络协议:TCP、UDP、QUIC、http以及https协议;例如TCP如何保证可靠性?UDP的使用场景? +- TCP丢包重传优化:设计TCP的初衷是优先保证数据传输的可靠性,丢包重传机制对TCP的传输性能影响较大(特别在网络环境较差时),算是TCP为保证可靠性做的性能妥协,可通过**使用UDP协议并在应用层重新设计丢包重传机制实现性能优化**,比如**QUIC**协议便采用了这种优化思路; + + + + + +#### 数据库 + +**索引** +Mysql 索引,数据结构为什么使用 B+ 树 +索引覆盖了解吗? +索引失效的场景? 联合索引,没有最左前缀匹配。 +- a and b > and c 能用上索引嘛? +- a and c 能用上索引嘛? +简单描述一下数据库的四种隔离级别以及对应的三种相关问题 + +MVCC + 锁 保证隔离性 +讲讲MySQL锁机制(共享锁、排他锁、行级锁、表级锁、意向锁、记录锁、间隙锁、next-key Lock) +redo log&undo log作用,什么时候生成 +如何写redo log(redo log buffer等) +两段锁协议,分布式事务中两阶段提交 + + +zookeeper : ZAB、Paxox 活锁 用随机sleep, 三阶段提交: +ZAB为了解决活锁问题,只允许一个进程提交提案,属于3PC提交。而,leader挂了时候选举算法是2PC,所有的follower都可以提交,就是我选我。 + + +造成幻读的原因了解吗,快照读、当前读。 +数据库自增 ID 和 UUID 对比 + + +MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据 + +(3)数据库:MySQL、Redis,例如redis的持久优化方式? +(4)缓存分布式以及编程语言相关的知识:例如分布式锁的实现方式?Golang以及Java语言的区别? + +**redis**: + +Redis分布式锁如何实现的 +分布式锁还有哪些实现方案 +Redis大Key,会引发哪些问题? +怎么解决大Key问题?拆 +Redis怎么实现分布式锁?说了redission +redission那个不实用,有没有更简单的? +Redis的IO模型了解吗?(要说出关键点) +Redis是怎么解决线程安全问题的? +说下Redis的数据结构及其使用场景? +Redis作为缓存的话,说下如何保证数据一致性?(延迟双删、消息队列重试、基于binlog) +说下缓存击穿,如何解决?(设置热点数据永不过期、更新缓存时,加全局锁,保证只有一个走数据库) +分布式锁有哪些实现方式? + + +**查询优化** + +- 数据量大,状态枚举值分布不均匀,不是直接group by,而是直接指定checkstatus=1 或者0,查。如果指导2最多,其他都很少,那就异步同事查总数,查1,0,最后相减即可。 + + +#### 操作系统 +操作系统虚存实现原理,交换,覆盖区别。 + +1)关于操作系统:进程、线程、协程相关,例如三者的区别在哪里;gevent的基本调度? + + +#### 分布式 +拜占庭问题 +一致性哈希 +如何控制负载均衡 + +paxos算法 +raft算法 +ZAB(ZooKeeper Atomic Broadcas + + +#### 情景题 +1.实现一个Word文档中的单词拼写检查功能? +2.字典树?前缀树、后缀树? +3.LRU?有什么优化? +4.Reids?跳表? + +场景题:亿级别黑名单、短链接,你考虑使用什么数据结构?布隆过滤器、前缀树。其中布隆过滤器问了基本的原理和实现方式 + + +设计王者荣耀战力排行榜 +- 战区 +- 好友 + + + +# 大厂面试题候选 + +## 蚂蚁 +阿里面试总结 +【蚂蚁金服一面】 +1.hashmap concurrenthashmap +2.list set map 等常见java集合类 +3.redis 数据结构及应用场景 分布式锁 缓存击穿 缓存穿透 +4.kafka相关 架构 消息重复消费怎么解决 +5.项目相关 +6.分库分表 +7.线程池 参数 拒绝策略 执行流程 Worker +8.看什么技术书籍 规划 +9.设计模式列举 +10.死锁 +11.类加载 双亲委派模型 + +【蚂蚁金服二面】 +项目细节 整体架构 领域划分 +PS:需要对项目非常熟悉,因为是现场面,需要在白板上画出项目领域划分和项目核心流程 + +【蚂蚁金服三面】 +1.项目细节 +2.缓存一致性 +3.缓存淘汰策略 +4.挑战性的事情 +5.未来规划 + +【蚂蚁金服四面】 +全程问项目,项目的细节扣的比较深。 + +【蚂蚁金服hr面】 +1.做的最有意义的项目介绍 +2.个人优缺点列举 +3.和其他人比自身优势在哪 +4.最近绩效 + +【总结】 +1.基础要牢固,阿里的面试会对细节扣的比较深入 +2.项目需要非常熟悉,项目中领域划分及核心流程需要了然于胸 +3.https://tech.meituan.com/2018/08/16/10-principles-for-engineers.html +这篇文章可以好好看看,当问到个人优缺点和自身优势的时候可以套用一下 +4.当面试官最后问「有什么问题要问我」的时候,问的问题需要考究一下。可以问面试官所做的业务,或者问「如果我能够入职阿里,您希望我三个月或者半年做到怎样的成就」 + +另外简历中介绍项目或者面试中介绍项目的时候一定要遵循STAR原则去阐述,这样给别人的印象是对项目有过梳理和自身的思考。并且汇报的时候比较清晰。 + diff --git "a/\350\275\257\346\212\200\350\203\275/\347\224\237\346\264\273/\346\231\232\347\235\241\345\274\272\350\277\253\347\227\207-\345\246\202\344\275\225\346\227\251\347\235\241\346\227\251\350\265\267.md" "b/\350\275\257\346\212\200\350\203\275/\347\224\237\346\264\273/\346\231\232\347\235\241\345\274\272\350\277\253\347\227\207-\345\246\202\344\275\225\346\227\251\347\235\241\346\227\251\350\265\267.md" new file mode 100644 index 0000000..b0e1a7c --- /dev/null +++ "b/\350\275\257\346\212\200\350\203\275/\347\224\237\346\264\273/\346\231\232\347\235\241\345\274\272\350\277\253\347\227\207-\345\246\202\344\275\225\346\227\251\347\235\241\346\227\251\350\265\267.md" @@ -0,0 +1,16 @@ + +# 晚睡的危害 + + + +# 晚睡的原因 + + + +# 如何解决晚睡 + + + +# 解决晚睡实际过程 + + diff --git "a/\351\205\215\345\233\276/Untitled Diagram.drawio" "b/\351\205\215\345\233\276/Untitled Diagram.drawio" new file mode 100644 index 0000000..6966dc3 --- /dev/null +++ "b/\351\205\215\345\233\276/Untitled Diagram.drawio" @@ -0,0 +1 @@ +UzV2zq1wL0osyPDNT0nNUTV2VTV2LsrPL4GwciucU3NyVI0MMlNUjV1UjYwMgFjVyA2HrCFY1qAgsSg1rwSLBiADYTaQg2Y1AA== \ No newline at end of file