DEV Community

Ryan Westlund
Ryan Westlund

Posted on

A story of data hiding and why you shouldn't

I wrote an article a while ago titled What's the case for data hiding? where I argued that the concept of private fields and attributes is a misguided one. Today I had an experience with the Go standard library that illustrates exactly why.

Background: our app needs to be able to act as an email client (beacuse our customers don't believe in the Unix philosophy sadly). That means we need to negotiate with different SMTP servers and use an authentication method they support. Here's the hard part: Microsoft Office does not support AUTH PLAIN.

Aside: I'm fucking pissed at Microsoft for this. Here's their article explaining that they dropped support for AUTH PLAIN in 2017. Far as I can tell, AUTH PLAIN superseded AUTH LOGIN 14 years before that. Despite all the evidence being in agreement, I am struggling to believe what happened. How could Microsoft have removed existing support for a new replacement in favor of a standard that was already deprecated 14 years ago? I just have no words 🤬

So anyway, we self-implement AUTH LOGIN as an alternative to AUTH PLAIN, but that leaves an obstacle: our SMTP client library, mailyak, requires passing in an smtp.Auth struct to create a mail struct which means we have to decide which method to use before connecting to the server and finding out which ones it supports.

So off I went to use the building blocks in Go's stdlib net/smtp to implement a function that would connect to a server before we make our mail, and determine what AUTH methods it supports. Here's where the point of the story comes in: almost everything in there is unexported so I can't use it. A Client.Hello method is exported, but doesn't return the results; it stores them in Client.auth (unexported). The underlying Client.cmd is also unexported. In the end I had to basically copy the text of the cmd method instead of using it.

That's what enforced data hiding does. It doesn't stop implementation details from leaving the package, it just duplicates the implementation details.

More general lesson: enforcing things is almost never correct because when you limit someone else's options you don't see what the alternative is. It's true that it's usually a bad idea to access implementation details of a package, but enforcing privateness takes the decision out of the hands of the end-developer, the only one actually qualified to decide as they are the only one aware of the context of the situation.

Discussion (10)

Collapse
yoursunny profile image
Junxiao Shi • Edited on

The purpose of having something unexported is that the library implementer can change it at any time without worrying about breaking dependent code.

There's usually an escape hatch when you really need to: Reflection.
I did that in .Net in 2009: How to specify server IP in HttpWebRequest.

Go's reflection wouldn't allow writing to an unexported field though, but it's still possible if you dare to use unsafe.Pointer.

Collapse
yujiri8 profile image
Ryan Westlund Author

It's standard library code. It's frozen: golang.org/pkg/net/smtp/

But more generally, I still don't think enforced privacy has the benefit you describe, because the benefit can be had with non-enforced privacy. A non-enforced way of marking a field private (perhaps similar to what Python does) can provide a warning to not use it, but still leave the decision in the hands of the person in a position to know what's best.

Collapse
yoursunny profile image
Junxiao Shi

Go doesn't have warnings.
As I said, you can read unexported fields through reflection.

Thread Thread
yujiri8 profile image
Ryan Westlund Author

Go doesn't have warnings.

I wasn't talking about a language feature called warnings.

I don't even know what process you're talking about with reflection, but why should it have to be any more complicated than just accessing the field?

Thread Thread
yoursunny profile image
Junxiao Shi

Look into the reflect package. It allows you to read unexported fields.

Thread Thread
yujiri8 profile image
Ryan Westlund Author

I found out how to do it for simple types but not for a slice. I needed a field of []string, as its actual type and not as an interface{}.

Thread Thread
yoursunny profile image
Junxiao Shi

play.golang.org/p/u5hi1fE6g3_p

package main

import (
    "fmt"
    "reflect"
)

type X struct {
    y []string
}

func main() {
    x := new(X)
    x.y = []string{"yyyy"}

    vx := reflect.ValueOf(x)
    vy := vx.Elem().FieldByName("y")
    vy0 := vy.Index(0)
    y0 := vy0.String()

    fmt.Println(y0)
}
Enter fullscreen mode Exit fullscreen mode

The Index function does the trick.

Thread Thread
yujiri8 profile image
Ryan Westlund Author

So you're telling me there isn't an actual way to convert directly from a reflect.Value that represents a slice to an actual slice type, I would've had to use Len and Index or Slice to rebuild the slice?

Thread Thread
yoursunny profile image
Junxiao Shi

play.golang.org/p/ADAMCB_GuTE

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type X struct {
    y []string
}

func main() {
    x := new(X)
    x.y = []string{"yyyy"}

    vx := reflect.ValueOf(x)
    vy := vx.Elem().FieldByName("y")

    var y []string
    *(*reflect.SliceHeader)(unsafe.Pointer(&y)) = *(*reflect.SliceHeader)(unsafe.Pointer(vy.UnsafeAddr()))

    fmt.Println(y)
}
Enter fullscreen mode Exit fullscreen mode

You can use SliceHeader. However, this requires the unsafe package.

Thread Thread
yujiri8 profile image
Ryan Westlund Author

That is quite some black magic.