golang的注意點(diǎn)
目錄
額,本來是有的,但貌似簡書不支持[TOC]或者是html語法
我的github地址:我的Github
里面有一個blog,里面會記錄寫學(xué)習(xí)相關(guān)的內(nèi)容。
1. 可以返回局部變量的指針
作為少數(shù)包含指針的語言,它與C還是有所不同。C中函數(shù)不能夠返回局部變量的指針,因?yàn)楹瘮?shù)結(jié)束時局部變量就會從棧中釋放。而golang可以做到返回局部變量的一點(diǎn)
#include <iostream>
using namespace std;
int* get_some() {
int a = 1;
return &a;
}
int main() {
cout << "a = " << *get_some() << endl;
return 0;
}
*這個明顯在c/c++中是錯誤的寫法,a出棧后什么都沒了。 會發(fā)生一下錯誤:
$ g++ t.cpp
> t.cpp: In function 'int* get_some()':
> t.cpp:4:6: warning: address of local variable 'a' > returned [-Wreturn-local-addr]
> int a = 1;
^
go語言試驗(yàn)代碼如下:
package main
import "fmt"
func GetSome() *int {
a := 1;
return &a;
}
func main() {
fmt.Printf("a = %d", *GetSome())
}
基本相同的代碼,但是有以下運(yùn)行結(jié)果
> $ go run t.go
> a = 1
顯然不是go的編譯器識別不出這個問題,而是在這個問題上做了優(yōu)化。參考go FAQ的原文:
How do I know whether a variable is allocated on the heap or the stack?
From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
這里的意思就是讓我們無需擔(dān)心返回的指針是空懸指針。我理解的意思是,普通情況下函數(shù)中局部變量會存儲在棧中,但是如果這個局部變量過大的話編譯器可能會選擇將其存儲在堆中,這樣會更加有意義。還有一種情況,當(dāng)編譯器無法證明在函數(shù)結(jié)束后變量不被引用那么就會將變量分配到垃圾收集堆上。總結(jié)一句:編譯器會進(jìn)行分析后決定局部變量分配在棧還是堆中
嗯。。。利用這個特性我們可以使用以下方式來達(dá)到并發(fā)做某事的作用
func SomeFun() <-chan int {
out := make(chan int)
go func() {
//做一些不可告人的事情。。。
}()
return out
}
2. Go提供的兩種分配原語——內(nèi)建函數(shù)new和make
Go語言提供了兩種分配的原語,即內(nèi)建函數(shù)new和make。它們做的事情不同。
new它不會初始化內(nèi)存,而是將內(nèi)存置零。也就是說new(T)會為類型T的新項(xiàng)分配一個已置零的內(nèi)存空間,并返回它的地址,也就是*T。即它會返回一個指針,這個指針是指向這個類型T的零值的那份空間。
make的函數(shù)簽名make(T, args)。它僅用于切片、map和chan類型的創(chuàng)建。make會直接返回一個類型為T的值而非指針,當(dāng)然這個值是已初始化過的。用法已切片為例,例如:
make([]int, 10, 100)
會分配一個容量為100,長度為10的int類型的切片結(jié)構(gòu)。
new([]int)
這個會返回一個指向新分配得,已置零得切片結(jié)構(gòu),即指向nil切片值的指針。
下面例子闡明了new和make之間的區(qū)別:
var p *[]int = new([]int) //分配切片結(jié)構(gòu);*p = nil;基本沒用
var v []int = make([]int, 100) //切片v現(xiàn)在引用了一個具有100個int元素的新數(shù)組
//沒必要這么麻煩
var p *[]int = new([]int)
*p = make([]int, 100, 100)
//習(xí)慣用法
v := make([]int, 100)
記住,make只適用于map、切片和chan且不返回指針。若要獲得明確的指針,請使用new分配內(nèi)存
3. 復(fù)合字面
在os標(biāo)準(zhǔn)包中有以下代碼,這個函數(shù)相當(dāng)于其他語言中的構(gòu)造函數(shù)
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
這里顯得代碼顯得過于冗長,可以使用復(fù)合字面來簡化代碼
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
由上面的代碼可以知道復(fù)合字面<code>File{fd, name, nil, 0}</code>返回的是一個量的引用而非指針,所以最后返回時需要取地址符。
4. 基礎(chǔ)類型中數(shù)組、切片、map、chan是值類型還是引用類型
這個是很有必要注意的一件事,用為當(dāng)你讓函數(shù)中傳入一個數(shù)組時,能不能改變外部數(shù)值的值呢?這就要考驗(yàn)到數(shù)值類型是值類型還是引用類型了。如果是引用類型的話,相當(dāng)于c語言中傳入指針一樣,可以在函數(shù)內(nèi)部改變傳入?yún)?shù)的外部的值,但是如果是值類型的話,在傳入函數(shù)過程中只是將一份拷貝傳入,故不可在函數(shù)內(nèi)部修改外部的值。
go語言中的數(shù)組是值類型的,這與其他語言大不一樣,拿c/c++為例:
#include <iostream>
using namespace std;
const int NUM = 5;
int a[NUM] = {5,4,3,2,1};
void change_a(int arr[],int n) {
for(int i = 0; i < n; i++){
arr[i]--;
}
}
int main() {
change_a(a,NUM);
for(int i = 0; i < NUM; i++) {
cout << "a[" << i << "] = " << a[i] << endl;
}
}
結(jié)果如下:
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ g++ v.cpp -o v.exe
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ ./v.exe
a[0] = 4
a[1] = 3
a[2] = 2
a[3] = 1
a[4] = 0
很顯然,函數(shù)內(nèi)部改變了形參數(shù)組導(dǎo)致全局變量a數(shù)組發(fā)生了改變
下面是golang的代碼
package main
import "fmt"
var a [5]int = [5]int{5,4,3,2,1}
func changeA(arr [5]int) {
for i := 0; i < 5; i++ {
arr[i]--
}
}
func main() {
changeA(a)
for i,v := range a {
fmt.Println("a[",i,"] = ",v)
}
}
結(jié)果如下:
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ go run v.go
a[ 0 ] = 5
a[ 1 ] = 4
a[ 2 ] = 3
a[ 3 ] = 2
a[ 4 ] = 1
從此可以看出go語言中的數(shù)組是值類型。事實(shí)上我們很少用數(shù)組去傳參數(shù),因?yàn)樵?go中如果用數(shù)組傳參的話需要在函數(shù)的參數(shù)形式列表中寫死數(shù)組的大小,而這種情況在c/c++中是不需要的。
但是go中傳參可以使用切片,因?yàn)?strong>切片是引用類型的。同上例子如下:
package main
import "fmt"
func main() {
a := []int{5,4,3,2,1}
func(arr []int) {
for i := 0; i < len(arr); i++ {
arr[i]--
}
}(a)
for i,v := range a {
fmt.Println("a[",i,"] = ",v)
}
}
結(jié)果如下:
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ go run vv.go
a[ 0 ] = 4
a[ 1 ] = 3
a[ 2 ] = 2
a[ 3 ] = 1
a[ 4 ] = 0
由此可見golang的數(shù)組是值類型的,但是切片是引用類型的。
記下 []T{}、map、chan作為基礎(chǔ)系統(tǒng)類型里的三個引用類型,而且這三個都是可以使用make這個內(nèi)聯(lián)函數(shù)的。第七點(diǎn)會歸納這部分內(nèi)聯(lián)函數(shù)
5.初始化函數(shù)init
這個函數(shù)比較神奇啊,我看官方文檔的時候有些看不懂.官方文檔(鏡像網(wǎng)站上的中文官方文檔)的一段
最后,每個源文件都可以通過定義自己的無參數(shù) init 函數(shù)來設(shè)置一些必要的狀態(tài)。 (其實(shí)每個文件都可以擁有多個 init 函數(shù)。)而它的結(jié)束就意味著初始化結(jié)束: 只有該包中的所有變量聲明都通過它們的初始化器求值后 init 才會被調(diào)用, 而那些 init 只有在所有已導(dǎo)入的包都被初始化后才會被求值。
除了那些不能被表示成聲明的初始化外,init 函數(shù)還常被用在程序真正開始執(zhí)行前,檢驗(yàn)或校正程序的狀態(tài)。
我在試驗(yàn)了之后大概得出結(jié)論,在import某個包的會執(zhí)行該包下所有文件的init函數(shù),執(zhí)行順序與文件在文件系統(tǒng)的排序有關(guān)。
vv.go,main函數(shù)所在文件
package main
import(
"fmt"
"./some"
_ "./another"
)
func init() {
fmt.Println("hello")
}
func main() {
a := []int{5,4,3,2,1}
some.ChangeA(a)
for i,v := range a {
fmt.Println("a[",i,"] = ",v)
}
var c int = 10
fmt.Println(c)
}
package some下有三個文件
some0.go
package some
import "fmt"
func init() {
fmt.Println("some0")
}
some1.go
package some
import (
"fmt"
)
var a int
func init(){
a = 10
fmt.Println("package some init done!",a)
}
func ChangeA(arr []int) {
for i := 0; i < len(arr); i++ {
arr[i]--
}
}
some2.go
package some
import "fmt"
func init() {
fmt.Println("some2")
}
package another下一個文件
another.go
package another
import "fmt"
func init() {
fmt.Println("package another init done!")
}
以上代碼為了節(jié)省空間,某些為了美觀的空行省略了。
最后運(yùn)行結(jié)果如下:
some0
package some init done! 10
some2
package another init done!
hello
a[ 0 ] = 4
a[ 1 ] = 3
a[ 2 ] = 2
a[ 3 ] = 1
a[ 4 ] = 0
10
假如將another.go的文件小做修改,修改如下:
package another
import "fmt"
import _ "../some"
func init() {
fmt.Println("package another init done!")
}
得到的結(jié)果不變,可見,init函數(shù)只會執(zhí)行一遍,而不是碰到import它所在的包就執(zhí)行。
6.關(guān)于指針與值
這個我也是比較糊的,所以在這里進(jìn)行了部分整理和試驗(yàn)。估計(jì)以后還有更多關(guān)于這點(diǎn)的問題
先把重要點(diǎn)記下:
- 綁定在類型指針*T上的方法可以改變該類型的值,但是只綁定在類型T上的方法是無法改變該類型的值的。有以下代碼:
package main
import "fmt"
type Si int
func (s *Si)Plus1(a Si) {
*s += a
}
func (s Si)Plus2(a Si) {
s += a
}
func main() {
var s Si = 10
s.Plus1(1)
fmt.Println("after Plus1: s = ",s)
s.Plus2(1)
fmt.Println("after Plus2: s = ",s)
}
運(yùn)行結(jié)構(gòu)如下:
after Plus1: s = 11
after Plus2: s = 11
可見Plus2并沒有發(fā)揮其作用。
go語言是一門一眼就能看得懂的語言,其他語言中把成員函數(shù)神奇的封裝在一個類里,但go不是,函數(shù)在前面的小括號里寫的參數(shù)就是指定了我這個函數(shù)是歸屬于哪個類型的,而且顯式的將該類型的值傳入函數(shù)了,也就是函數(shù)名前面的括號其實(shí)就可以看作是形參列表
- 類型向接口賦值的時候應(yīng)該取地址
package main
import "fmt"
type Si int
type Plus interface {
Plus1(a Si)
Plus2(a Si)
}
func (s *Si)Plus1(a Si) {
*s += a
}
func (s Si)Plus2(a Si) {
s += a
}
func main() {
var s Si = 10
var ss Plus = &s
ss.Plus1(1)
fmt.Println("after Plus1: s = ",s)
}
以上代碼是成立并且是能正確運(yùn)行的
但是如果將上面的<code>var ss Plus = &s</code>變成<code>var ss Plus = s</code>就會出現(xiàn)編譯錯誤。該編譯錯誤如下:
# command-line-arguments
cmd\tt.go:26:6: cannot use s (type Si) as type Plus in assignment:
Si does not implement Plus (Plus1 method has pointer receiver)
這個編譯錯誤提示很有意思啊,前半段提醒我們并沒有實(shí)現(xiàn)Plus接口,我們可能會認(rèn)為Si類型明明實(shí)現(xiàn)了Plus接口啊。這是怎么回事呢?其實(shí)看括號里的話結(jié)合最上面未出錯的程序就會明白,其實(shí)編譯器的意思就是*Si實(shí)現(xiàn)了接口Plus但是Si并沒有實(shí)現(xiàn)。
為什么會出現(xiàn)這種情況呢,其實(shí)是因?yàn)榻涌谟幸粋€函數(shù)綁定在指針上<code>func (s *Si)Plus1(a Si)</code>,而Si類型是沒有實(shí)現(xiàn)這個函數(shù)的,故沒有實(shí)現(xiàn)Plus接口??墒菫槭裁?lt;code>func (s Si)Plus1(a Si)</code>綁定在Si上但是*Si也實(shí)現(xiàn)了Plus接口。那是因?yàn)間o編譯器可以自動根據(jù)<code>func (s Si)Plus1(s Si)</code>這個函數(shù)生成<code>func (s *Si)Plus1(a Si)</code>,故而*Si實(shí)現(xiàn)了所有函數(shù)。
當(dāng)然以上自動生成的過程反過來是無法實(shí)現(xiàn)的,因?yàn)橹羔樀臋?quán)限大的原因,<code>func (s *Si)Plus1(a Si)</code>可能會改變s的值,而<code>func (s Si)Plus1(a Si)</code>無法做到,故而編譯器也不會自動生成。
通過以上分析,我們以以下例子做試驗(yàn):
package main
import "fmt"
type Si int
type Plus interface {
Plus1(a Si)
Plus2(a Si)
}
func (s Si)Plus1(a Si) {
s += a
}
func (s Si)Plus2(a Si) {
s += a
}
func main() {
var s Si = 10
var ss Plus = s
ss.Plus1(1)
fmt.Println("after Plus1: s = ",s)
}
運(yùn)行結(jié)果:
after Plus1: s = 10
為什么是10在第一點(diǎn)已有介紹了。編譯通過,第二點(diǎn)分析合理!
- 根據(jù)以上例子發(fā)現(xiàn)了一個奇怪的但又不奇怪的現(xiàn)象。不論是*T還是T都可以直接調(diào)用函數(shù)。而且在對接口賦值時接口聲明部分無需聲明為指針也不能聲明為指針。接口賦值好后,不能取內(nèi)容,雖然有些接口看上去是一個指針。
這里不再舉例
考慮到以上三點(diǎn),我覺得自己有必要養(yǎng)成的幾個習(xí)慣:
1.接口賦值時應(yīng)該最好使用類型指針對其賦值
2.寫成員函數(shù)時遵循最小權(quán)限原則,注意*T和T的區(qū)別
3.在調(diào)用成員函數(shù)時無論是指針還是類型本身都可以直接調(diào)用
4.接口在調(diào)用成員函數(shù)時就直接調(diào)用
7. 切片、map和chan有關(guān)的內(nèi)聯(lián)函數(shù)
這一部分也是比較亂的一點(diǎn),關(guān)聯(lián)這三個基本類型的內(nèi)建函數(shù)大致可以分成三類。分別與創(chuàng)建、刪除、操作。
創(chuàng)建:map、slice、channel的創(chuàng)建一般都是用make函數(shù)來進(jìn)行內(nèi)存分配。
刪除:delete主要用于map中刪除實(shí)例。嗯,channel的close也放在這項(xiàng)中吧。
-
操作:len、cap可用于不同的類型,len可用于string、slice、array的長度。cap一般返回slice的分配空間的大小。copy用于復(fù)制slice。append用于追加slice.
ps:new用于各種類型的內(nèi)存分配不止以上幾種。
<h6>具體用法如下:</h6>
-
make
1.1. channel: 這里只拿常用類型int做例子:
ch1 := make(chan int) //不帶緩存的channel
ch2 := make(chan int, 1024) //帶緩存的channel
1.2. slice: 針對slice的函數(shù)簽名make([]type,len)和make([]type,len,cap)
slice1 := make([]int, 10) //slice1中有10個初始值為零值的元素
slice2 := make([]int, 10, 100) //slice2中有10個初始值為零的元素,且初始容量為100
1.3. map:簽名make(map[keyType]valueType)
mp := make(map[string]int)
mp["啊啊啊"] = 3
-
append
2.1. slice: append(slice []Type, elems ...Type) []Type
append函數(shù)主要用于向slice的末尾添加元素的,作為一個特殊的存在可以在字節(jié)切片【】byte("hello")中添加字符串string。它會返回一個被更新過的slice,如果要使用它就需要一個變量接收這個更新的值。例子如下:
slice1 = append(slice1,2,3,4)
slice2 = append(slice2,slice1...)
slice3 := append([]byte("hello"),"world"...)
-
copy
3.1. slice: copy(dst, src []Type) int
這個函數(shù)需要小心的一點(diǎn),slice1和slice2兩個長度分別為5和3.還是用代碼表示吧。。。
//len(slice1)是5
//len(slice2)是3
//i==3,只會復(fù)制slice1的前三個元素到slice2中
i := copy(slice2,slice1)
//i==3,只會將slice2中的前三個元素復(fù)制到slice1中
i = copy(slice1,slice2)
-
len、cap
4.1. len用于獲取切片和map長度,channel未取元素個數(shù),cap用于獲取切片的容量和channel的緩沖容量。簽名:len(v Type) int,cap(v Type) int
len(ch1)
len(slice1)
len(mp)
cap(slice1)
cap(ch1)
len不只用于這三個數(shù)據(jù)類型,還包括string、數(shù)組和指向數(shù)組的指針。源代碼注釋如下:
// The len built-in function returns the length of v, according to its type:
// Array: the number of elements in v.
// Pointer to array: the number of elements in *v (even if v is nil).
// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
// String: the number of bytes in v.
// Channel: the number of elements queued (unread) in the channel buffer;
// if v is nil, len(v) is zero.
cap雖然也可以用在數(shù)組和指向數(shù)據(jù)的指針但是其返回內(nèi)容與len函數(shù)相同。源代注釋如下:
// The cap built-in function returns the capacity of v, according to its type:
// Array: the number of elements in v (same as len(v)).
// Pointer to array: the number of elements in *v (same as len(v)).
// Slice: the maximum length the slice can reach when resliced;
// if v is nil, cap(v) is zero.
// Channel: the channel buffer capacity, in units of elements;
// if v is nil, cap(v) is zero.
-
delete
5.1. delete 只用于map刪除元素。簽名:delete(m map[Type]Type1, key Type)
delete(mp,"啊啊啊")
-
close
6.1. channel可以接受和發(fā)送數(shù)據(jù),也可以被關(guān)閉。當(dāng)channel關(guān)閉后向channel發(fā)送數(shù)據(jù)的操作會引起panic。但是當(dāng)channel關(guān)閉后,我們還能向其中取數(shù)據(jù),若是之前的數(shù)據(jù)還沒有取完那么還可以將這些數(shù)據(jù)取出。當(dāng)緩存的數(shù)據(jù)全部取完后,仍然可以對channel取數(shù)據(jù),此時的數(shù)據(jù)為零值數(shù)據(jù)。簽名:close(c chan<- Type)
close(ch)
待續(xù)。。。