本文是在实际开发中所遇到的问题以及知识点进行了罗列,也和其他的编程语言进行了比较.

Go中不支持的语法

1.Go不支持三元运算符
因为Go的设计者认为,三元运算很长的表达式中会有些难以理解,所以只能使用if-else控制

2.Go不支持结构体常量.

3.++运算符只能放在变量后面,而且不能被用于复杂表达式嵌套,但可以用于for循环的使用.这一点对类C语言的改进还是很重要的,使代码更加清晰.

4.if条件只能判断bool类型,或者表达式,int,float,string等类型无法做判断.也没有隐式的类型转换.

TestMain

在编写单元测试时可以使用TestMain在一个包中声明一个测试的主函数,每当测试该包时,可以执行一些初始化的操作例如涉及到数库日志文件路径初始化等可以放到该函数下.

1
2
3
4
5
6
func TestMain(m *testing.M) { 
mysql.InitDbPool()
retCode := m.Run()
mysql.ReleasePool()
os.Exit(retCode)
}

map与slice引用问题

Go语言当中存在引用的问题,但是没有引用传递,因为map和channel类型需要注意是指向runtime 类型的指针.

因此在运行中传参与指针传参一样,改变map,slice当中的值,该实际参数原来的值也会改变.
map,slice能够在函数当中更改,是因为slice本身是值拷贝,而内部保留了对底层数据结构的的引用.

Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.

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

func main() {
s := []int{1, 2}
fmt.Printf("%p\n", &s)//0xc42000a260
t(s)
fmt.Printf("%p\n", &s)//0xc42000a260
//这时的s是 [9900 2]
}

func t(s []int) {
fmt.Printf("%p\n", &s[0]) //0xc42000e260
s[0] = 9900 //将实参改为了 [9900 2]
s = []int{8800} //这个时候重新修改参数,不会影响实际参数
fmt.Printf("%p\n", &s)//0xc42000a280
}

上述例子中对t函数当中的slice重新赋值,不会影响实参,因为t函数的s变量有自己单独的地址.
小结:slice,map,channel是值传递,因为实参和形参的地址不是同一地址,但是内部保存了对原来内容的引用,修改内容会影响实参.本质是实参和形参都是独立的指针,指向了同一块数据,实际更改其实都是对数据内容的更改.

map与slice补充

1.[]T[]interface不能直接转换,官方文档说的是这两种在内存当中的存储是不一样的,因此不能只接转换,
详细请点击.

2.map是非原子操作类型,因此在多协程操作下无法保证Map类型的安全性.

3.如果需要深拷贝一个map或者slice类型,那么需要新建一个map,通过遍历要拷贝的map复制到新map当中.

array 类型

Go中还有一个不起眼的类型是array类型:数字类型组成,且有固定的长度.
一定要和map,slice以及channel区分开他们是引用类型.

array和上述类型存在本质区别,array类型是作为值类型.

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

import "fmt"

func main() {
a := [5]int{1, 2, 3, 4, 5}
t(a)
fmt.Println(a)//[1 2 3 4 5]
}

func t(a [5]int) {
a[0] = 10
fmt.Println(a)//[10 2 3 4 5]
}

创建方法时receiver是结构体还是指针?

官方推荐如果需要改变receiver,那么就定义为指针,如果不需要进行修改那么就定义为值.
如果结构体相当大,那么考虑采用只指针.
如果有些方法需要修改receiver,那么需要统一的将方法设置为指针传递.

注意:指针receiver所有的方法包含指针的receiver和非指针的receiver.而非指针的receiver只包含非指针receiver的方法集.

关于原子操作

Go很方便提供了sync/atomic包保证访问内存对象的原子性.
但是这是一种简单的方式,更高级的是采用协程配合管道来实现原子操作.
这样的方式设计起来和之前的方式不同.需要关注管道数据中流向,因此体出了更高设计理念.
不要使用共享内存方式进行通信,相反通过通信实现共享内存.
Do not communicate by sharing memory. Instead, share memory by communicating.

不由自已的赞叹Go的设计理念,提供更先进的编程方式.

接口类型的实现

接口类型有两个元素组成一个是类型一个是值.如果将int 3存储到空接口当中那么,实质上这个接口的类型为int,值为3.
声明一个接口类型类型默认是nil,值默认是nil.
当我们拿一个接口类型和nil比较的时候需要注意的是,nil接口类型是有nil类型和nil的值组成.
只要接口类型中的值或者类型不等于nil那么,就不能正确判断.
尤其是在返回自定义error类型的时候,这样是不能按照理解的意思去执行的.所以要返回一个接口类型的值如果为空,那么必须返回为nil.

