Go: 正确处理 net 包中的错误

Go 网络开发中的常用的应用层协议库都是基于 net 标准库开发的,比如 gorilla/websocket 和 go-grpc,还有最常用的 http 库。

当请求一个不存在的 host 时会返回一个错误,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat main.go
package main

import (
"fmt"
"net/http"
)

func main() {
if _, err := http.Get("http://unknownhost.example"); err != nil {
fmt.Println(err)
}
}
$ go run main.go
Get "http://unknownhost.example": dial tcp: lookup unknownhost.example: no such host

这个错误本质其实是 *net.DNSError,在 Go 早期开发中,会经常使用接口断言匹配错误。

比如这样 if v, ok := err.(*net.DNSError);ok {} 的形式。

如果这样使用的话,实际上不会匹配断言匹配到,在 debug 模式可以看到最后返回其实是 *url.Error,然后内部才实际包裹了 (*net.DNSError) 错误,并实现 Unwarp() error 方法。

1
2
3
4
5
6
// url.Error
type Error struct {
Op string
URL string
Err error // 上述例子中,这里包裹了 *net.DNSError 错误
}

在 go 1.13 之后,errors.As 可以进行解包错误,那么我们就可以直接使用下面方式即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"errors"
"fmt"
"net"
"net/http"
)

func main() {
if _, err := http.Get("http://unknownhost.example"); err != nil {
if v := (*net.DNSError)(nil); errors.As(err, &v) {
fmt.Println("can't found", v.Name)
}
}
}

除了 *net.DNSError 之外,还有下面内置错误结构体:

  • net.InvalidAddrError
  • net.UnknownNetworkError
  • net.AddrError
  • net.DNSConfigError

上面错误除了实现了 Error() 接口,还实现了 Timeout() boolTemporary() bool 接口,也就是实现了 net.Error 接口

1
2
3
4
5
6
// net.Error
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}

同时也实现了 Unwarp() 接口,用于错误解包。那么我们就可以区分请求的错误是网络错误,还是服务器错误。

比如在 jsonrpc 协议中定义了 Error 结构体,使用下面方式进行判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type JsonError struct {
Code int `json:"code"`
Msg string `json:"message"`
Data interface{} `json:"data"`
}

func (e *JsonError) Error() string {
return e.Msg
}

func main() {
if _, err := jsonrpc.Call("http://jsonrpc.example"); err != nil {
if v := (net.Error)(nil); errors.As(err, &v) {
fmt.Println("is timeout?", v.Timeout())
return
}

if v := (*Error)(nil); errors.As(err, &v) {
fmt.Println("jsonrpc error: Code:", v.Code, "Msg", v.Msg)
}
}
}

如果按照以前的方式不断通过接口断言的解包错误的形式,那么代码很快会形成类似”回调地狱“的模样。

所以,在实际应用层开发上,直接可以使用 fmt.Errorf("Stack: %w",err) 包装错误,最后调用方最后使用 errros.Aserrros.Is 解包按需处理错误。

solidity 处理智能合约转账的安全辅助函数

在智能合约中处理转账,最常见是 Ether 和 ERC20Token 的转账。

在转账 Ether 时,一般我们直接使用 <address payable>.transfer(uint256)(或者 send) 函数,但是这个函数有个限制,只能使用 2300 gas,而且不能调整,这样就会出现一个问题,如果在合约内转给另一个合约地址,合约内对 fallback 方法做了额外的操作,这样消耗的 gas 就增加,进而交易会被 revert。

如下所示,如果使用转到下面这个合约,额外带有一个 event 会有额外的 gas 消耗,那么 transfer(send) 固定的 2300 gas 限制一定会失败。

1
2
3
4
5
6
7
8
9
pragma solidity ^0.7.0;

contract Wallet {
event Deposit( address indexed sender,uint256 indexed amount );

receive() payable external {
emit Deposit(msg.sender, msg.value);
}
}

我们可以使用下面函数就行封装,使用 call 方法并指定 value 即可,这样就可以事先调用 rpc 的 eth_estimateGas 接口来动态调整 gas limit。

1
2
3
4
function safeTransferETH(address to, uint value) internal {
(bool success,) = to.call{value:value}(new bytes(0));
require(success, 'ETH_TRANSFER_FAILED');
}

