Con trỏ so với giá trị trong tham số và giá trị trả về


326

Trong Go có nhiều cách khác nhau để trả về structgiá trị hoặc lát. Đối với những người cá nhân tôi đã thấy:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

Tôi hiểu sự khác biệt giữa những điều này. Cái đầu tiên trả về một bản sao của struct, cái thứ hai là một con trỏ tới giá trị struct được tạo trong hàm, cái thứ ba hy vọng một cấu trúc hiện có sẽ được truyền vào và ghi đè giá trị.

Tôi đã thấy tất cả các mẫu này được sử dụng trong các bối cảnh khác nhau, tôi tự hỏi những thực tiễn tốt nhất liên quan đến những mẫu này. Khi nào bạn sẽ sử dụng cái nào? Ví dụ, cái đầu tiên có thể ổn đối với các cấu trúc nhỏ (vì chi phí tối thiểu), thứ hai cho các cấu trúc lớn hơn. Và thứ ba nếu bạn muốn cực kỳ hiệu quả bộ nhớ, bởi vì bạn có thể dễ dàng sử dụng lại một thể hiện cấu trúc duy nhất giữa các cuộc gọi. Có thực hành tốt nhất khi sử dụng cái nào không?

Tương tự, câu hỏi tương tự liên quan đến lát:

func myfunc() []MyStruct {
    return []MyStruct{ MyStruct{Val: 1} }
}

func myfunc() []*MyStruct {
    return []MyStruct{ &MyStruct{Val: 1} }
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{ MyStruct{Val: 1} }
}

func myfunc(s *[]*MyStruct) {
    *s = []MyStruct{ &MyStruct{Val: 1} }
}

Một lần nữa: những thực hành tốt nhất ở đây là gì. Tôi biết các lát luôn là con trỏ, vì vậy việc trả về một con trỏ tới một lát không hữu ích. Tuy nhiên, tôi có nên trả lại một lát các giá trị cấu trúc, một lát con trỏ cho các cấu trúc không, tôi có nên chuyển một con trỏ tới một lát làm đối số (một mẫu được sử dụng trong API của Máy ứng dụng Go ) không?


1
Như bạn nói, nó thực sự phụ thuộc vào trường hợp sử dụng. Tất cả đều hợp lệ tùy thuộc vào tình huống - đây có phải là một đối tượng có thể thay đổi không? chúng ta muốn có một bản sao hoặc con trỏ? vv BTW bạn đã không đề cập đến bằng cách sử dụng new(MyStruct):) Nhưng thực sự không có sự khác biệt giữa các phương pháp phân bổ con trỏ khác nhau và trả lại chúng.
Not_a_Golfer

15
Đó là nghĩa đen của kỹ thuật. Các cấu trúc phải khá lớn để trả về một con trỏ giúp chương trình của bạn nhanh hơn. Đừng bận tâm, mã, hồ sơ, sửa chữa nếu hữu ích.
Volker

1
Chỉ có một cách để trả về một giá trị hoặc một con trỏ và đó là trả về một giá trị hoặc một con trỏ. Làm thế nào bạn phân bổ chúng là một vấn đề riêng biệt. Sử dụng những gì làm việc cho tình huống của bạn và viết một số mã trước khi bạn lo lắng về nó.
JimB

3
BTW chỉ vì tò mò mà tôi đã chuẩn bị điều này. Việc trả lại các cấu trúc so với các con trỏ dường như có cùng tốc độ, nhưng việc chuyển các con trỏ tới các chức năng xuống các dòng nhanh hơn đáng kể. Mặc dù không ở cấp độ nhưng nó sẽ có vấn đề
Not_a_Golfer

1
@Not_a_Golfer: Tôi cho rằng việc phân bổ bc được thực hiện ngoài chức năng. Ngoài ra giá trị điểm chuẩn so với con trỏ phụ thuộc vào kích thước của các mẫu truy cập cấu trúc và bộ nhớ sau thực tế. Sao chép những thứ có kích thước dòng bộ đệm nhanh như bạn có thể nhận được và tốc độ của các con trỏ hội nghị từ bộ đệm CPU khác nhiều so với việc loại bỏ chúng khỏi bộ nhớ chính.
JimB