示例:

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

type MyError struct{
error
test string
}

func returnErr() (e error) {
var mErr *MyError = nil
fmt.Println(reflect.TypeOf(mErr), reflect.ValueOf(mErr))//*main.MyError <nil>
if mErr == nil {//不是接口类型能正常判断
fmt.Println("=========== error true")//输出=========== error true
}
e = mErr

fmt.Println(reflect.TypeOf(e), reflect.ValueOf(e))//*main.MyError <nil>
if e == nil {
fmt.Println("=========== error true")//没有输出
}
return mErr
}

上述例子的e类型已经改变,变为 *MyError 类型,但值是nil所以不能等于nil.

map[string]interface{}

由弱类型语言转换到强类型的语言,必须有一个概念,类型不能轻易的变化,不能随意滥用.
例如PHP语言关联数组可以用于很多场景,因为PHP语言弱类型的特性这点优势很大.
但是对于Go语言而言,没有那么方便.

在关联数组可以简单的在GO中可以转换map[string]interface{}.
PHP:

1
2
3
arr = [
"a" => 10
]

Go

1
2
3
arr := map[string]interface{}{
"a" : 10,
}

当我们使用这个值时

PHP:

1
2
3
if(arr["a"] == 10){
//do somthing
}

Go:

1
2
3
if arr["a"].(int) == 10{
//do somthing
}

可以看到Go中使用类型断言来告诉编译器这是一个int类型,反观PHP当中没有这样的操作,因为PHP中不同的类型是可以比较的.

再来看一个二维数组例子,实际上会使阅读更加困难.

PHP:

1
2
3
4
5
6
7
8
arr = [
"b" => [
"c" => 0
]
]
if(arr["b"]["c"] == 10){
//do somthing
}

Go

1
2
3
4
5
6
7
8
arr := map[string]interface{}{
"b" : map[string]interface{}{
"c" : 0
}
}
if arr["b"].(map[string]interface{})["c"].(int) == 10{
//do somthing
}

可以看到上述的表达式一个简单的取值行为需要多次类型的断言.整个代码变的很长,且违背了Go语言的简洁性.因此map[string]interface{}的乱用,会使代码变的难以阅读,而且编译时不容易发现错误,转而导致运行时才可以发现错误.

package的相关问题

1.内部包
内部包规则定义:导入的路径包含"internal",当需要导入的代码不与"internal"父级目录为同一目录的不允许导入.

An import of a path containing the element “internal” is disallowed if the importing code is outside the tree rooted at the parent of the “internal” directory.

例: /a/b/c/internal/d/e/f,f包可以被/a/b/c导入,但无法被/a/b/g导入.
2.包目录
查找路径:
当前包下的vendor目录。
向上级目录查找vendor目录。
$GOPATH下面查找依赖包。
$GOROOT目录下查找

PS:GOOPATH为WorkSpace,GOROOT为Go的安装目录.
vendor可以嵌套vendor目录,这项新特性在Go 1.5以后支持,目的是为了解决不同工程依赖不同的包版本的问题.

目录如下:

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
31
32
33
34
35
36
├── css_test.go
├── main.go
└── vendor
├── github.com
│ ├── andybalholm
│ │ └── cascadia
│ │ ├── LICENSE
│ │ ├── parser.go
│ │ ├── README.md
│ │ └── selector.go
│ └── tdewolff
│ ├── buffer
│ │ ├── buffer.go
..................
│ └── parse
│ ├── common.go
│ ├── css
│ │ ├── hash.go
│ │ ├── lex.go
│ │ ├── parse.go
│ │ ├── README.md
│ │ └── util.go
│ ├── LICENSE.md
│ ├── README.md
│ └── util.go
├── golang.org
│ └── x
│ └── net
│ └── html
│ ├── atom
│ │ ├── atom.go
│ │ ├── gen.go
│ │ └── table.go
│ ├── const.go
......
└── vendor.json

3.使用go get时,安装的是包,如果要安装指定地址下所有包可以使用 go get test.com/... 安装test.com域名下的所有包,如果直接go get test.com/ 会报no buildable Go source files的错.

字符常量

我们在Go中可以表示写一个表达式:

1
time := 1 * time.Second

注意这时的1是一个无类型常量,而它的类型根据他的上下文确定(An untyped constant takes the type needed by its context.).
所以1就变成了time.Duration这个类型,这个类型实质是int64.

panic 与 recover