对于ERC20 的转账,一般我们会使用接口将地址转化合约对象方式来直接调用转账方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.7.0;

interface ERC20 {
function transfer(address to, uint256 tokens) external returns (bool success);
}

contract Manager {
function ERC20Transfer(
Token _token,
address _to,
uint256 _value
) public returns (bool) {
return _token.transfer(_to, _value);
}
}

但这有个问题,部分ERC20合约是使用了没有返回值非标准的函数接口,但 Solidity 编译器会把函数调用的返回值进行转换成接口定义的 bool 值,非标准合约调用会造成 revert。

在旧版本的 ^0.4.22 版本的 solidity 可以使用下面方式检查,代码来源自sec-bit/badERC20Fix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function isContract(address addr) internal {
assembly {
if iszero(extcodesize(addr)) { revert(0, 0) }
}
}

function handleReturnData() internal returns (bool result) {
assembly {
switch returndatasize()
case 0 { // not a std erc20
result := 1
}
case 32 { // std erc20
returndatacopy(0, 0, 32)
result := mload(0)
}
default { // anything else, should revert for safety
revert(0, 0)
}
}
}

function asmTransfer(address _erc20Addr, address _to, uint256 _value) internal returns (bool result) {
// Must be a contract addr first!
isContract(_erc20Addr);
// call return false when something wrong
require(_erc20Addr.call(bytes4(keccak256("transfer(address,uint256)")), _to, _value));
// handle returndata
return handleReturnData();
}

当时的 address.call 方法调用只返回是否调用成功,所以需要加入很多汇编代码。但现在 address.call 方法除了返回是否调用成功外,还有了调用返回值。

1
<address>.call(bytes memory) returns (bool, bytes memory)

所以我们可以更加方便的进行数据判断:

1
2
3
4
5
6
function safeTransferERC20(address token, address to, uint value) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
// 如果返回值 data 不为空,那么解码为 bool 并判断是否为 true
require(success && (data.length == 0 || abi.decode(data, (bool))), 'ERC20_TRANSFER_FAILED');
}

对于 ERC20 这种调用方法,还需要注意一点,低级调用 call 方法,如果地址不是合约那么也会返回 true,所以这里为了安全,最好先保证地址为合约地址。

1
2
3
4
5
6
7
8
modifier OnlyContract(address token) {
assembly {
if iszero(extcodesize(token)) {
revert(0, 0)
}
}
_;
}

其它 ERC20 方法,类如 transferFrom 以及 approve 方法都可以同上面方式处理。

需要注意的是,这种处理 ERC20 的几个辅助函数也不是万能的,例如下面代码,transfer 调用了 _transfer() 方法,但是并没有返回值,所以始终会返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
function _transfer(address _from ,address _to, uint256 _value) internal returns (bool) {
require(_to != address(0));
require(_value <= _balances[msg.sender]);

_balances[_from] -= _value;
_balances[_to] += _value;
emit Transfer(_from, _to, _value);
return true;
}

function transfer(address to, uint value) public returns (bool) {
_transfer(msg.sender, to, value);
}

这种不规范的合约还有很多,最简单的处理方式是单独处理特殊合约。

solidity: 判断目标地址是否为合约地址的两种方法

目前常用两种方式判断目标地址是否为合约,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.7.0;

contract Contract {
function isContract_1(address token) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(token)
}
return size > 0;
}

function isContract_2(address account) internal view returns (bool) {
bytes32 codehash;
bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
assembly { codehash := extcodehash(account) }
return (codehash != 0x0 && codehash != accountHash);
}
}

第一种根据目标地址的代码是否为空判断,extcodesize 操作码 gas 消耗为 700。

另一种是根据目标代码 keccack256 的哈希来判断,参考 EIP1052,空值哈希正好是 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470,extcodehash 操作码 gas 消耗为 400。

Go: 按位清除运算符(bit clear)

在 Go 语言中有个特殊的位运算符,使用 &^ 符号表示,包含与操作和取反操作。

《Go语言圣经》里面有对此的描述,z = x &^ y,如果 y 中比特位为 1 那么 z 对应比特位值就是 0,否则 z 就使用 x 对应位置的比特位值。