Câu trả lời:


392

tl; dr :

  • Phương pháp sử dụng con trỏ máy thu là phổ biến; quy tắc của người nhận là "Nếu nghi ngờ, hãy sử dụng một con trỏ."
  • Các lát, bản đồ, kênh, chuỗi, giá trị hàm và giá trị giao diện được triển khai với các con trỏ bên trong và một con trỏ tới chúng thường là dự phòng.
  • Ở những nơi khác, sử dụng các con trỏ cho các cấu trúc hoặc cấu trúc lớn mà bạn sẽ phải thay đổi và nếu không thì sẽ chuyển các giá trị , bởi vì việc thay đổi bất ngờ thông qua một con trỏ rất khó hiểu.

Một trường hợp bạn thường sử dụng một con trỏ:

  • Người nhận là con trỏ thường xuyên hơn so với các đối số khác. Không có gì lạ khi các phương thức sửa đổi thứ mà chúng được gọi hoặc các loại được đặt tên là các cấu trúc lớn, vì vậy hướng dẫn là mặc định cho con trỏ trừ trường hợp hiếm.
    • Công cụ copyfolder của Jeff Hodges tự động tìm kiếm các máy thu không nhỏ được truyền theo giá trị.

Một số tình huống mà bạn không cần con trỏ:

  • Các nguyên tắc đánh giá mã đề xuất chuyển các cấu trúc nhỏ như type Point struct { latitude, longitude float64 }, và thậm chí có thể lớn hơn một chút, dưới dạng các giá trị, trừ khi chức năng bạn đang gọi cần có thể sửa đổi chúng tại chỗ.

    • Ngữ nghĩa giá trị tránh các tình huống răng cưa trong đó việc gán ở đây làm thay đổi giá trị ở đó một cách bất ngờ.
    • Không phải là hy sinh ngữ nghĩa sạch trong một tốc độ nhỏ, và đôi khi vượt qua các cấu trúc nhỏ theo giá trị thực sự hiệu quả hơn, vì nó tránh được việc bỏ lỡ bộ nhớ cache hoặc phân bổ heap.
    • Vì vậy, trang nhận xét đánh giá mã của Go Wiki đề xuất chuyển theo giá trị khi các cấu trúc nhỏ và có khả năng giữ nguyên như vậy.
    • Nếu mức cắt "lớn" có vẻ mơ hồ, thì đó là; có thể cho rằng nhiều cấu trúc nằm trong một phạm vi mà con trỏ hoặc giá trị là OK. Khi giới hạn dưới, các nhận xét đánh giá mã đề xuất các lát (ba từ máy) là hợp lý để sử dụng làm máy thu giá trị. Khi một cái gì đó ở gần một giới hạn trên, bytes.Replacesẽ có giá trị của các đối số 10 từ (ba lát và một int).
  • Đối với các lát , bạn không cần phải vượt qua một con trỏ để thay đổi các phần tử của mảng. io.Reader.Read(p []byte)thay đổi các byte của p, ví dụ. Có thể coi đó là một trường hợp đặc biệt của "đối xử với các cấu trúc nhỏ như các giá trị", vì bên trong bạn đang đi qua một cấu trúc nhỏ gọi là tiêu đề lát (xem giải thích của Russ Cox (rsc) ). Tương tự, bạn không cần một con trỏ để sửa đổi bản đồ hoặc giao tiếp trên kênh .

  • Đối với các lát, bạn sẽ thay đổi vị trí (thay đổi thời gian bắt đầu / chiều dài / công suất), các hàm tích hợp như appendchấp nhận giá trị lát và trả về giá trị mới. Tôi bắt chước điều đó; nó tránh răng cưa, trả về một lát mới giúp thu hút sự chú ý đến thực tế là một mảng mới có thể được phân bổ và nó quen thuộc với người gọi.

    • Nó không phải lúc nào cũng thực tế theo mô hình đó. Một số công cụ như giao diện cơ sở dữ liệu hoặc trình tuần tự hóa cần phải nối vào một lát có loại không được biết đến tại thời điểm biên dịch. Đôi khi họ chấp nhận một con trỏ tới một lát trong một interface{}tham số.
  • Bản đồ, kênh, chuỗi và các giá trị chức năng và giao diện , như các lát, là các tham chiếu hoặc cấu trúc bên trong đã chứa các tham chiếu, vì vậy nếu bạn chỉ cố gắng tránh sao chép dữ liệu cơ bản, bạn không cần chuyển con trỏ đến chúng . (rsc đã viết một bài riêng về cách lưu trữ giá trị giao diện ).

    • Bạn vẫn có thể cần phải chuyển con trỏ trong trường hợp hiếm hơn mà bạn muốn sửa đổi cấu trúc của trình gọi: flag.StringVarlấy một *stringlý do đó, ví dụ.

