前文
- golang快速入門[1]-go語言導論
- golang快速入門[2.1]-go語言開發環境配置-windows
- golang快速入門[2.2]-go語言開發環境配置-macOS
- golang快速入門[2.3]-go語言開發環境配置-linux
- golang快速入門[3]-go語言helloworld
- golang快速入門[4]-go語言如何編譯爲機器碼
- golang快速入門[5.1]-go語言是如何運行的-鏈接器
- golang快速入門[5.2]-go語言是如何運行的-內存概述
- golang快速入門[5.3]-go語言是如何運行的-內存分配
- golang快速入門[6.1]-集成開發環境-goland詳解
- golang快速入門[6.2]-集成開發環境-emacs詳解
- golang快速入門[7.1]-項目與依賴管理-gopath
- golang快速入門[7.2]-北冥神功—go module絕技
- golang快速入門[8.1]-變量類型、聲明賦值、作用域聲明週期與變量內存分配
- golang快速入門[8.2]-自動類型推斷的祕密
- golang快速入門[8.3]-深入理解浮點數
- golang快速入門[8.4]-常量與隱式類型轉換
前言
- 在常量和自動類型推斷的文章中,我們介紹過整數、浮點數在詞法解析階段的過程。簡單的說,整數是全爲數字的常量,浮點數是帶了
小數點
的常量。字符串也一樣,字符串常量聲明有兩種方式:
var a string = `hello world`
var b string = "hello world"
- 詞法解析階段,挨個的讀取Uft-8字符, 當發現了
單撇號
或者是雙引號
時,說明其是一個字符串。解析函數如下
func (s *scanner) next() {
...
c := s.getr()
for c == ' ' || c == '\t' || c == '\n' && !nlsemi || c == '\r' {
c = s.getr()
}
// token start
s.line, s.col = s.source.line0, s.source.col0
if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) {
s.ident()
return
}
switch c {
case '"':
s.stdString()
case '`':
s.rawString()
...
}
- 解析時
單撇號
會調用rawString,雙引號
會調用stdString,兩者略微有所不同 單撇號
比較簡單,始終要尋找下一個配對的單撇號
func (s *scanner) rawString() {
s.startLit()
for {
r := s.getr()
if r == '`' {
break
}
if r < 0 {
s.errh(s.line, s.col, "string not terminated")
break
}
}
// We leave CRs in the string since they are part of the
// literal (even though they are not part of the literal
// value).
s.nlsemi = true
s.lit = string(s.stopLit())
s.kind = StringLit
s.tok = _Literal
}
雙引號
有所不同,其調用stdString函數。
func (s *scanner) stdString() {
s.startLit()
for {
r := s.getr()
if r == '"' {
break
}
if r == '\\' {
s.escape('"')
continue
}
if r == '\n' {
s.ungetr() // assume newline is not part of literal
s.error("newline in string")
break
}
if r < 0 {
s.errh(s.line, s.col, "string not terminated")
break
}
}
s.nlsemi = true
s.lit = string(s.stopLit())
s.kind = StringLit
s.tok = _Literal
}
- 當出現另一個
雙引號
則直接退出,當出現了字符\
,代表會對後面的字符進行轉義。 雙引號
不能出現如下的換行符,會報錯。
str := " 微信:
1131052403 "
- 無論是標準字符串還是原始字符串最終都會被標記成 StringLit 類型的 Token 並傳遞到編譯的下一個階段
- s.lit = string(s.stopLit()) 將解析到的字節轉換爲字符串,例如"hello" 最後會被解析爲""hello""
// go/src/cmd/compile/internal/gc
func (p *noder) basicLit(lit *syntax.BasicLit) Val {
case syntax.StringLit:
if len(s) > 0 && s[0] == '`' {
// strip carriage returns from raw string
s = strings.Replace(s, "\r", "", -1)
}
// Ignore errors because package syntax already reported them.
u, _ := strconv.Unquote(s)
return Val{U: u}
default:
panic("unhandled BasicLit kind")
}
}
- 無論是 import 語句中包的路徑、結構體中的字段標籤還是表達式中的字符串都會使用
strings.Replace
方法將原生字符串中最後的換行符刪除並對字符串 Token 進行 Unquote(strconv.Unquote(s)
),也就是去掉字符串兩邊的引號等無關干擾,還原其本來的面目。例如將""hello"" 轉換爲 "hello"
字符串拼接
- op操作爲:OADDSTR
- 常量中的字符串函數會在語法分析階段調用sum函數進行拼接。例如對於
"hello"+"world"
,會在noder.sum函數中完成拼接。
/usr/local/go/src/cmd/compile/internal/gc/noder.go
func (p *noder) sum(x syntax.Expr) *Node {
for i := len(adds) - 1; i >= 0; i-- {
add := adds[i]
r := p.expr(add.Y)
if Isconst(r, CTSTR) && r.Sym == nil {
if nstr != nil {
// Collapse r into nstr instead of adding to n.
chunks = append(chunks, r.Val().U.(string))
continue
}
nstr = r
chunks = append(chunks, nstr.Val().U.(string))
} else {
if len(chunks) > 1 {
nstr.SetVal(Val{U: strings.Join(chunks, "")})
}
nstr = nil
chunks = chunks[:0]
}
n = p.nod(add, OADD, n, r)
}
if len(chunks) > 1 {
nstr.SetVal(Val{U: strings.Join(chunks, "")})
}
return n
}
- 但是如果是變量之間的拼接,例如對於如下代碼,其拼接操作是在運行時完成的。
var a = "hello"
str := a + "xxs"
- 在語法分析階段會做一些準備工作。例如在類型檢查階段
typecheck1
函數進行賦值和字符串拼接語義。 - 在walkexpr函數中,還會進行準備工作,決定使用運行時的哪一個拼接函數。
go/src/cmd/compile/internal/gc/walk.go
func walkexpr(n *Node, init *Nodes) *Node {
case OADDSTR:
n = addstr(n, init)
}
- walkexpr函數中調用函數
addstr(n, init)
- 當拼接數量小於等於5個時,會調用運行時concatstring1~concatstring5之中的函數
- 當字符串的數量大於5個時,調用運行時concatstrings函數,並且字符串通過
切片
傳入
func addstr(n *Node, init *Nodes) *Node {
// build list of string arguments
args := []*Node{buf}
for _, n2 := range n.List.Slice() {
args = append(args, conv(n2, types.Types[TSTRING]))
}
var fn string
if c <= 5 {
// small numbers of strings use direct runtime helpers.
// note: orderexpr knows this cutoff too.
fn = fmt.Sprintf("concatstring%d", c)
} else {
// large numbers of strings are passed to the runtime as a slice.
fn = "concatstrings"
t := types.NewSlice(types.Types[TSTRING])
slice := nod(OCOMPLIT, nil, typenod(t))
if prealloc[n] != nil {
prealloc[slice] = prealloc[n]
}
slice.List.Set(args[1:]) // skip buf arg
args = []*Node{buf, slice}
slice.Esc = EscNone
}
cat := syslook(fn)
r := nod(OCALL, cat, nil)
r.List.Set(args)
r = typecheck(r, ctxExpr)
r = walkexpr(r, init)
r.Type = n.Type
return r
}
- 運行時字符串string的表示結構爲
type StringHeader struct {
Data uintptr
Len int
}
- 運行時具體的拼接代碼如下,其實無論使用 concatstring{2,3,4,5} 中的哪一個,最終都會調用 runtime.concatstrings,該函數會先對傳入的切片參數進行遍歷,先過濾空字符串並計算拼接後字符串的長度。
/usr/local/go/src/runtime/string.go
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
if n == 0 {
continue
}
if l+n < l {
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 {
return ""
}
// If there is just one string and either it is not on the stack
// or our result does not escape the calling frame (buf != nil),
// then we can return that string directly.
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
s, b := rawstringtmp(buf, l)
for _, x := range a {
copy(b, x)
b = b[len(x):]
}
return s
}
func concatstring2(buf *tmpBuf, a [2]string) string {
return concatstrings(buf, a[:])
}
func concatstring3(buf *tmpBuf, a [3]string) string {
return concatstrings(buf, a[:])
}
func concatstring4(buf *tmpBuf, a [4]string) string {
return concatstrings(buf, a[:])
}
func concatstring5(buf *tmpBuf, a [5]string) string {
return concatstrings(buf, a[:])
}
- 這裏要注意,如果拼接後的字符串大小 小於32字節時,會有一個臨時的緩存供其使用。如果拼接後的字符串大小
大於
32字節時,會請求分配內存。 - 拼接的過程就是開闢一個足夠大的內存空間,並將多個字符串存入其中的過程。期間會涉及到內存的
Copy
拷貝
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
if buf != nil && l <= len(buf) {
b = buf[:l]
s = slicebytetostringtmp(b)
} else {
s, b = rawstring(l)
}
return
}
字符串與字節數組的轉換
- 字節數組與字符串相互轉換的形式如下:
a := "微信:1131052403"
b := []byte(a)
c := string(b)
- 需要注意的是,字節數組與字符串的相互轉換並不是無損的簡單的一個指針的差別。而是涉及到了拷貝!因此相對而言,其仍然是消耗資源的。
- 如下爲字節數組轉換爲字符串
func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
...
var p unsafe.Pointer
if buf != nil && len(b) <= len(buf) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(len(b)), nil, false)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = len(b)
memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
return
}
如下爲字符串轉換爲字節數組
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
總結
- 本節我們深入介紹了字符串,字符常量存儲於靜態存儲區,其內容不可以被改變。聲明時有
單撇號
或者是雙引號
兩種方法 - 字符常量的拼接發生在編譯時,變量字符串的拼接發生在運行時。如果拼接後的字符串大小 小於32字節時,會有一個臨時的緩存供其使用。如果拼接後的字符串大小
大於
32字節時,會請求分配內存 - 需要注意的是,字節數組與字符串的相互轉換並不是無損的簡單的一個指針的差別。而是涉及到了拷貝!因此相對而言,其仍然是消耗資源的
- 本文還對編譯時和運行時涉及到的函數進行了具體的說明
- see you~
參考資料