这种解释方法太绕了,实际执行方式是 x 和 y取反后的值进行与操作。实际应用中,如果我们需要一次清除多个比特位的,就可以使用这个运算符。

如下所示,我们定义了类似 unix 文件权限的枚举值,如果我们需要移除 Read 和 Write 权限,那么可以这样做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
const (
Read byte = 1 << iota
Write
Execute
)

var f1 = Read | Write | Execute
var f2 = Read | Write

var f = f1 &^ f2

fmt.Printf("%03b &^ %03b = %03b\n", f1, f2, f)

// Output:
// 111 &^ 011 = 100
}

证书中CommonName和SubjectAlternativeName

这是 github.com 的 https 证书解码后的数据,通过 Subject 字段可以看到证书所有者的基本信息,其中 CN 是 CommonName 的简写,中文常称作通用名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0a:06:30:42:7f:5b:bc:ed:69:57:39:65:93:b6:45:1f
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 Extended Validation Server CA
Validity
Not Before: May 8 00:00:00 2018 GMT
Not After : Jun 3 12:00:00 2020 GMT
Subject: businessCategory=Private Organization/jurisdictionC=US/jurisdictionST=Delaware/serialNumber=5157550, C=US, ST=California, L=San Francisco, O=GitHub, Inc., CN=github.com
X509v3 extensions:
X509v3 Authority Key Identifier:
keyid:3D:D3:50:A5:D6:A0:AD:EE:F3:4A:60:0A:65:D3:21:D4:F8:F8:D6:0F

X509v3 Subject Key Identifier:
C9:C2:53:61:66:9D:5F:AB:25:F4:26:CD:0F:38:9A:A8:49:EA:48:A9
X509v3 Subject Alternative Name:
DNS:github.com, DNS:www.github.com

当浏览器进行 TLS 握手时是不是通过 CommonName 来判断证书是属于当前域名 github.com 的呢?

答案是否定的,而是通过 SubjectAlternativeName 主题替换名称来确定的。 我们看到 X509 SubjectAlternativeName 中包括了 github.comwww.github.com 两个域名,那么就可以用来于这两个域名的 https 证书。

其实有过使用通用名称作为证书验证的,但是通用名称只是一个字段,并不能给多个域名颁发证书。

除了给域名颁发证书之外,还可以给 ip 颁发证书,可以访问 https://1.1.1.1 看到这个浏览器是正常的,并没有报错,下面去掉大部分信息后,可以看到证书给 ipv4 和 ipv6 都有颁发证书。

1
2
3
4
5
6
7
8
9
10
11
Certificate:
Issuer: C=US, O=DigiCert Inc, CN=DigiCert ECC Secure Server CA
Validity
Not Before: Jan 28 00:00:00 2019 GMT
Not After : Feb 1 12:00:00 2021 GMT
Subject: C=US, ST=California, L=San Francisco, O=Cloudflare, Inc., CN=cloudflare-dns.com

X509v3 Subject Key Identifier:
70:95:DC:5C:A3:8E:66:07:DB:CB:81:10:C6:AB:E7:C3:A8:45:7F:A0
X509v3 Subject Alternative Name:
DNS:cloudflare-dns.com, DNS:*.cloudflare-dns.com, DNS:one.one.one.one, IP Address:1.1.1.1, IP Address:1.0.0.1, IP Address:162.159.132.53, IP Address:2606:4700:4700:0:0:0:0:1111, IP Address:2606:4700:4700:0:0:0:0:1001, IP Address:2606:4700:4700:0:0:0:0:64, IP Address:2606:4700:4700:0:0:0:0:6400, IP Address:162.159.36.1, IP Address:162.159.46.1

除此之外,我们 golang/x509 包可以看到,还可以给 email 和 url 进行颁发证书:

1
2
3
4
5
6
7
// Subject Alternate Name values. (Note that these values may not be valid
// if invalid values were contained within a parsed certificate. For
// example, an element of DNSNames may not be a valid DNS domain name.)
DNSNames []string
EmailAddresses []string
IPAddresses []net.IP // Go 1.1
URIs []*url.URL // Go 1.10

