博客
HiGrid is one of the best jQuery grid plugin!
别的语言都有异常处理语法,以 python 的异常处理语法为例:
try:
# do something
raise Exception
except KeyError as e:
pass
except Exception as e:
pass
else:
pass
finally:
pass
Golang 中并没有如此语法
Golang 中错误和异常并不是一码事
type error interface {
Error() string
}
错误是实现了 error 接口的对象。
异常是由内置函数 func panic(v interface{}) 发起的,可以被 func recover() interface{} 函数捕获
第三方或内置库函数最后一个函数返回 error 对象,调用者可以查看 err != nil 来判断本次调用是否成功执行,这会导致大量判断是否发生错误的冗余代码,开发中应该把处理错误逻辑相同的部分抽取出来。如果对是否发生错误不关心可以使用 _ 来接收返回错误。(PS:_ 在编译时会处理掉)
而第三方或内置库函数不会 panic 一个异常,让调用者 recover。
如果 panic 没有被 recover,那么有可能会挂起整个程序。
panic 应该在包内 recover,panic 只能由当前 goroutine recover。panic 目的就是为了解决:
遇到了在本包中无法处理的异常,一个跳出多个调用栈,再被外围的 recover 捕获,该包再返回调用者 error
recover 只能够在 defer 中使用,func recover() interface{} 返回的是 func panic(v interface{}) 的参数 v。如果没有 panic 但是调用了 recover,recover 返回的是 nil,多次调用 recover 不会有副作用。
defer 是一个先出后出的调用栈,参数的赋值在 defer 语句处已经完成,无论该函数是正常的执行,或返回 error,或 panic,defer 中的函数都能够保证得到执行。常用于 recover 捕获 panic、关闭文件资源、释放锁等等。
func main() {
}
func f(i int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(i)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
output:
main start
Calling g.
Printing in g 2
Printing in g 3
Panicking
Defer in g 3
Defer in g 2
Recovered in f 4
main end
尝试一下在别的 goroutine 中处理 panic,改造一下上面的 demo,defer recover 放到 main,然后使用 goroutine 执行 f(i) 函数。
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("main start")
go f(2)
// 当前 goroutine 睡眠,等待 panic
time.Sleep(time.Second)
fmt.Println("main end")
}
func f(i int) {
fmt.Println("Calling g.")
g(i)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
output:
main start
Calling g.
Printing in g 2
Printing in g 3
Panicking
Defer in g 3
Defer in g 2
panic: 4
goroutine 5 [running]:
exit status 2
panic 并没有被解决,一直抛到最上层,使整个程序崩溃,fmt.Println("main end") 并没有被执行,panic 只能由当前 goroutine 解决。
func g(i int) 函数使用了递归调用本身,也就是 panic 发生在 defer 之前,但是 defer fmt.Println("Defer in g", i) 还是成功执行了。
尝试把 recover 移除 defer,看看发生什么
func main() {
fmt.Println("main start")
f(2)
fmt.Println("main end")
}
func f(i int) {
fmt.Println("Calling g.")
g(i)
fmt.Println("Returned normally from g.")
r := recover()
fmt.Println("Recovered in f", r)
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
output:
main start
Calling g.
Printing in g 2
Printing in g 3
Panicking
Defer in g 3
Defer in g 2
panic: 4
goroutine 1 [running]:
exit status 2
Ooops
函数返回 error 中有一个坑,看以下代码,我们自定义了一个 MyError 实现了错误 error 接口:
func main() {
if err := try(); err != nil {
fmt.Println("err != nil")
}
}
type MyError struct {}
func (e *MyError) Error() string {
return "Oooops"
}
func try() error {
var err *MyError = nil
if false {
err = new(MyError)
}
return err
}
无论运行多少次,运行结果都是 err != nil。
原因:
error 是一个接口类型,其中接口类型的存储有:data 和 type,函数返回会执行:
var err *MyError = nil
var returnError error = err
经过这个赋值后,returnError 中存储的 data==nil,但是 type==*MyError,returnError 已不再是 nil。
那该如何办?
errors.New() 创建一个错误,其在 errors/errors.go 中的剪短源码如下:package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
type MyError struct {}
func (e *MyError) Error() string {
return "Ooops"
}
func main() {
err := CallMe()
if err != nil {
fmt.Println(err.Error())
}
}
// 大写 C 开头,被外部调用
func CallMe() error {
err := deal()
if err != nil {
errors.New("Some msg")
}
return nil
}
// 小写 d 开头,只能被内部使用
func deal() (e *MyError) {
if false {
e = new(MyError)
return
}
return
}
今天学习 Golang 文件操作实践时,当我创建一个文件(夹)出现文件权限与我代码设置不一致的问题
以下为我创建文件夹的代码:
func main() {
err := os.MkdirAll("g10guang/t1/t2", 0666)
if err != nil {
fmt.Println(err)
}
}
output:
mkdir g10guang/t1: permission denied
g10guang 文件夹创建成功,可是下面的 t1 文件夹创建失败
通过 ls -l 命令查看 g10guang 文件夹信息
drw-rw-r-- 2 g10guang g10guang 4096 Feb 8 13:23 g10guang
比如:
drw-rw-r-- 2 g10guang g10guang 4096 Feb 8 13:23 g10guang
开头有 10 个字符,我们来一一分析:
第一个 d 代表这是一个文件夹,- 为文件,l 为链接
当前用户与文件关系分为三个等级:
而后面的 9 个字符分别代表文件所有者、同组用户、其他用户对文件的读写权限
r 读权限w 写权限x 执行权限- 不具有该权限rwx ==> 111
rw- ==> 110
r-- ==> 100
--- ==> 000
如权限 -rwxrw-r-- 表示这是一个文件,文件所有者拥有读写执行权限、同组用户有读写权限、其他用户只有读权限
可以通过 chmod 改变一个文件的权限,或者 chown 改变文件的所有者
我们创建文件夹时候指定的权限是 0666,这是一个八进制数字,拆分成二进制为 110110110,也就是 rw-rw-rw-,但是为什么我们创建的文件夹的权限是 rw-rw-r-- 也就是 0664 呢?
其实我们创建文件夹、文件时候都是经过 Linux 系统调用完成的,所以这是 Linux 创建文件时候会把用户指定的权限 - 减去 umask,而我本地 umask 为 002。通过 umask 命令查看 umask
通过 g10guang 用户运行代码,文件夹当然所属于 g10guang
尝试 cd g10guang,出现错误,错误信息:cd: permission denied: g10guang
文件夹是属于我的,我怎么不能够 cd 呢?
原因是:需要 cd 到一个文件夹,那么必须要有当前文件夹的执行权限 x,Linux 下设计一切皆文件,文件夹也是一个特殊的文件
如果使用 os.MkdirAll 方法创建文件夹时,必须基于文件夹所有者 x 执行权限以及 w 写权限,为什么需要写权限,可以通过 vim 去打开一个文件夹,可以看到文件夹里的信息(记得吗,文件夹也是文件),比如我一个文件夹通过 vim 打开的信息:
" ============================================================================
" Netrw Directory Listing (netrw v155)
" /home/g10guang/Templates/blog
" Sorted by name
" Sort sequence: [\/]$,\<core\%(\.\d\+\)\=\>,\.h$,\.c$,\.cpp$,\~\=\*$,*,\.o$,\.obj$,\.info$,\.swp$,\.ba
" Quick Help: <F1>:help -:go up dir D:delete R:rename s:sort-by x:special
" ==============================================================================
../
./
.git/
.sass-cache/
_posts/
_site/
g10guang.github.io/
.gitignore
.gitmodules
404.html
Gemfile
Gemfile.lock
_config.yml
about.md
index.md
文件夹中记录着里面有哪些文件以及文件夹,其中 xxx/ 有 / 结尾的是文件夹,其他的是文件,所以在文件夹中创建一个文件夹需要改变文件夹信息,需要有写文件夹的权限
两种方法:
umask 后再创建文件,其后再把 umask 改为原来的 umask改变 umask 后再创建文件,其后再把 umask 改为原来的 umask
import (
"os"
"fmt"
"syscall"
)
func main() {
mask := syscall.Umask(0) // 改为 0000 八进制
defer syscall.Umask(mask) // 改为原来的 umask
err := os.MkdirAll("g10guang/t1/t2", 0766)
if err != nil {
fmt.Println(err)
}
}
先创建文件,然后再改变文件的权限
import (
"os"
"fmt"
)
func main() {
err := os.MkdirAll("g10guang/t1/t2", 0777)
if err != nil {
fmt.Println(err)
}
os.Chmod("g10guang", 0777)
os.Chmod("g10guang/t1", 0777)
os.Chmod("g10guang/t1/t2", 0777)
}
golang 还不支持递归更改多个文件夹的权限,所有需要一个一个调用。
Linux 文件操作都是调用 Linux 的系统调用完成的,虽然 Python 、java 等创建一个文件不会让显示让编程人员指定所创建的文件的权限,但是 Golang 需要,所以我们编程还是需要了解一点内核知识,比如网络编程涉及到 I/O,而 Linux 下 I/O 有I/O 阻塞、I/O非阻塞、I/O复用、SIGIO 、异步I/O,而I/O复用中有 select / poll / epoll 等,上次面试被问到 epoll 时,闻所未闻,更多I/O讲解。
现在找实习、找工作,发现公司会看重面试者是否有阅读源码等经验,因为我们不仅仅需要懂得调用别人的 API 完成某个功能,而且需要某个 API 底层到底完成了哪些操作,出了问题如何定位问题,以及如何解决问题。
以下代码省略 condition statement 将会引起无限循环
for ;; {
fmt.Println("hello world")
}
相对与其他语言,Go 中支持 if 像 for-loop 一样先执行 init,init 中声明的变量只能在 if {} else {} 中使用
if v:=method(); v == 0 {
statment()
}
// cannot use v from here
switch 是一连串 if-else 的简写,但是 Go 中 switch 与 C java 不同,Go 中只有命中的 case 会被执行。所以 break 相当于出现在每个 case 之后
func test_switch() {
fmt.Println("Go runs on")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X")
case "linux":
fmt.Println("Linux")
default:
fmt.Printf("%s\n", os)
}
}
swithc without the condition == 选择第一个 true case,空 switch 有利于书写很长的 if-else 语句
func switch3() {
t := time.Now()
// without condition equals to select true
switch {
case t.Hour() < 12:
fmt.Println("Good morning")
case t.Hour() < 17:
fmt.Println("Good afternoon")
default:
fmt.Println("Good evening")
}
}
不想 C java 中 case 紧随一定要是常量,Go 中 case 后可以跟随任何变量,甚至函数
switch-case 中强制执行命中 case 的下一条 case
func main() {
// 空 switch 则相当于寻找第一个为 true 的 case
switch {
case 1+1 == 2:
fmt.Println("1+1=2")
fallthrough
case 1 == 2:
fmt.Println("fallthrough")
fallthrough
default:
fmt.Println("default")
}
}
output:
1+1=2
fallthrough
default
defer 语句能够推迟某些语句的执行,直到 return 语句附近
func defer1() {
defer fmt.Println("world")
defer fmt.Println("AAAA")
fmt.Println("Hello")
}
defer 将语句放到最后执行,然后将语句执行的顺序完全相反。其背后是通过栈(FILO)实现的。
output:
Hello
AAAA
world
A goroutine is a light weight thread managed by the Go runtime.
go f(x, y, z)
f, x, y, z 的计算会发生在当前 goroutine,但执行在另一个新的 goroutine
goroutine 运行在同一个地址空间,所以访问共享内存时需要同步
Go 哲学不要使用共享内存通信,而是通过 channel 通信来达到共享内存的目的
channel 是用于 goroutine 之间的通信
通道是一个通信管道,可以通过通道操作符 <- 来接收,发送消息(PS: 估计类似与进程管道进行通信)
表现同时也像队列,先进先出。普通 channel 可以用来做同步控制(synchronization),因为 channel 的一方必须等另一方准备完成才能进行读写,比如:
Sender 把一个数据放进 channel,Reader 还未进行读,此时 Sender 再次把数据放进 channel 就会阻塞 Sender,知道 Reader 把数据从 channel 读取出来
一个 goroutine 可以写和读 channel,但是不能够读自己写的数据,读自己写的数据将会发生阻塞,如果所有 goroutine 都 asleep 那么 go runtime 认为出现了死锁
ch <- v // 发送 v 到 channel ch
v := <-ch // 从 channel ch 接收一个值,并且赋予 v
数据流动方向就是箭头方向,<-
创建 channel
ch := make(chan int)
默认情况下,直到对方准备好,才开始发送和接收消息。这提供了 goroutine 之间的同步机制,而不是采用特殊的锁或条件变量
channel 可以是有缓存的,提高缓存的长度用来初始化 channel
ch := make(chan int, 2)
向满的 buffered channel 中输入数据,或者是向空 buffered channel 中读取数据,会发生错误
发送者可以 close 关闭 channel,表明没有再多的元素被发送;
接受者可以测试一个 channel 是否已经关闭 v, ok := <- ch
只有发送者才能够关闭 channel,向已经关闭了的 channel 发送数据会引发
panic
ok == false 如果 channel 中没有更多元素,channel 已经被关闭
使用 for 循环读取 channel 中的数据,直到 channel 被挂壁
for i := range ch
不像文件,关闭 channel 并不是必须的。只有当需要通知接受者没有更多的元素时,才需要主动关闭 channel,例如通知接受者停止
for i := range ch循环
读取 buffered channel 目前元素个数 length 和可以容纳的最多元素个数 capability
len(ch)
cap(ch)
从 channel 中不断发送和读取 fibonacci 数列
func fibonacci(n int, ch chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
ch <- y
x, y = y, x + y
}
close(ch)
}
func main() {
ch := make(chan int, 10)
go fibonacci(cap(ch), ch)
// 不断地读取元素,直到 channel closed
for i := range ch {
fmt.Println(i)
}
}
select 从多个 channel 中随机选取一个可读或者可写的 channel,执行该 case。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- y:
x, y = y, x+y
// 从 quit 通道中读取元素,但是没有发生赋值
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
// 从 c 中读取 10 个元素,然后向 quit 中发送信号,让程序退出
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
用于控制超时 select
select {
case v := <-ch:
doSomething()
// time.After 可以保证一定时间后 channel 即可通信
case <-time.After(1 * time.Second):
timeout()
}
sync.Mutex 用于同步,比如上锁等操作
mux.Lock() // 上锁
defer mux.Unlock() // 解锁
goroutine 不是 thread,每一个 goroutine 都拥有一个自己的调用栈。goroutine 开销很低,可以同时开启上千个 goroutine。
有可能一个 thread 中有上千个 goroutine。goroutine 多路动态复用 thread,也就是说真正执行计算的还是 thread,但是一个 thread 可能利用异步等技术来 concurrent 并发多个 goroutine 执行,并且减少切换 thread 上下文的成本
我为什么要学 golang? golang 相对其他语言的优势:
channel 能够实现很多同步,需要使用一定的技巧。比如控制最多执行的 goroutine,select 通知一个 goroutine 的退出。
1 和 2 都是基于 go 运行时的强大
golang 的 GC 显然不算强大,应该比不上 JVM
结构体、接口设计与 C 系语言不一样。接口的实现是隐式的
defer 语句确保某些操作能够被执行
不一样的错误异常处理,将错误以返回值的形式传递。
听说类型别名使得代码更加易于维护
指针,类似于 java 等语言的引用
可变长的 goroutine 栈,最多能够扩展至 1GB,可以执行很多层递归
go 的工具链,但是我还没使用过高级功能
内置函数都是通过调用 C 来完成的,cgo协议。所以每个语言内置的东西都差不多
背后有 Google 维护,强大的创新力,很多大公司使用 Go,使得 Go 有创新力
语法糖上没有 Python 强大,当然做小事情的时候写起来没有 Python 方便,也 Python 的第三方包丰富。but golang 打包后只是一个执行文件,而且非常小。而 java 编译后就是多个 class 文件然后在真实运行时在一个一个地连接在一起。Python GIL 啊。。。
Python 有 GIL 锁,在多核高并发并不适合,Python 的协程书写不方便
当然 Go 只是一门编程语言,在功能上 Go 能够实现的其他语言同样能够实现。就像英文、中文一样,使用人数越多,该语言就越有价值。编程语言也类似,使用的人越多,文档也就越丰富,比如翻译过来的中文文档,stackoverflow 等社区上关于相关问题的答案也就越多,成员也更加活跃,网上相关博客更多,学习书籍也更多,第三方库也更多,轮子啊,再也不用重新造轮子……这就是价值。
语言层面一些差别,我指的是一般开发者即使努力也不会改变的一些事实。语言的语法,运行速度,底层内存模型,有无 GC,GC 的质量,编译型或解释性……这些对于开发者来说就是天大的难题
先编译生成对应的 .a 文件,然后再移动到 $GOPATH/pkg 或者 $GOPATH/bin 目录下
go install
-v 参数显示底层执行信息
将该包打包为一个 xxx.a 文件,然后可以被其他包引用
go build
如果当前包为package main,则将此包打包为一个可运行程序;如果需要在 $GOPATH/bin 下生成可执行文件,那么需要执行 go install
go build 默认情况下会忽略,_ 和 . 开头的go文件
go build 参数说明:
-o- 指定输出的文件名,可以带上路径-i 安装相应的包,编译+go install-n 把需要执行的命令打印出来,但是不执行,这样可以理解相应命令底层到底做了什么-p n 指定并行可运行的编译数量,默认是 CPU 数量-a 更新包,对标准包不起作用-race 开启编译时自动显示数据竞争情况-v 打印我们正在编译的包名-work 打印编译时候临时文件夹的名称,如果已经存在就不删除-x 打印出编译需要执行的命令,与-n不同的是 -x 执行打印输出的命令-ccflags 'args list' 传递参数给 5c,6c,8c 调用-compile name 指定编译器,gccgo or gc-gcflags 'args list' 传递参数给 5g,6g,8g-installsuffix suffix 为了和安装包区分开来-ldflags 'args list' 传递参数给 5l,6l,8l-tags 'tags list' 设置在编译时候可以适配的那些 tag如果代码需要执行某些跨平台的操作,那么可以使用 xxxx_linux.go xxxx_darwin.go xxxx_windows.go 等命名,执行 go 命令只会采用以 _platform 当前系统名称结尾的文件
go get -u github.com/g10guang/xxxxx
-u 参数可以自动更新包,而且 go get 的时候会自动获取第三方依赖
目录结构:
$GOPATH
src
|-github.com
|-g10guang
|-xxxx
pkg
|-github.com
|-g10guang
|xxxx.a
go get 本质上可以理解为先把远程代码 fetch 下来,然后执行 go install
参数说明:
-d 只下载不安装-v 显示执行的命令-u 强制使用网络去更新包和它的依赖包-t 下载测试所需要的包-fix 在获取源码之后,先运行 fix,然后再去做其他事-f 只有包含了 -u 参数时才生效,不去验证每一个 import 是否已经获取了,对于本地 fork 的包特别有效删除当前源码包和关联源码包里面编译生成的文件
参数说明:
-i 清除关联的安装的包和可运行文件,也就是通过 go install 生成的文件-n 打印执行的相应命令,但是不执行-r 循环清除 import 中引入的包-x 输出执行的命令,并且执行格式化 go 代码,也就是 vscode 失去焦点后自动执行的命令,这样可以保证一个团队中代码风格统一
参数说明:
-l 显示需要格式化的文件-w 将格式化的结果输出到文件,而不是打印输出,如果不带该参数最终结果将不会输出到文件-r 添加重写规则,方便做批量替换-s 简化文件中的代码-d 显示格式户前后的 diff,而不写入文件-e 打印所有的语法错误到控制台-cpuprofile 支持调试模式,写入相应的 cpufile 到指定文件读取源码目录下的 *_test.go 文件,执行测试用例
参数说明:
-bench regexp 执行相应的 benchmark,例如 -bench=.-cover 开启测试覆盖率-run regexp 只运行匹配正则 regexp 的函数,例如 -run=Array 只运行所有以 Array 开头的函数-v 显示测试的详细命令go tool 下聚集了很多命令,比如 fix vet
go tool fix 用来修复老版本的代码到新版本,自动修改变化的 APIgo tool vet directories | files 分析代码的语法是否正确,比如检查 fmt.Printf() 中的参数是否正确,return 之后是否还有多余的代码用来在编译前生成某些代码,是通过分析源码中特殊的注释,判断需要生成某些特殊代码
go generate 是给当前包的开发人员使用的,而不是给使用该包的人使用的
比如我们经常使用 yacc 生成代码
go tool yacc -o gopher.go -p parser gopher.y
如果我们想让 go generate 替我们执行该命令,那么可以在代码中任意一个位置插入注释:
//go:generate go tool yacc -o gopher.go -p parser gopher.y
在编译时候执行:
go generate
go build
查看文档的命令,安装 godoc:
go get golang.org/x/tools/cmd/godoc
比如查看 net/http 包的使用文档可以执行:
godoc net/http
查看某个函数文档:
godoc fmt Printf
查看函数源码:
godoc -src fmt Printf
本地运行 golang.org 站点中的文档,运行在特定端口上:
godoc -http=:8080
我们可以在浏览器查看 127.0.0.1:8080 查看文档
查看当前 go 版本
查看当前 go 的环境变量
列出当前包正在使用的包
编译并运行 go 程序
查看某条命令的帮助文档