Nơi bạn sử dụng con trỏ:

  • Xem xét liệu hàm của bạn có phải là một phương thức trên bất kỳ cấu trúc nào bạn cần một con trỏ tới hay không. Mọi người mong đợi rất nhiều phương pháp xđể sửa đổi x, do đó, làm cho cấu trúc được sửa đổi trở thành máy thu có thể giúp giảm thiểu bất ngờ. Có hướng dẫn về thời điểm người nhận nên là con trỏ.

  • Các chức năng có ảnh hưởng đến các thông số không nhận của chúng sẽ làm rõ điều đó trong vị thần, hay tốt hơn là, vị thần và tên (như reader.WriteTo(writer)).

  • Bạn đề cập đến việc chấp nhận một con trỏ để tránh phân bổ bằng cách cho phép tái sử dụng; thay đổi API vì mục đích tái sử dụng bộ nhớ là một sự tối ưu hóa mà tôi trì hoãn cho đến khi rõ ràng việc phân bổ có chi phí không cần thiết, và sau đó tôi sẽ tìm cách không buộc API khó hơn đối với tất cả người dùng:

    1. Để tránh phân bổ, phân tích thoát của Go là bạn của bạn. Đôi khi bạn có thể giúp nó tránh phân bổ heap bằng cách tạo các loại có thể được khởi tạo với một hàm tạo tầm thường, một chữ đơn giản hoặc một giá trị 0 hữu ích như bytes.Buffer.
    2. Xem xét một Reset()phương pháp để đưa một đối tượng trở lại trạng thái trống, như một số loại stdlib cung cấp. Người dùng không quan tâm hoặc không thể lưu phân bổ không phải gọi nó.
    3. Xem xét việc viết các phương thức sửa đổi tại chỗ và các hàm tạo từ đầu như các cặp khớp nhau, để thuận tiện: existingUser.LoadFromJSON(json []byte) errorcó thể được bao bọc bởi NewUserFromJSON(json []byte) (*User, error). Một lần nữa, nó đẩy sự lựa chọn giữa sự lười biếng và phân bổ chèn ép cho từng người gọi.
    4. Người gọi tìm cách tái chế bộ nhớ có thể cho phép sync.Poolxử lý một số chi tiết. Nếu một phân bổ cụ thể tạo ra nhiều áp lực bộ nhớ, bạn tự tin rằng bạn biết khi phân bổ không còn được sử dụng nữa và bạn không có sẵn tối ưu hóa tốt hơn, sync.Poolcó thể giúp ích. (CloudFlare đã xuất bản một sync.Poolbài đăng blog (trước ) hữu ích về tái chế.)