如果使用 CommonName 而没有 SAN ,HTTPS 是无法成功握手的。另外 common name 也可以填写其它任意的字符串,通用名称本质上已经成了一个名称,并没有特殊的作用了。

ECMA2020: Optional Chaining & Nullish Coalescing

下面这个错误在 JavaScript 中最为常见

unable to get property of undefined or null reference

通常为了解决这个问题,我们可以使用 && 操作符来取对象属性值。

1
var street = user.address && user.address.street;

不过当数据很长的时候就特别麻烦,ECMA2020 通过了一项提案Optional Chaining,用语法糖的形式解决了这个问题:

1
var street = user?.address

当 user 为 null 或者 undefined 的时候直接返回,而不继续往后取值。

除了取属性外,还支持取索引、取方法、当做函数运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
a?.b                          // undefined if `a` is null/undefined, `a.b` otherwise.
a == null ? undefined : a.b

a?.[x] // undefined if `a` is null/undefined, `a[x]` otherwise.
a == null ? undefined : a[x]

a?.b() // undefined if `a` is null/undefined
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function
// otherwise, evaluates to `a.b()`

a?.() // undefined if `a` is null/undefined
a == null ? undefined : a() // throws a TypeError if `a` is neither null/undefined, nor a function
// invokes the function `a` otherwise

不过需要注意的是,以下场景并支持

  1. 构造函数:new a?.()
  2. 模板函数: a?.`string`
  3. 属性赋值:a?.b = c
  4. 可选父类: super?.(), super?.foo

目前可以在 TypeScript3.7 以及 Chrome 80 canary 中使用。

有些时候我们需要给属性赋于默认值,如下所示,height 属性的默认值为 400。

1
2
3
4
const setting = {
height: 100
}
const height = settting.height || 400

但是在 js 下这样做是不对的,因为如果 height 值为 0,那么也因为 0 隐式转化为布尔值是 false 而取值 400,这个不是我们想要的结果。

我们这样改:

1
const height = setting.height == null ? 400 : setting.height

这样当 height 值为 undefined 或者 null 都可以得到想要的结果,这里使用 null 和 undefined 在非严格等于下互等,而不和其他值相等的特性。但是这个会被大部分 linter 报错,如果使用严格等于的话,又会特别长:

1
const height = ( setting.height === null || setting.height === undefined) ? 400 : setting.height

新的提案 Nullish Coalescing 解决了个问题:

1
const height = setting.height ?? 400

目前也是可在 TypeScript3.7 或 Chrome Cannay 80 中使用。

Go: 显式声明某个类型实现了一个接口

Go 中没有面向对象的 implements 关键字,一个类型实现了一个接口是隐式的。

其实显式声明主要为了让编译器提前判断接口是否被实现而已。

所以在 Go 中还是有很多方式的。

如下所示 *UserCacher 实现了 Cacher 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Cacher key-value for string
type Cacher interface {
Get(string) (string, error)
Set(string, string) error
}

// UserCacher users cacher
type UserCacher struct {
}

// Get value by key
func (u *UserCacher) Get(key string) (string, error) {
return "", nil
}

// Set value by key
func (u *UserCacher) Set(key, value string) error {
return nil
}

为了让编译器提前判断的话,可这样做:

1
2
// Verify that *UserCacher implements Cacher
var _ Cacher = (*UserCacher)(nil)

或者在一个“构造函数”内实现:

1
2
3
func NewUserCacher() Cacher {
return &UserCacher{}
}

分享:你可能不知道的Go

本文是根据我在公司分享的《你可能不知道的 Go(上)》而整理的文章

本次分享的主题是《你不知道的 Go》,其实这个名称来源自《你不知道的 JavaScript》这本书,当然起这个名字有些“妄自尊大”,再者这次分享确实没有什么“高大上”的内容,只是一些大部分初学者不会注意到的盲点,所以之后我就改名为《你可能不知道的 Go》。(笑)

Type

首先,第一个话题是 Go 中的类型。如下面的代码所示,我们声明了一个新类型 Number,当然平时我们都是声明一个新的 struct 或者 interface。

1
2
3
4
5
6
7
8
9
10
11
type Number float64

var i float64
var j Number

// Error: cannot use i (type float64) as type Number in assignmentgo
j = i