开发时,需要留意panic与recover的恢复机制:
假设在函数F运行时发生panic,任意在F函数当中被defer函数会正常执行.
然后F函数调用者defer的函数被执行,在执行的gorutine内如此循环到最上层,然后程序终止.

如果函数Fdefer的函数G中有recover函数,且没有新的异常抛出,那么程序会恢复执行.
只要在G之前defer的函数截止被执行,然后F函数返回调用方,F函数执行终止.

1
2
3
4
5
6
7
8
9
10
func protect(g func()) {
defer func() {
log.Println("done") // Println executes normally even if there is a panic
if x := recover(); x != nil {
log.Printf("run time panic: %v", x)
}
}()
log.Println("start")
g()
}

零值定义

Bool:false
数字类型:0
strings:“”
pointers, functions, interfaces, slices, channels, and maps: nil

1
2
3
4
5
6
type T struct { i int; f float64; next *T }
t := new(T)
//对应的空值:
t.i == 0
t.f == 0.0
t.next == nil

不能对map[key]运算地址操作符&

这时根据Map底层数据结构定义的,Map在内存当中由许多的bucket组成,当Map内部元素增加,删除或者更新,bucket会被重新排放.
因此Map中的元素没有固定的地址,因此取地址运算符是错误的.
油管Map详解视频地址

string,rune到底是什么

string底层是由字节数组组成([]byte),直接索引会造成读取错误.

1
2
3
4
const str := `⌘`
for i := 0; i < len(str); i++ {
fmt.Printf("%x ", str[i])// e2 8c 98
}

一个UTF8是有可变长的代码点(Codepoint)组成,PHP内部是与其一致的,但是JS内部索引会获得’字符.’

而rune本质是一个int32类型,大家都知道UTF8是由一个到三个字节(Codepoint)组成.足够存储一个UTF8字符.
如果真正需要索引字符可以通过[]rune去读取每个UTF8字符.

补充:
Go source code is always UTF-8.Go源码是UTF8,一个字符串可以拥有任意字节,缺少字节转义的字符串字符,就会一直保持有效的UTF8序列.
这些序列代表着Unicode代码点,被称作runes.不保证字符串中的字符是标准的UTF8字符.
A string holds arbitrary bytes.
A string literal, absent byte-level escapes, always holds valid UTF-8 sequences.
Those sequences represent Unicode code points, called runes.
No guarantee is made in Go that characters in strings are normalized.

调试

编译类语言一定要学会Debug,C语言有强大的gdb进行调试,gdb对于go支持情况不太好,但是也可以用.gdb的调试,官方博客写的很清楚.
还有一个很好用的Go调试工具叫做DELVE,命令保持了与gdb一致的风格但是对go语言支持更好一些.

multi-part表单处理的问题

处理表达请求时代码,NewWriter实体要及时调用Close方法,不要使用defer,否则会导致请求体表单丢失边界,引起服务端解析错误.

1
2
3
4
5
6
7
w := multipart.NewWriter(buffer)

for key, val := range params {
_ = w.WriteField(key, val)
}
w.Close()//不要使用defer
request,_ := http.NewRequest("post", postUrl, buffer)

解析的时候需要注意的是,调用方法要使用r.ParseMultipartForm(1024 * 1024),不要使用r.ParseForm().

常用技巧归总

1.在判断map是否存在key功能和PHP中的isset函数用法是一致的,即如果存在key,但是value值为nil也会返回true.

2.[]byte转字符串:string(byteArr[:])

3.根据命名规则需要按照effctive go的方式进行编写.例如在PHP当中的define("TEST", true)这样的定义在GO当中是不被推荐的,推荐的是驼峰式命名规则.

4.string.Split可以分割字符串 转换字符串数组

5.defer可以在函数返回(return)之后执行.

6.判断一个值到底是不是空需要具体定,如果判断map,slice,channel通过len()函数判断.

7.自定义的结构体可以通过这样的形式判断是否为空 : == 或者 reflect.DeepEqual()包括所有的属性都会深度遍历进行比较.

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

type MyStruct struct{
a int
}

func main(){
s1 := MyStruct{} //空对象
s2 := MyStruct{ a:10 }
if s2 != s1{
//true
}
}

指针类型的判断只有当两个指针指向同一个对象时,才会为true.
8. Json 解析对应的类型:

  • bool for JSON booleans,
  • float64 for JSON numbers,注意这里JavaScript定义的数字类型用float类型表示.
  • string for JSON strings, and
  • nil for JSON null.

9.switch 与case语句对齐(PS:case里可以放置表达式)