Cuối cùng, về việc các lát cắt của bạn có phải là con trỏ hay không: các lát giá trị có thể hữu ích và giúp bạn tiết kiệm phân bổ và bỏ lỡ bộ đệm. Có thể có các trình chặn:

  • API để tạo các mục của bạn có thể buộc các con trỏ vào bạn, ví dụ: bạn phải gọi NewFoo() *Foothay vì để Go khởi tạo với giá trị 0 .
  • Tuổi thọ mong muốn của các mặt hàng có thể không giống nhau. Toàn bộ lát cắt được giải phóng cùng một lúc; nếu 99% các mục không còn hữu ích nhưng bạn có con trỏ đến 1% khác, tất cả các mảng vẫn được phân bổ.
  • Di chuyển các mục xung quanh có thể gây ra vấn đề cho bạn. Đáng chú ý, appendsao chép các mục khi nó phát triển mảng bên dưới . Con trỏ bạn nhận được trước appendđiểm đến sai địa điểm sau đó, sao chép có thể chậm hơn đối với các cấu trúc lớn và ví dụ như sync.Mutexsao chép không được phép. Chèn / xóa ở giữa và sắp xếp các mục tương tự di chuyển xung quanh.

Nói chung, các lát giá trị có thể có ý nghĩa nếu bạn đặt tất cả các vật phẩm của mình lên trước và không di chuyển chúng (ví dụ: không còn appendsau khi thiết lập ban đầu) hoặc nếu bạn tiếp tục di chuyển chúng xung quanh nhưng bạn chắc chắn rằng đó là OK (không / sử dụng cẩn thận con trỏ đến các mục, các mục đủ nhỏ để sao chép hiệu quả, v.v.). Đôi khi bạn phải suy nghĩ hoặc đo lường chi tiết cụ thể về tình huống của bạn, nhưng đó là một hướng dẫn sơ bộ.


12
Điều gì có nghĩa là cấu trúc lớn? Có một ví dụ về một cấu trúc lớn và một cấu trúc nhỏ?
Người dùng không có mũ

1
Làm thế nào để bạn nói với byte. Đặt lại các đối số có giá trị 80 byte trên amd64?
Tim Wu

2
Chữ ký là Replace(s, old, new []byte, n int) []byte; s, cũ và mới là ba từ mỗi từ ( tiêu đề lát là(ptr, len, cap) ) và n intlà một từ, vì vậy 10 từ, với tám byte / từ là 80 byte.
twotwotwo

6
Làm thế nào để bạn xác định các cấu trúc lớn? Lớn như thế nào là lớn?
Andy Aldo

3
@AndyAldo Không có nguồn nào của tôi (nhận xét đánh giá mã, v.v.) xác định ngưỡng, vì vậy tôi quyết định nói đó là cuộc gọi phán xét thay vì thực hiện ngưỡng. Ba từ (như một lát cắt) được coi là nhất quán đủ điều kiện để trở thành một giá trị trong stdlib. Tôi đã tìm thấy một phiên bản của máy thu giá trị năm từ ngay bây giờ (văn bản / máy quét. Vị trí) nhưng tôi sẽ không đọc nhiều về điều đó (nó cũng được truyền dưới dạng con trỏ!). Không có điểm chuẩn, v.v., tôi chỉ cần làm bất cứ điều gì có vẻ thuận tiện nhất để dễ đọc.
twotwotwo

10

Ba lý do chính khi bạn muốn sử dụng bộ thu phương thức làm con trỏ:

  1. "Đầu tiên và quan trọng nhất là phương thức có cần sửa đổi máy thu không? Nếu có, máy thu phải là một con trỏ."

  2. "Thứ hai là việc xem xét hiệu quả. Nếu máy thu lớn, ví dụ một cấu trúc lớn, sẽ rẻ hơn nhiều khi sử dụng máy thu con trỏ."

  3. "Tiếp theo là tính nhất quán. Nếu một số phương thức của loại phải có bộ thu con trỏ, phần còn lại cũng vậy, vì vậy tập phương thức là nhất quán bất kể loại được sử dụng như thế nào"

Tham khảo: https://golang.org/doc/faq#methods_on_values_or_pointers

Chỉnh sửa: Một điều quan trọng khác là phải biết "loại" thực tế mà bạn đang gửi đến chức năng. Loại có thể là 'loại giá trị' hoặc 'loại tham chiếu'.

Ngay cả khi các lát cắt và bản đồ đóng vai trò là các tham chiếu, chúng ta có thể muốn chuyển chúng thành các con trỏ trong các tình huống như thay đổi độ dài của lát cắt trong hàm.