// OK
j = Number(i)
j = 1.02

对于为什么 i 和 j 不能互相赋值,大部分 Gopher 都能说出,因为二者是不同的类型,而 Go 是强类型语言,不能隐式转换,所以二者不能进行赋值。

通过这个例子可以说明,在 Go 中使用 type 就可以声明一个新类型,而不同的类型名称就代表不同的类型,即使他们底层类型是一致的。到这里,基本上所有的文档或者教程都对此有基本的说明。

再说一个例子,如下所示:

1
2
3
4
5
6
7
type Hash []byte
var i []byte
var j Hash

// OK!
i = j
j = i

Hash 类型 i 和 []byte 类型 j 应该是不同的类型,根据我们上一页的说明,二者应该是不能互相赋值的,为什么这里就没有编译错误?

回答这个问题很简单,因为我们自以为的结论是错的,这里的 i 和 j 是同一类型(identical)。

这样说,肯定会有人疑惑,为什么在这里就是同一类型的呢?因为 []byte 是一个未声明的类型,对于未声明的类型,他们是没有名字的,所以只要其底层类型一致就可以,比如上面的 slice,元素都是 byte ,所以二者是相同的类型,可以互相赋值。

为什么说 []byte 是未声明的类型,而 float64 则是已声明的类型,其实这个已经是定义好的了,我们可以在官方文档找到所有的已定义的类型。

image

这里补充一句,大家可以看到 int 也是定义的类型,而不是关键字,而在 Java 和 c++ 是关键字的,所以下面的代码是不会编译失败的,而且运行也是成功的:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

var int = "相信我,这里不会编译错误"

func main() {
fmt.Println(int)
}

接着说未声明的类型。其它的比如 map array 等与其长度等定义相关,不能直接定义(长度的选择是无限的),所以这些都是未定义类型。我们可以在官方 spec 找到如何判断未定义类型二者是否类型一致:

  • 具有相同元素类型长度的 array
  • 具有相同元素类型的 slice
  • 具有相同键值类型的 map
  • 具有相同元素类型和传送方向的 channel
  • 具有相同字段序列 (字段名、类型、标签、顺序) 的匿名 struct
  • 签名相同 (参数和返回值,不包括参数名称) 的 function
  • ⽅法集相同 (方法名和方法签名相同和次序无关) 的 interface

不过上面还没有解释为什么 1.02 可以直接赋值给 Number。其实是因为 1.02 是一个无类型的浮点数字面量,注意这里的无类型是说没有类型名称。所以 1.02 可以代表底层类型为 float64 的 Number 类型,如果使用字符串字面量给其赋值就会编译失败。

关于这里可赋值性的概念,在官方文档中也有说明,基本就是上面所说明的内容,具体的大家可以看文档。

讲完这一段,大家就可以回答这个问题了,下面的的代码中赋值语句,哪个是对的,哪个是错的。

1
2
3
4
5
6
7
8
9
10
type Type0 []string
type Type1 []string

var x []string
var y Type0
var z Type1

y = x
z = x
y = z

第一个, y = x 由于 x 是未声明类型,而且与 y 底层类型一致,所以是可赋值的;第二个和第一个的原因一样;第三个,不能赋值,因为 y 和 z 是不同的类型。

当然这种方式会有很大的困扰,如果就想定义一个代表 float64 的 Number 类型怎么办?

也很简单,可以使用类型别名,类型别名代表二者类型完全一致。

1
2
3
4
5
type Number = float64
var i float64
var j Number
// OK
i = j

其类型方法也可以直接调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

type Mutex struct {
}

func (m *Mutex) Lock() {}
func (m *Mutex) Unlock() {}

type PtrMutex = *Mutex
type NewMutex = Mutex

func main() {
var a PtrMutex
a.Lock()

var b NewMutex
b.Lock()
}

在 go 也有内置的类型别名,比如说代表一个字节的 uint8 的别名就是 byte,而代表一个 unicode code pint 的就是 int32。

Slice

今天的第二个主题是 Slice,也就是切片。首先大家看下这段代码,函数参数接收一个 slice,然后在函数内部进行 append,最后输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func add(i []int) {
i = append(i, 3)
}
func main() {
i := []int{1, 2}
add(i)
fmt.Println(i)
}

