13_接口
接口
优势:
- 隐藏细节
- 解耦
- 权限控制
R; Go at Google: Language Design in the Service of Software Engineering
Go接口设计:
- 没有继承
- 面向组合
- 隐式实现 duck type
If it walks like duck, swims like a duck and quacks like a duck, it’s a duck.
如果它像鸭子一样走路,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就是鸭子。
接口最佳实践
示例: orm使用
在 xorm 中插入一行数据的语法是调用 Insert 方法。
user := User{Name: "jonson", Age: 18, Birthday: time.Now()}
db.Insert(&User)
而在 gorm 中,添加一行数据的语法是调用 Create 方法。
user := User{Name: "jonson", Age: 18, Birthday: time.Now()}
db.Create(&User)
初学者:
- 创建一个操作数据库的实例 XormDB,并把它嵌入到实际业务的结构体中。
- 假设现在需要将 xorm 更换到 gorm,我们就需要重新创建一个操作数据库的实例 GormDB。然后把项目中所有使用了 XormDB 的结构体替换为 GormDB,最后检查项目中所有 DB 的操作,把不兼容的 API 全部替换掉,或者使用一些新的特性。
缺点
- 在大型项目中不仅改动非常大,耗时耗力,更重要的是,我们很难对模块进行真正的拆分。
- 对数据库的修改可能破坏或影响项目中一些核心流程的代码(例如插入订单、修改金额等),难以保证结果的正确性。
- 同时,我们不希望随意操作数据库 DB 对象。例如,我们不想暴露删除表的操作,而只希望暴露有限的方法。
改造:
- 创建一个接口实例 DBer,该接口包含一个自定义的插入方法 Insert。
- 再创建一个数据库实例 XormDB,实现了 Insert 方法。
type DBer interface{
Insert(ctx context.Context,instance interface{})
...
}
type XormDB struct{
db *xorm.Session
}
func (xorm *XormDB) Insert(ctx context.Context,instance ...interface{}){
xorm.db.Context(ctx).Insert(instance)
}
现在我们要实现从 xorm 到 gorm 的切换将变得非常简单,只需要新增一个实现了 DBer 的 GormDB 实例,同时在初始化时调用 AddDB 设置新的数据库实例就好了,其他地方的代码完全不用变动。
type GormDB struct{
db *xorm.Session
}
func (gorm *GormDB) Insert(ctx context.Context,instance ...interface{}){
gorm.db.Context(ctx).Create(instance)
}
有了接口,代码变得更具通用性和可扩展性了。而且,我们也不用修改 InsertTrade 等核心业务的方法,这就减少了出错的可能性。更重要的是,我们实现了模块间的解耦,修改 DB 模块不会影响到其他模块,每个模块都可以独立地开发、更换和调试。
依赖注入
程序中的模块通常会依赖其他模块返回的结果,测试中遇到的困难:
- 第三方模块的环境不太容易和线上完全一致,依赖的模块可能又依赖了其他的模块。
- 除了依赖服务太多这个问题外,依赖配置也很繁琐。例如,要测试一个场景,需要往数据库中插入数据、删除数据,这增加了复杂性。
- 场景很难完全覆盖。打个比方,如果当前服务在进行逻辑处理时,非常依赖外部服务返回的数据,那我想测试外部服务返回特定的数据时,当前服务会有什么不同的行为就非常困难。
- 有一些第三方模块涉及到复杂逻辑,或者会 sleep 很长时间,这时进行完整测试需要花费很长的时间。
示例1 InsertTrade不需要启动数据库
它的内部有一个插入订单的操作,测试时不必真的启动一个数据库,也不必真的将订单插入到数据库中。
下面这段代码中,EmptyDB 实现了 DBer 接口,但是实际函数中并不执行任何操作。
type Trade struct {
db DBer
}
func (t *Trade) AddDB(db DBer) {
t.db = db
}
func (t*Trade) InsertTrade() error{
...
t.db.Create(t)
}
// 测试代码
type EmptyDB struct {
}
func (e *EmptyDB) Insert(ctx context.Context, instance ...interface{}) {
return
}
func TestHandleTrade(t *testing.T) {
t := Trade{}
t.add(EmptyDB{})
err := t.handleTrade()
assert.NotNil(t,err)
}
示例2 redigo时间函数
redigo 库的一个重要功能是维持 Redis 的连接池。但是连接一段时间后,需要强制断开,这段时间被称为最大连接时间。假设我们设置的最大连接时间是 300 秒。redigo 在取出连接池的连接后,会先判断当前时间减去连接创建时间是否超过 300 秒。如果超过,则立即销毁连接(这段代码省略掉了不必要的细节,原始代码:
var nowFunc = time.Now
func (p *Pool) GetContext(ctx context.Context) (Conn, error) {
// 从连接池获取连接
for p.idle.front != nil {
pc := p.idle.front
p.idle.popFront()
// 当前时间减去连接创建时间未超过300秒,立即返回。
if (nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
return &activeConn{p: p, pc: pc}, nil
}
}
}
这里比较有意思的是,获取当前时间的方式通过了一个 nowFunc 变量。
nowFunc 是一个函数变量,其本质上也是 time.Now 函数。
为什么不直接使用我们比较熟悉的 time.Now(),而是额外增加了一层呢?
- 为了方便测试。你试想一下,如果我们想测试函数在 300s 之后能否断开,那么我们的单元测试必须要等 300s 这么久吗?显然是不可能的,这样做效率太低了。
- Redigo 的做法是,通过修改 now 函数变量对应的值,我们可以任意修改当前时间,从而影响 GetContext 函数的行为。当时间未超过最大连接时间时,我们预期连接会被复用,达不到测试超时的效果,所以我们可以设置 now = now.Add(p.MaxConnLifetime + 1) ,巧妙地让当前时间超过最大连接时间,看连接是不是真的和预期一样被销毁。
// pool_test.go
func TestPoolMaxLifetime(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
MaxConnLifetime: 300 * time.Second,
Dial: d.dial,
}
defer p.Close()
// 设置now为当前时间
now := time.Now()
redis.SetNowFunc(func() time.Time { return now })
defer redis.SetNowFunc(time.Now)
c := p.Get()
_, err := c.Do("PING")
require.NoError(t, err)
c.Close()
d.check("1", p, 1, 1, 0)
// 设置now为最大连接时间+1
now = now.Add(p.MaxConnLifetime + 1)
c = p.Get()
_, err = c.Do("PING")
require.NoError(t, err)
c.Close()
d.check("2", p, 2, 1, 0)
}
接口底层
接口的底层结构如下,它分为 tab 和 data 两个字段。
type iface struct {
tab *itab // 存储了接口的类型、接口中的动态数据类型、动态数据类型的函数指针等
data unsafe.Pointer // 存储了接口中动态类型的数据指针
}
接口能够容纳不同的类型的秘诀
- 存储当前接口的类型
- 存储动态数据类型
- 存储动态数据类型对应的数据
- 动态数据类型实现接口方法的指针。
这种为不同数据类型的实体提供统一接口的能力被称为多态。实际上,接口只是一个容器,当我们调用接口时,最终会找到接口中容纳的动态数据类型和它所对应方法的指针,并完成调用。
接口成本
由于动态数据类型对应的数据大小难以预料,接口中使用指针来存储数据。
同时,为了方便数据被寻址,平时分配在栈中的值一旦赋值给接口后,Go 运行时会在堆区为接口开辟内存,这种现象被称为内存逃逸,它是接口需要承担的成本之一。
内存逃逸意味着堆内存分配时的时间消耗。
接口的另一个成本是调用时查找接口中容纳的动态数据类型和它对应的方法的指针带来的开销。
这种开销的成本有多大呢?
这里我们用一个简单的 Benchmark 测试来说明一下。在下面这个例子中,BenchmarkDirect 测试直接调用调用的开销。BenchmarkInterface 测试进行接口调用的开销,但其函数接收者是一个非指针。BenchmarkInterfacePointer 也是测试接口调用的开销,但其函数接收者是一个指针。
package escape
import "testing"
type Sumifier interface{ Add(a, b int32) int32 }
type Sumer struct{ id int32 }
func (math Sumer) Add(a, b int32) int32 { return a + b }
type SumerPointer struct{ id int32 }
func (math *SumerPointer) Add(a, b int32) int32 { return a + b }
func BenchmarkDirect(b *testing.B) {
adder := Sumer{id: 6754}
b.ResetTimer()
for i := 0; i < b.N; i++ {
adder.Add(10, 12)
}
}
func BenchmarkInterface(b *testing.B) {
adder := Sumer{id: 6754}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sumifier(adder).Add(10, 12)
}
}
func BenchmarkInterfacePointer(b *testing.B) {
adder := &SumerPointer{id: 6754}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sumifier(adder).Add(10, 12)
}
}
在 Benchmark 测试中,我们静止编译器的优化和内联汇编,避免这两种因素对耗时产生的影响。测试结果如下。可以看到直接函数调用的速度最快,为 1.95 ns/op, 方法接收者为指针的接口调用和函数调用的速度类似,为 2.37 ns/op, 方法接收者为非指针的接口调用却慢了数倍,为 14.6 ns/op。
N: Windows直接运行**go test -gcflags "-N -l" -bench=.
**好像不行
» go test -gcflags "-N -l" -bench=.
BenchmarkDirect-12 535487740 1.95 ns/op
BenchmarkInterface-12 76026812 14.6 ns/op
BenchmarkInterfacePointer-12 517756519 2.37 ns/op
go test -gcflags "-N -l" -bench=.
goos: linux
goarch: amd64
pkg: github.com/funbinary/go_example/example/crawler/benchmark
cpu: 12th Gen Intel(R) Core(TM) i7-12700F
BenchmarkDirect-20 1000000000 0.9737 ns/op
BenchmarkInterface-20 906270834 1.305 ns/op
BenchmarkInterfacePointer-20 1000000000 1.080 ns/op
PASS
ok github.com/funbinary/go_example/example/crawler/benchmark 3.587s
**方法接收者为非指针的接口调用速度之所以很慢是受到了内存拷贝的影响。**由于接口中存储了数据的指针,而函数调用的是非指针,因此数据会从对堆内存拷贝到栈内存,让调用速度变慢。
启发:
- 在使用接口时,方法接收者使用指针的形式能够带来速度的提升
- 接口调用带来的性能损失很小,在实际开发中,不必担心接口带来的效率损失
爬取技术
- 模拟浏览器访问
- 代理访问
爬取接口抽象
-
创建collect用于采集引擎, 存放与爬取相关代码
-
定义Fetcher接口,内部方法Get,参数为url N: 后续会修改,不用提前费劲设计
type Fetcher interface { Get(url string) ([]byte, error) }
-
定义一个结构体 BaseFetch,用最基本的爬取逻辑实现 Fetcher 接口:
func (BaseFetch) Get(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { fmt.Println(err) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Printf("Error status code:%d", resp.StatusCode) } bodyReader := bufio.NewReader(resp.Body) e := DeterminEncoding(bodyReader) utf8Reader := transform.NewReader(bodyReader, e.NewDecoder()) return ioutil.ReadAll(utf8Reader) }
-
在 main.go 中定义一个类型为 BaseFetch 的结构体,用接口 Fetcher 接收并调用 Get 方法,这样就完成了使用接口来实现基本爬取的逻辑。
var f collect.Fetcher = collect.BaseFetch{} body, err := f.Get(url)
模拟浏览器访问
-
反爬机制
-
User-Agent 字段: 表明当前正在使用的应用程序、设备类型和操作系统的类型与版本。
-
Mozilla/5.0 (操作系统信息) 运行平台(运行平台细节) <扩展信息>
-
我的浏览器
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
- Mozilla/5.0 由于历史原因,是现在的主流浏览器都会发送的。
- Windows NT 10.0; Win64; x64: 操作系统版本号。
- AppleWebKit/537.36: 使用的 Web 渲染引擎标识符。
- KHTML: 在 Safari 和 Chrome 上使用的引擎。
- Chrome/111.0.0.0 Safari/537.36: 浏览器版本号
-
不同浏览器,User-Agent会不同
Lynx: Lynx/2.8.8pre.4 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/2.12.23 Wget: Wget/1.15 (linux-gnu) Curl: curl/7.35.0 Samsung Galaxy Note 4: Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-N910F Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36 Apple iPhone: Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1 Apple iPad: Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4 Microsoft Internet Explorer 11 / IE 11: Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
-
有时候,我们的爬虫服务需要动态生成 User-Agent 列表,方便在测试、或者在使用代理大量请求单一网站时,动态设置不同的 User-Agent。
实现BrowserFetch
- 创建一个 HTTP 客户端 http.Client
- 通过 http.NewRequest 创建一个请求。
- 在请求中调用 req.Header.Set 设置 User-Agent 请求头。
- 调用 client.Do 完成 HTTP 请求。
type BrowserFetch struct {
}
func (b *BrowserFetch) Get(url string) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36\t\n")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.Errorf("Error status code:%v", resp.StatusCode)
}
r := bufio.NewReader(resp.Body)
e := DeterminEncoding(r)
utf8r := transform.NewReader(r, e.NewDecoder())
return io.ReadAll(utf8r)
}
远程访问浏览器
要借助浏览器的能力实现自动化爬取,目前依靠的技术有以下三种:
- 借助浏览器驱动协议(WebDriver protocol)远程与浏览器交互;
- 借助谷歌开发者工具协议(CDP,Chrome DevTools Protocol)远程与浏览器交互;
- 在浏览器应用程序中注入要执行的 JavaScript,典型的工具有 Cypress, TestCafe。
通常只用于测试,所以下面我们就重点来说说前面两种技术。
Webdriver Protocol
Webdriver 协议是操作浏览器的一种远程控制协议。借助 Webdriver 协议完成爬虫的框架或库有 Selenium,WebdriverIO,Nightwatch,其中最知名的就是 Selenium。Selenium 为每一种语言(例如 Java、Python、Ruby 等)都准备了一个对应的 clinet 库,它整合了不同浏览器的驱动(这些驱动由浏览器厂商提供,例如谷歌浏览器的驱动和火狐浏览器的驱动)。
Selenium 通过 W3C 约定的 WebDriver 协议与指定的浏览器驱动进行通信,之后浏览器驱动操作特定浏览器,从而实现开发者操作浏览器的目的。由于 Selenium 整合了不同的浏览器驱动,因此它对于不同的浏览器都具有良好的兼容性。
R: Selenium
Chrome DevTools Protocol(谷歌开发者工具协议)
该协议最初是由谷歌开发者工具团队维护的,负责调试、操作浏览器的协议。目前,现代大多数浏览器都支持谷歌开发者工具协议。我们经常使用到的谷歌浏览器的开发者工具(快捷键 CTRL + SHIFT + I 或者 F12)就是使用这个协议来操作浏览器的。
查看谷歌开发者工具与浏览器交互的协议的方式是:
-
打开谷歌浏览器,在开发者工具 →设置→ 实验中勾选 Protocol Monitor(协议监视器)。
-
我们要重启开发者工具,在右侧点击更多工具,这样就可以看到协议监视器面板了。
-
面板中有开发者工具通过协议与浏览器交互的细节。
与 Selenium 需要与浏览器驱动进行交互不同的是,Chrome DevTools 协议直接通过 Web Socket 协议与浏览器暴露的 API 进行通信,这使得 Chrome DevTools 协议操作浏览器变得更快。
在 Go 中实现了 Chrome DevTools 协议的知名第三方库是chromedp。它的操作简单,也不需要额外的依赖。借助chromedp 提供的能力与浏览器交互,我们就具有了许多灵活的能力,例如截屏、模拟鼠标点击、提交表单、下载 / 上传文件等。chromedp 的一些操作样例你可以参考example 代码库。
模拟鼠标点击事件
假设我们访问Go time 包的说明文档,例如 After 函数,会发现下图的参考代码是折叠的。
通过鼠标点击,折叠的代码可以展示出 time.After 函数的参考代码。
我们经常面临这种情况,即需要完成一些交互才能获取到对应的数据。要模拟上面的完整操作,代码如下所示:
package main
import (
"context"
"log"
"time"
"github.com/chromedp/chromedp"
)
func main() {
// 1、创建谷歌浏览器实例
ctx, cancel := chromedp.NewContext(
context.Background(),
)
defer cancel()
// 2、设置context超时时间
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()
// 3、爬取页面,等待某一个元素出现,接着模拟鼠标点击,最后获取数据
var example string
err := chromedp.Run(ctx,
chromedp.Navigate(`https://pkg.go.dev/time`),
chromedp.WaitVisible(`body > footer`),
chromedp.Click(`#example-After`, chromedp.NodeVisible),
chromedp.Value(`#example-After textarea`, &example),
)
if err != nil {
log.Fatal(err)
}
log.Printf("Go's time.After example:\\n%s", example)
}
-
首先我们导入了 chromedp 库,并调用 chromedp.NewContext 为我们创建了一个浏览器的实例。
实现原理: 查找当前系统指定路径下指定的谷歌应用程序,并默认用无头模式(Headless 模式)启动谷歌浏览器实例。
通过无头模式,我们肉眼不会看到谷歌浏览器窗口的打开过程,但它确实已经在后台运行了。func findExecPath() string { var locations []string switch runtime.GOOS { case "darwin": locations = []string{ // Mac "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", } case "windows": locations = []string{ // Windows "chrome", "chrome.exe", // in case PATHEXT is misconfigured `C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe`, `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe`, filepath.Join(os.Getenv("USERPROFILE"), `AppData\\Local\\Google\\Chrome\\Application\\chrome.exe`), filepath.Join(os.Getenv("USERPROFILE"), `AppData\\Local\\Chromium\\Application\\chrome.exe`), } default: locations = []string{ // Unix-like "headless_shell", ... } }
-
所以说,当前程序能够运行的重要前提是在指定路径中存在谷歌浏览器程序。当然,一般我们系统中可浏览的谷歌浏览器的大小都是比较大的,所以 chromedp 还好心地为我们提供了一个包含了无头谷歌浏览器的应用程序的镜像:headless-shell。
-
用 context.WithTimeout 设置当前爬取数据的超时时间,这里我们设置成了 15s。
-
第三步,chromedp.Run 执行多个 action,chromedp 中抽象了
action
和 **task
**两种行为。-
action : 爬取、等待、点击、获取数据这样的行为。
-
task 指的是一个任务,task 是多个 action 的集合。
-
因此,chromedp.Run 会将多个 action 封装为一个任务,并依次执行。
func Run(ctx context.Context, actions ...Action) error { ... return Tasks(actions).Do(cdp.WithExecutor(ctx, c.Target)) }
-
-
chromedp.Navigate 指的是爬取指定的网址:https://pkg.go.dev/time。
-
chromedp.WaitVisible 指的是“等待当前标签可见”,其参数使用的是 CSS 选择器的形式。在这个例子中,body > footer 标签可见,代表正文已经加载完毕。
-
chromedp.Click 指的是“模拟对某一个标签的点击事件”。
-
chromedp.Value 用于获取指定标签的数据。
想法: 使用这个库进行webrtc的压力测试
空接口
-
任何类型都隐式实现了空接口
-
通用的能力。
-
然而在处理接口的过程中却需要默默承受解析空接口带来的痛苦。
通过使用空接口,常见的 fmt.Println 函数提供了打印任何类型的功能。
如果不使用空接口,那么每一个类型都需要实现一个对应的 Println 函数,是非常不方便的。func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) }
-
不过,空接口带来便利的同时,也意味着我们必须在内部解析接口的类型,并对不同的类型进行相应的处理。以 fmt.Println 为例,Println 函数内部通过检测接口的具体类型来调用不同的处理函数。如果是自定义类型,还需要使用反射、递归等手段完成复杂类型的打印功能。
func (p *pp) printArg(arg interface{}, verb rune) { switch f := arg.(type) { case bool: p.fmtBool(f, verb) case float32: p.fmtFloat(float64(f), 32, verb) case float64: p.fmtFloat(f, 64, verb) case complex64: p.fmtComplex(complex128(f), 64, verb) .... }
-
对于跨服务调用的 API,使用空接口可以提高它们的扩展性。因为在这种场景下,修改 API 的成本通常比较高,服务器需要改造并发布新的 SDK,客服端还需要适配新的 SDK 并联调测试。
如下所示,在 Info 结构体中增加扩展类型 map[string]interface{},新的功能如果需要传递新的信息,当前服务甚至可以不用修改 API。type info struct{ ExtraData map[string]interface{} `json:"extra_data"` ... }
-
空接口为 API 带来了扩展性和灵活性
-
模块的内部处理增加了额外的成本。因为 API 内部处理空接口时使用了大量的反射,而反射通常比较消耗性能。在实际项目中,当我们 JSON 序列化一个复杂的结构体时,有时候会有上百毫秒的耗时。
-
空接口是实现反射的基础,因为空接口中会存储动态类型的信息,这为我们提供了复杂、意想不到的处理能力和灵活性。我们可以获取结构体变量内部的方法名、属性名,能够动态地检查函数或方法的参数个数和返回值个数,也可以在运行时通过函数名动态调用函数。这些能力不使用反射都无法做到。
反射实现sql query
func createQuery(q interface{}) string{
// 判断类型为结构体
if reflect.ValueOf(q).Kind() == reflect.Struct {
// 获取结构体名字
t := reflect.TypeOf(q).Name()
// 查询语句
query := fmt.Sprintf("insert into %s values(", t)
v := reflect.ValueOf(q)
// 遍历结构体字段
for i := 0; i < v.NumField(); i++ {
// 判断结构体类型
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
} else {
query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
}
case reflect.String:
if i == 0 {
query = fmt.Sprintf("%s\\"%s\\"", query, v.Field(i).String())
} else {
query = fmt.Sprintf("%s, \\"%s\\"", query, v.Field(i).String())
}
...
}
}
query = fmt.Sprintf("%s)", query)
fmt.Println(query)
return query
}
}
接口的陷阱
-
当接口中存储的是值,但是结构体是指针时,接口动态调用无法编译通过。如下所示:
type Binary struct { uint64 } type Stringer interface { String() string } func (i *Binary) String() string { return "hello world" } func main(){ a:= Binary{54} b := Stringer(a) b.String() }
Go 语言在编译时阻止了这样的写法,原因在于这种写法会让人产生困惑。如果转换为接口的是值, 那么由于内存逃逸,在转换为接口时必定已经把值拷贝到了堆区。因此如果允许这种写法存在,那么即便看起来在方法中修改了接口中的值,却无法修改原始值,这非常容易引起误解。
-
将类型切片转换为接口切片
func foo() []interface{} { return []int{1,2,3} }
编译时报错:Go 语言禁止了这种写法,就像前面所说的,批量转换为接口是效率非常低的操作。因为每个元素都需要完成内存逃逸的额外开销。
-
接口与 nil 之间的关系。当接口为 nil 时,接口中的动态类型 itab 和动态类型值 data 必须都为 nil,初学者常常会在这个问题上犯错。例如在下面的 foo 函数中,由于返回的 err 没有任何动态类型和动态值,因此 err 等于 nil。
func foo() error { var err error // nil return err } func main() { err := foo() fmt.Println(err == nil) // true }
然而,如果在 foo 函数中将错误类型定义为自定义类型,例如 *os.PathError ,我们会发现 err 不等于 nil。
func foo() error { var err *os.PathError return err } func main() { err := foo() fmt.Println(err != nil) // true }
这是因为当接口为 nil 时,代表接口中的动态类型和动态类型值都为 nil,而当前由于接口 error 具有动态类型 *os.PathError,接口的内部结构体 itab 不为空。如下图所示:
避免这一问题需要谨慎地使用自定义的 Error 作为定义,而更多的使用内置的 errors.New 或 fmt.Errorf 来生成和包裹错误。我在之后的课程还会详细介绍错误处理的最佳实践。
思考题
除了带方法的接口,其实还有可以容纳任何类型的空接口。你觉得他们分别在什么场合使用更好呢?
- 函数传参
- map值
在对接口方法进行设计时,一般有一个原则是方法参数应该抽象,例如为空接口。但是方法的返回值应该具体,例如为实际的结构体,你觉得这种设计正确吗?
如果一个网站需要登录才可以访问,我们应该如何实现自动登录的能力?
1、通常都有验证码,先获取验证码图片,然后识别为文字,然后带用户名密码及验证码请求登录接口。
2、有的是滑块,可以使用webdriver调用浏览器来完成; 其他生物识别的,可以用弹出浏览器然后人工登录后继续。 登录成功后保存cookie给后续的浏览器使用。
拓展
「此文章为3月Day13学习笔记,内容来源于极客时间《Go分布式爬虫实战》,强烈推荐该课程!/推荐该课程」
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/137591.html