1
Đối với 2, những gì cắt? Làm thế nào để tôi biết nếu cấu trúc của tôi là lớn hay nhỏ? Ngoài ra, là có một struct đó là đủ nhỏ như vậy mà nó là nhiều hơn hiệu quả sử dụng một giá trị chứ không phải là con trỏ (để nó không nhất thiết phải được tham chiếu từ các đống)?
zlotnika

Tôi muốn nói rằng số lượng các trường và / hoặc các cấu trúc lồng nhau bên trong càng nhiều thì cấu trúc càng lớn. Tôi không chắc có cách cắt cụ thể hoặc cách tiêu chuẩn để biết khi nào một cấu trúc có thể được gọi là "lớn" hay "lớn". Nếu tôi đang sử dụng hoặc tạo một cấu trúc, tôi sẽ biết nó lớn hay nhỏ dựa trên những gì tôi đã nói ở trên. Nhưng đó chỉ là tôi!.
Santosh Pillai

2

Nếu bạn có thể (ví dụ: tài nguyên không chia sẻ không cần truyền qua làm tham chiếu), hãy sử dụng giá trị. Bởi những lý do sau:

  1. Mã của bạn sẽ đẹp hơn và dễ đọc hơn, tránh các toán tử con trỏ và kiểm tra null.
  2. Mã của bạn sẽ an toàn hơn trước sự hoảng loạn của Null Pulum.
  3. Mã của bạn sẽ thường nhanh hơn: có, nhanh hơn! Tại sao?

Lý do 1 : bạn sẽ phân bổ ít vật phẩm hơn trong ngăn xếp. Phân bổ / phân bổ từ ngăn xếp là ngay lập tức, nhưng phân bổ / phân bổ trên Heap có thể rất tốn kém (thời gian phân bổ + thu gom rác). Bạn có thể xem một số số cơ bản tại đây: http://www.macias.info/entry/201802102230_go_values_vs_Vferences.md

Lý do 2 : đặc biệt là nếu bạn lưu trữ các giá trị được trả về trong các lát, các đối tượng bộ nhớ của bạn sẽ được nén chặt hơn trong bộ nhớ: lặp một lát trong đó tất cả các mục liền kề sẽ nhanh hơn nhiều so với việc lặp lại một lát trong đó tất cả các mục là con trỏ đến các phần khác của bộ nhớ . Không phải cho bước gián tiếp mà là để tăng bộ nhớ cache.

Bộ ngắt huyền thoại : một dòng bộ đệm x86 điển hình là 64 byte. Hầu hết các cấu trúc nhỏ hơn thế. Thời gian sao chép một dòng bộ đệm trong bộ nhớ tương tự như sao chép một con trỏ.

Chỉ khi một phần quan trọng trong mã của bạn chậm, tôi sẽ thử một số tối ưu hóa vi mô và kiểm tra xem việc sử dụng con trỏ có cải thiện phần nào tốc độ hay không, với chi phí ít dễ đọc và dễ hiểu hơn.


1

Một trường hợp mà bạn thường cần trả về một con trỏ là khi xây dựng một thể hiện của một số tài nguyên trạng thái hoặc có thể chia sẻ . Điều này thường được thực hiện bởi các chức năng có tiền tố New.

Vì chúng đại diện cho một trường hợp cụ thể của một cái gì đó và chúng có thể cần phối hợp một số hoạt động, nên việc tạo ra các cấu trúc trùng lặp / sao chép đại diện cho cùng một tài nguyên - sẽ không có ý nghĩa gì .

Vài ví dụ:

Trong các trường hợp khác, con trỏ được trả về chỉ vì cấu trúc có thể quá lớn để sao chép theo mặc định:


Ngoài ra, có thể tránh trực tiếp trả về con trỏ bằng cách trả về một bản sao của cấu trúc có chứa con trỏ bên trong, nhưng có lẽ điều này không được coi là thành ngữ:


Mặc định trong phân tích này là, theo mặc định, các cấu trúc được sao chép theo giá trị (nhưng không nhất thiết là các thành viên không được chỉ định của họ).
tộc
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.