Should I change my value receiver to a pointer receiver?

polaris · · 721 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>Hi, sorry for asking a pretty simple question but I can&#39;t seem to find a definitive answer.</p> <p>I have a custom type I use throughout my codebase and I tried to stick to using value receivers because I had no need to perform any mutations.</p> <p>Now I need to update a variable the custom type relies on and I am wondering if I should change everything to a pointer receiver and mutate the field on the struct (which I am apprehensive to do because I don&#39;t want to make all those changes and change all my tests, etc), or if it acceptable to do something like this</p> <pre><code>func (t myType) setField(s string) myType { t.someField = s return t } </code></pre> <p>And then whenever I want to mutate, I just copy over the previous instance. </p> <pre><code>myInstance = myInstance.setField(&#34;new value&#34;) </code></pre> <p>I lend towards the latter approach just because I have been indoctrinated on immutability, but I am curious to hear thoughts on this approach. </p> <p>Thanks.</p> <hr/>**评论:**<br/><br/>goomba_gibbon: <pre><p>Personally I would expect setField to mutate and not return a copy, based purely on the name of the method. Unless you have specific reasons to return a copy, I would use a pointer receiver. It is cleaner when calling because you don&#39;t have to assign it and doesn&#39;t require an allocation.</p> <p>I also don&#39;t think that having to update unit tests and calling code is a good reason to not change it.</p> <p>After reading your post I did some googling and found this. It might be useful: <a href="https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/" rel="nofollow">https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/</a></p></pre>nhooyr: <pre><p>He could just rename the method to <code>withField</code></p></pre>goomba_gibbon: <pre><p>Of course, but if the only use is to then assign the copy to itself, it seems a little pointless.</p> <p>There might well be value in returning a copy but there isn&#39;t enough info. OP, can you confirm?</p></pre>cameronjerrellnewton: <pre><p>Yes, the name is not important here. And the part about it being cleaner isn&#39;t a convincing argument because this is exactly how append() works. </p> <p>The blog post doesn&#39;t contain any information I wasn&#39;t aware of as I understand the difference between pointer and value receivers. I am more asking for consensus opinion and best practice, or if there are any other practical reasons. Do immutability principles apply to go as well? It certainly helps with concurrency. If I wanted to use my custom type across multiple go routines, I would then need to place a guard around the field it mutates.</p> <p>Not wanting to update unit tests and calling code is a wonderful reason in my opinion. I never want to change things unless I have to, you don&#39;t agree? </p></pre>Veonik: <pre><p>Yep, I think you are on the right track. I personally disagree (mutation is fine imo) but your reasoning seems pretty solid. Immutable state is a great way to manage concurrent access. To me, it&#39;s less intuitive and easier to forget (calling <code>myType.withField()</code> without assignment, for example).</p> <p>However, as far as best practice, naming is indeed important. <code>setField</code> is classically a mutation-- <code>withField</code> is classically a copy.</p></pre>goomba_gibbon: <pre><p>Completely agree. I think naming is important too and there is a convention for it.</p> <p>Concurrent access was not specified as a requirement up front. If that is needed then returning a copy is one way of managing it.</p> <p>append() is a bit different, in my opinion, because it&#39;s a built-in function and not a method.</p> <blockquote> <p>Not wanting to update unit tests and calling code is a wonderful reason in my opinion. I never want to change things unless I have to, you don&#39;t agree?</p> </blockquote> <p>I disagree. By that reasoning, nothing would ever be refactored.</p></pre>karma_vacuum123: <pre><p>returning values is not the right approach to safe concurrent use anyway. if you require mutual exclusion, use the <code>sync</code> package</p></pre>xiegeo: <pre><p>Since one as to merge the computations in the end. Mutation by copying is not a full solution, but it can go very far in minimizing mutual exclusion and simplifying concurrency.</p></pre>jerf: <pre><p>Go manages mutation by using convention to assign certain data structures to certain goroutines and expects the common case to be that goroutines do not generally share the same data. &#34;Share data by communicating, don&#39;t communicate by sharing memory.&#34;</p> <p>The biggest problem with copying structs around like that is that Go <a href="https://play.golang.org/p/1O7rDzeXVi" rel="nofollow">shallowly copies structs</a>. The way you are speaking about these issues cameronjerrellnewton leads me to believe you understand exactly what I mean by that, I just wanted to highlight it as an issue for you. Using Go in this way is going to tend to push you into struct designs that may otherwise be non-optimal or require you to write lots of .Copy() methods to get around the mutability of maps and the arrays under the slices and such. I&#39;m pretty good with Haskell, but I don&#39;t try to use Go immutably; instead I focus on using interfaces to isolate my IO behind interfaces, both for code clarity and for testability.</p> <p>Also you&#39;ll stress the GC, but I&#39;d consider that a secondary concern distantly behind being made reluctant to use slices or maps in structs.</p></pre>earthboundkid: <pre><blockquote> <p>Go manages mutation by using convention to assign certain data structures to certain goroutines and expects the common case to be that goroutines do not generally share the same data. &#34;Share data by communicating, don&#39;t communicate by sharing memory.&#34;</p> </blockquote> <p>Yes, once you get used to this, it&#39;s an easy way to get performant concurrency. Make it so that only one goroutine &#34;owns&#34; the data wherever possible, and you won&#39;t need immutability (which is a performance drag unless your compiler is smart as hell) to bail you out. </p> <p>I wrote a blog post about it: <a href="https://blog.carlmjohnson.net/post/share-memory-by-communicating/" rel="nofollow">https://blog.carlmjohnson.net/post/share-memory-by-communicating/</a></p></pre>earthboundkid: <pre><p>Append mutates your slice in place: <a href="https://play.golang.org/p/8Wqj0owqPZ" rel="nofollow">https://play.golang.org/p/8Wqj0owqPZ</a></p> <p>It only does a return because sometimes it allocates a new backing array, and there needs to be some way to indicate that to the caller.</p></pre>tmornini: <pre><blockquote> <p>the name is not important here</p> </blockquote> <p>Names are the most important part of code!</p></pre>cameronjerrellnewton: <pre><p>Not important in my question I mean</p></pre>karma_vacuum123: <pre><p>if you are focused on extending the benefits of immutability into your work, frankly Go is not a great tool. the Go developers have resisted even the basic value proposition of <code>let</code> style variables that can only be set once (see Rust&#39;s <code>let</code> etc)</p> <p>i&#39;m personally fine with the Go approach but i also have no expectations of <code>let</code> style safety</p></pre>weberc2: <pre><p>It&#39;s more conventional to use a pointer receiver for mutation, and I&#39;m not sure you&#39;re gaining anything by returning a copy. Not a big deal either way though.</p></pre>clbanning: <pre><p>First: you need to return &#34;t&#34; not &#34;myType&#34;. Second: you can mix and match receiver types; some can be values where you don&#39;t want to manipulate the struct members, and others can be pointers where you do.</p></pre>cameronjerrellnewton: <pre><p>Oops, that&#39;s a typo, thanks. </p> <p>Is mix and matching a good idea? I took a go training course from ardanlabs, and I remember the instructor discouraging the practice. </p></pre>zemo: <pre><blockquote> <p>I took a go training course from ardanlabs, and I remember the instructor discouraging the practice.</p> </blockquote> <p>that&#39;s terrible advice, inasmuch as it&#39;s advice based on dogma, not based on understanding. I&#39;ll answer the question as asked in a separate comment at the top level.</p></pre>clbanning: <pre><p>I prefer consistence also. But a couple of times it seemed a trivial convenience only because other methods were part of an interface signature. If you don&#39;t need that kind of compatibility, then make all your receivers pointers - you shouldn&#39;t have to change the source code that uses the package; the compiler handles everything for you.</p></pre>zemo: <pre><p>you don&#39;t need to update all of the method receivers to have pointer receivers if you only want one to have a pointer receiver. </p> <p>Everything in Go is passed by value. A method on type <code>T</code> will receive a value of type <code>T</code> when it is called. It will be passed by value; i.e., a copy will be created automatically, and the method will actually be operating on the copy. </p> <p>In the case of a struct, a method on a value receiver will always receive a value of that struct. That is: </p> <ul> <li><p>if in the calling context, you have a value of type <code>T</code>, and you call a method whose receiver is on type <code>T</code>, the value in the calling context and the value inside the body of the method are separate structs. The value inside the body of the method is a copy of the value in the calling context. </p></li> <li><p>if in the calling context, you have a value of type <code>*T</code>, and you call a method whose receiver is on type <code>T</code>, the value in the calling context won&#39;t change, even though it&#39;s a pointer. The value inside the body of the method is a copy of the value to which your pointer is pointing. That is: the pointer will automatically be de-referenced, and the thing it points to will be copied. </p></li> </ul> <p><strong>Any mutation made to a receiver of a struct type will not be visible to the calling context.</strong> </p> <p>In the case of a struct, a method on a pointer receiver will always receive a value that is a pointer to type <code>T</code>. That is:</p> <ul> <li>if in the calling context, you have a value of type <code>T</code>, and you call a method on it, the runtime will create a pointer, and the method will receive a pointer <em>to that value</em>. It&#39;s still passed by value, but the value being passed is a pointer. Meaning that if you have a value of type <code>T</code>, and you invoke a method whose receiver is of type <code>*T</code>, that mutation is visible to the calling context, because it&#39;s made <em>on the same value</em>. What&#39;s being passed into the method is <em>a pointer to that value</em>.<br/></li> <li>if in the calling context, you have a value of type <code>*T</code>, and you call a method on it, the runtime will create <em>another</em> pointer. This pointer is what will be seen inside the method body. However, <em>these pointers are pointing to the same thing</em>, so the mutations performed inside of the method body are visible to the calling context, because there are now <em>two</em> pointers.<br/></li> </ul> <p>If you want to mutate a value, you need a pointer receiver. Generally if you&#39;re naming a method <code>SetX</code>, the assumption is that the receiver is a pointer, and that the receiving value will be mutated. It&#39;s not really written anywhere, but it&#39;s commonly the case that a method that returns a <em>new</em> value that is the old value plus some mutation will be named <code>WithX</code>. </p> <p>there&#39;s only one thing to understand: everything is passed by value! There&#39;s always a copy! In the case of the pointer, the pointer is passed by value, so the pointer on the outside and the pointer on the inside are actually separate pointers, but they point to the same thing. In the case of a struct, the struct on the outside and the struct on the inside are separate values. </p> <p>edit: here&#39;s an example: <a href="https://play.golang.org/p/renx2pUDKP" rel="nofollow">https://play.golang.org/p/renx2pUDKP</a></p></pre>joncalhoun: <pre><p>In some cases I prefer returning values, in others I prefer mutation. It really depends. </p> <p>The upside to returning values like you do here is that you can chain things, so if you intend to do that it is nice. Eg when creating an SQL query it might feel nice to be able to chain like so</p> <p><code> q = q.Where(&#34;age = ?&#34;, age).Limit(5) </code></p> <p>But when working with a user object it might feel a little more awkward to do:</p> <p><code> u = u.WithAge(123).WithName(&#34;Jon&#34;) </code></p> <p>vs</p> <pre><code>u.SetAge(123) u.SetName(&#34;Jon&#34;) </code></pre></pre>karma_vacuum123: <pre><p>no matter how you go, i would avoid naming methods like this <code>set...</code> ...since it is in fact more akin to a copy constructor.</p> <p>what i like to do in these cases is create a specific <code>Clone</code> or <code>Copy</code> method when you want to provide that...there may be other things you need to do in order to safely clone your instance, you might as well keep it all in one place....so i would say something like</p> <p>t := NewMyType()</p> <p>t.SetSomething()</p> <p>c := t.Clone()</p> <p>this also prevents a bunch of GC-unfriendly copying of potentially large objects. a <code>Clone</code> method is also a more durable API over time...there&#39;s just one function to call to get a clone, ever. there&#39;s a reason cloning is typically in the base class of any OO language....</p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

721 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传