这个其实在 Twiiter 上有过类似的投票,选择输出 [1 2 3] 居多,大多数人留言说 slice 是引用类型,应该会修改原有的数据。

最后的打印结果是 [1 2],也就是说 slice 不是引用类型。准确地说,Go 里面参数传递只有拷贝传递,没有引用传递,更不会有引用类型之说。

其实 slice 是一个称之为胖指针的数据结构。什么是胖指针?看下面这副图,这个是 slice 的内部结构,可以看到除了一个指针之外,还有长度和容量两个字段,记录指针对象额外信息的结构就可以称之外胖指针。

go-slice

从这张图,然后我们再次分析下上一页的代码,由于 go 只有拷贝传递,所以 slice 内部的结构都会被赋值一份。当 append 的时候会插入一个元素,首先检查这里容量是否足够足够,这里因为直接定义的 slice 有两个元素,所以容量也是 2,所以会新开辟一块新的,然后函数内部 i 变量指向这块内存,而外部变量 i 还是指向原先的内存。

好了,如果我们假设有足够的容量呢,如下所示,append 会不会修改?也不会。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func add(slice []int) {
slice = append(slice, 1, 2)
}
func main() {
slice := make([]int, 1, 10)
add(slice)
fmt.Println(slice)
}

为什么?append 增加元素的同时,一定会修改内部长度字段,而又因为 go 只有拷贝复制,长度变化不会影响到外部的长度,外部仍旧是长度为 1 的切片。

那如何修改 slice 呢?有两种方式,第一种传递指针,那么整体修改肯定会修改外部;另一种是大家常见的修改索引位置的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

func main() {
data := []string{"1", "2"}

var changeItem = func(data []string) {
data[0] = "10"
}
changeItem(data)

var changeItemByPoint = func(data *[]string) {
*data = append(*data, "3")
}
changeItemByPoint(&data)
}

除了现在这种使用 make 或者从数组内截取的或者使用字面量定义 slice,go 语言中还有一个结构也是 slice,那就是函数的可变参数。

如下所示,根据上述说明也会修改原有的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
data := []string{"good", "evening"}
test(data...)
fmt.Println(data)
}

func test(args ...string) {
args[0] = "hello"
args[1] = "world"
}

那么如果我们简单的一个一个传递会怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
data := []string{"good", "evening"}
test(data[0], data[1])
fmt.Println(data)
}

func test(args ...string) {
args[0] = "hello"
args[1] = "world"
}

如果以这样方式传递的话,go 会先构建一个 slice,然后按照顺序存储参数,所以这种不会影响原有数据。

string 类似 slice,不过内部并没有 cap 容量这个字段,主要是因为字符串内部是不可变的。

image

通过上图的字符串内存结构可以看到,底层也是一个字节数组。

如果使用 for…range 的方式遍历字符串,每个元素是不是一个 byte 呢?这个不是的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"reflect"
)

func main() {
raw := "我喜欢Go语言"
for i, v := range raw {
fmt.Println(i, reflect.TypeOf(v), v)
}
// 0 int32 25105
// 3 int32 21916
// 6 int32 27426
// 9 int32 71
// 10 int32 111
// 11 int32 35821
// 14 int32 35328
}

根据输出,我们可以看到,每个元素都是 int32,根据我们上面所说明的,int32 有个别名是 rune,而 rune 是一个 unicode code point,可以表示字符。

如果想打印输入一个字符,而不是一个数值,那么可以使用 string 将 v 进行转换即可。

大家也注意到,虽然每个元素都是按照 rune 来输出的,但是索引却不是,如果我们要一个 rune 对应一个索引的话,可以使用下面的方式,将 string 转化为 []rune:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
raw := "我喜欢Go语言"
for i, v := range []rune(raw) {
fmt.Println(i, string(v))
}
// 0 我
// 1 喜
// 2 欢
// 3 G
// 4 o
// 5 语
// 6 言
}

当然上面所说仅对 for…range 的方式有效,如果用 for 循环的方式,根据 string 长度,这个时候每个元素就会是 byte 类型。

Error

go 的 error 是一个接口,接口的零值是 nil。

1
2
3
type error interface {
Error() string
}

其实大家也知道除了接口外,函数、指针等的零值也是 nil。

这个引发了一个问题,新手经常会犯的错误,也就是所谓的 nil error != nil

我们看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string {
return "any error"
}

func test() error {
var myErr *MyError
return myErr
}

func main() {
fmt.Println(test() == nil) // false
}

为什么呢?不是说指针的零值也是 nil 么?

我们先看下接口的内存接口,其实 go 中的接口结构类似 Java 中的对象,都包括元数据和数据的指针。

image

看到这里,我们再分析下之前的代码,test 函数中,myErr 是一个 MyError 类型的指针,但是这个数据并不是一个接口类型,需要进行转化成接口,那转换成接口怎么处理呢,就是上述我们说的填充元数据和数据的指针。

这样说大家就应该明白了,非接口的 nil 的数据在转换成接口的时候就不是 nil 了。

Enum

今天的最后一个话题是枚举,大家应该都觉得这个很简单,没什么可说的。

嗯,确实是这样。不过,大家可以试试这个,说下面的所有枚举值是多少。

image

我觉得大多数人第一次看到这个应该懵逼的,当然我也是这样。

首先我们看下是否能编译,这里有多个 iota ,还有一个 float64 的枚举,实话讲,刚看到这个对于能不能编译我也不能确认。这里是能编译成功的。

然后再看具体的值,首先 x 一定是可以确认的,是 0,那么 y 呢?是 0 还是 1 呢?有经验的小伙伴一定知道还是 0。

那么知道这个的话,下面就好说了,a b c 分别代表 0 1 2。

接下来,下面的 d 已经定义,那么就是 1,那 e 呢,是 1 还是 2?正确答案是 1,因为 d 占据了 iota 为 0 的位置,f 那么就是 2。

接下来,g 是 100,那么 h 呢?正确答案是和 g 一样 100。之后 i 呢? iota 又回到了正确的位置,代表其位置,变成了 5。

接下来,看最后一列,j 和 k 根据上面可以推断出为 0 和 1。那么 l 呢?正确的是 2,属于同一列的变量都是 iota 的位置的数据。

关于 iota 的说明,大家可以在 spec 中找到详细说明,这里就不再赘述了。

OK,这就是今天所有分享的内容,感谢大家参加!

Win10 Chrome播放视频非常卡顿?

最近我发现用 WindowsChrome 的时候,播放视频非常卡顿,视频就像一帧一帧的播放一样,但是 Mac 下没有这样的问题。

找了好久的解决方案,重装 Chrome,禁用所有 Chrome 插件,安装新显卡驱动都试过,都没解决。

最后我尝试禁用了 Chrome 的硬件加速功能,没想到解决了。

最后给出具体解决方式:

打开 Chrome 设置,搜索“硬件”,关闭“使用硬件加速模式(如果可用)”,然后重启 Chrome。

MySQL upsert操作

upsert 操作是说如果这条记录存在(含有 unique key 或 primary key 的字段存在)那么就更新,否则就插入一条新的记录。

首先创建一张新表,包含一个主键约束 id

1
2
3
4
CREATE TABLE users(
id int unsigned primary key auto_increment,
name varchar(2) not null
);

然后插入多条数据:

1
INSERT INTO users(name) VALUES ('a'), ('b'), ('c');

如果现在要插入 id 为 2 的记录,那么就会报错:

1
2
mysql> insert users values(2,"test");
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'

如果简单改一下,就不会报错了:

1
INSERT INTO users(id, name) values (2, "test") on duplicate key update name="test";

或者将 update 设置为取 values

1
INSERT INTO users(id, name) values (2, "test") on duplicate key update name=VALUES(name);

提示 Query OK, 2 rows affected (0.01 sec)

查看一下数据,确实也更新了:

1
2
3
4
5
6
7
mysql> select * from users where id = 2;
+----+------+
| id | name |
+----+------+
| 2 | test |
+----+------+
1 row in set (0.00 sec)

如果插入一条不会重复 id 的记录的话,就正常 insert 了:

1
INSERT INTO users VALUES(4, 'xiaoming') ON DUPLICATE KEY UPDATE name=VALUES(name);

提示:Query OK, 1 row affected (0.01 sec)